#!/usr/bin/env node const fs = require('fs'); const path = require('path'); function main() { const input = arg('--input'); if (!input) throw new Error('用法:node scripts/export-software-markdown.js --input [--output ]'); const output = arg('--output') || input.replace(/\.csv$/i, '.md'); const rows = parseCsv(fs.readFileSync(input, 'utf8').replace(/^\uFEFF/, '')) .filter(row => row.some(cell => String(cell || '').trim())); if (!rows.length) throw new Error(`CSV 为空:${input}`); const summary = readSummary(input); const audit = summary.duplicateAudit || {}; const sourceDuplicateRemovedCount = summary.sourceDuplicateRemovedCount ?? summary.duplicateCount ?? 0; const finalDuplicateGroupCount = summary.finalDuplicateGroupCount ?? countDuplicateGroups(audit); const lines = [ '# 软件端去重博主名单', '', `来源:\`${path.basename(input)}\``, '', `- 原始行数:${summary.sourceRows ?? rows.length - 1}`, `- 去重后行数:${summary.outputRows ?? rows.length - 1}`, `- 源表去重丢弃行数:${sourceDuplicateRemovedCount}`, `- 最终名单重复组数:${finalDuplicateGroupCount}`, '- 去重口径:软件端交付表按全局博主唯一;同一平台 + 规范化主页链接不重复;没有主页链接时使用同一平台 + 规范化博主名称。', '- 排名规则:去重后按每个 brief 重新从 1 排名。', `- 同 brief URL 重复组:${count(audit.sameBriefUrl)}`, `- 同 brief 名称重复组:${count(audit.sameBriefName)}`, `- 全局 URL 重复组:${count(audit.globalUrl)}`, `- 全局平台+名称重复组:${count(audit.globalNamePlatform)}`, '', markdownTable(rows) ]; fs.writeFileSync(output, `\uFEFF${lines.join('\n')}`); console.log(JSON.stringify({ input, output, rows: rows.length - 1 }, null, 2)); } function arg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : ''; } function readSummary(input) { const summaryPath = input.replace(/\.csv$/i, '.summary.json'); if (!fs.existsSync(summaryPath)) return {}; return JSON.parse(fs.readFileSync(summaryPath, 'utf8')); } function count(value) { return Array.isArray(value) ? value.length : 0; } function countDuplicateGroups(audit) { if (!audit) return 0; return Object.values(audit).reduce((sum, groups) => sum + count(groups), 0); } function markdownTable(rows) { const [header, ...body] = rows; const lines = [ `| ${header.map(markdownCell).join(' | ')} |`, `| ${header.map(() => '---').join(' | ')} |` ]; for (const row of body) lines.push(`| ${row.map(markdownCell).join(' | ')} |`); return lines.join('\n'); } function markdownCell(value) { return String(value ?? '').replace(/\|/g, '|').replace(/\r?\n/g, ' ').trim(); } function parseCsv(text) { const rows = []; let row = []; let cell = ''; let quoted = false; for (let index = 0; index < text.length; index += 1) { const char = text[index]; if (char === '\r') continue; if (char === '"' && quoted && text[index + 1] === '"') { cell += '"'; index += 1; } else if (char === '"') { quoted = !quoted; } else if (char === ',' && !quoted) { row.push(cell); cell = ''; } else if (char === '\n' && !quoted) { row.push(cell); rows.push(row); row = []; cell = ''; } else { cell += char; } } if (cell || row.length) { row.push(cell); rows.push(row); } return rows; } main();