const http = require("http"); const fs = require("fs"); const path = require("path"); const XLSX = require("xlsx"); const formidable = require("formidable"); const PORT = process.env.PORT || 3000; const DATA_DIR = path.join(__dirname, "uploads"); const ANALYSIS_FILE = path.join(__dirname, "analysis.json"); if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); function readAnalysis() { try { return JSON.parse(fs.readFileSync(ANALYSIS_FILE, "utf8")); } catch (_) { return null; } } function writeAnalysis(data) { fs.writeFileSync(ANALYSIS_FILE, JSON.stringify(data, null, 2)); } function body(req) { return new Promise((resolve) => { let s = ""; req.on("data", (c) => (s += c)); req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } }); }); } function parseFile(filepath) { const ext = path.extname(filepath).toLowerCase(); if (ext === ".csv") { const content = fs.readFileSync(filepath, "utf8"); const lines = content.split(/\r?\n/).filter(l => l.trim()); if (lines.length < 2) return { columns: [], rows: [] }; const parseLine = (line) => { const result = []; let current = '', inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === ',' && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; }; const columns = parseLine(lines[0]); const rows = lines.slice(1).map(line => { const values = parseLine(line); const obj = {}; columns.forEach((col, i) => { obj[col] = values[i] || ''; }); return obj; }); return { columns, rows }; } else { const wb = XLSX.readFile(filepath); const sheet = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(sheet); const columns = rows.length > 0 ? Object.keys(rows[0]) : []; return { columns, rows }; } } function calculateStats(rows, columns) { const stats = {}; const numericCols = columns.filter(col => { const sample = rows.slice(0, 10).map(r => r[col]); return sample.some(v => !isNaN(parseFloat(v)) && isFinite(v)); }); numericCols.forEach(col => { const values = rows.map(r => parseFloat(r[col])).filter(v => !isNaN(v)); if (values.length > 0) { const sum = values.reduce((a, b) => a + b, 0); const avg = sum / values.length; const min = Math.min(...values); const max = Math.max(...values); const sorted = [...values].sort((a, b) => a - b); const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; stats[col] = { sum, avg, min, max, median, count: values.length }; } }); return stats; } function generateInsights(rows, columns, stats) { const insights = []; const totalRows = rows.length; if (totalRows > 0) { insights.push({ type: "info", text: `Всего записей: ${totalRows}`, category: "Общие данные" }); } Object.keys(stats).forEach(col => { const s = stats[col]; if (s.count > totalRows * 0.5) { insights.push({ type: "metric", text: `Среднее по "${col}": ${s.avg.toFixed(2)}`, category: "Статистика", value: s.avg }); if (s.max > s.avg * 2) { insights.push({ type: "warning", text: `Выявлен аномальный максимум в "${col}": ${s.max} (в ${((s.max/s.avg).toFixed(1))} раз выше среднего)`, category: "Аномалии" }); } } }); const stringCols = columns.filter(col => !stats[col]); stringCols.forEach(col => { const values = rows.map(r => r[col]).filter(v => v); const unique = [...new Set(values)]; if (unique.length < totalRows * 0.3 && unique.length > 1) { const counts = {}; values.forEach(v => { counts[v] = (counts[v] || 0) + 1; }); const top = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 3); if (top.length > 0) { insights.push({ type: "info", text: `Наиболее частые значения в "${col}": ${top.map(([k, v]) => `${k} (${v})`).join(', ')}`, category: "Категории" }); } } }); if (insights.length === 0) { insights.push({ type: "info", text: "Загрузите данные для получения аналитических выводов", category: "Рекомендации" }); } return insights; } http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); } if (req.method === "GET" && req.url === "/api/analysis") { const analysis = readAnalysis(); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify(analysis || { columns: [], rows: [], stats: {}, insights: [] })); } if (req.method === "POST" && req.url === "/api/upload") { const form = formidable({ uploadDir: DATA_DIR, keepExtensions: true }); form.parse(req, async (err, fields, files) => { if (err) { res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: err.message })); } const file = files.file && files.file[0]; if (!file) { res.writeHead(400, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Файл не загружен" })); } try { const { columns, rows } = parseFile(file.filepath); const stats = calculateStats(rows, columns); const insights = generateInsights(rows, columns, stats); const analysis = { columns, rows, stats, insights, filename: file.originalFilename }; writeAnalysis(analysis); res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ ok: true, columns, rows: rows.slice(0, 100), stats, insights })); } catch (e) { res.writeHead(500, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: e.message })); } }); return; } if (req.method === "GET" && req.url.startsWith("/api/data")) { const analysis = readAnalysis(); if (!analysis) { res.writeHead(404, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ error: "Данные не загружены" })); } const url = new URL(req.url, "http://localhost"); const filterCol = url.searchParams.get("filterColumn"); const filterVal = url.searchParams.get("filterValue"); const sortCol = url.searchParams.get("sortColumn"); const sortOrder = url.searchParams.get("sortOrder") || "asc"; const limit = parseInt(url.searchParams.get("limit")) || 100; let filtered = [...analysis.rows]; if (filterCol && filterVal) { filtered = filtered.filter(r => String(r[filterCol] || "").toLowerCase().includes(filterVal.toLowerCase())); } if (sortCol) { filtered.sort((a, b) => { const aVal = parseFloat(a[sortCol]) || a[sortCol]; const bVal = parseFloat(b[sortCol]) || b[sortCol]; if (aVal < bVal) return sortOrder === "asc" ? -1 : 1; if (aVal > bVal) return sortOrder === "asc" ? 1 : -1; return 0; }); } res.writeHead(200, { "Content-Type": "application/json" }); return res.end(JSON.stringify({ columns: analysis.columns, rows: filtered.slice(0, limit), total: filtered.length, stats: analysis.stats, insights: analysis.insights })); } let file = req.url === "/" ? "/index.html" : req.url.split("?")[0]; const full = path.join(__dirname, file); if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) { const ext = path.extname(full); const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html"; res.writeHead(200, { "Content-Type": type + "; charset=utf-8" }); return res.end(fs.readFileSync(full)); } res.writeHead(404); res.end("Not found"); }).listen(PORT, () => console.log("Аналитическое приложение на порту " + PORT));