export-software-markdown.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. function main() {
  5. const input = arg('--input');
  6. if (!input) throw new Error('用法:node scripts/export-software-markdown.js --input <software-csv> [--output <markdown>]');
  7. const output = arg('--output') || input.replace(/\.csv$/i, '.md');
  8. const rows = parseCsv(fs.readFileSync(input, 'utf8').replace(/^\uFEFF/, ''))
  9. .filter(row => row.some(cell => String(cell || '').trim()));
  10. if (!rows.length) throw new Error(`CSV 为空:${input}`);
  11. const summary = readSummary(input);
  12. const audit = summary.duplicateAudit || {};
  13. const sourceDuplicateRemovedCount = summary.sourceDuplicateRemovedCount ?? summary.duplicateCount ?? 0;
  14. const finalDuplicateGroupCount = summary.finalDuplicateGroupCount ?? countDuplicateGroups(audit);
  15. const lines = [
  16. '# 软件端去重博主名单',
  17. '',
  18. `来源:\`${path.basename(input)}\``,
  19. '',
  20. `- 原始行数:${summary.sourceRows ?? rows.length - 1}`,
  21. `- 去重后行数:${summary.outputRows ?? rows.length - 1}`,
  22. `- 源表去重丢弃行数:${sourceDuplicateRemovedCount}`,
  23. `- 最终名单重复组数:${finalDuplicateGroupCount}`,
  24. '- 去重口径:软件端交付表按全局博主唯一;同一平台 + 规范化主页链接不重复;没有主页链接时使用同一平台 + 规范化博主名称。',
  25. '- 排名规则:去重后按每个 brief 重新从 1 排名。',
  26. `- 同 brief URL 重复组:${count(audit.sameBriefUrl)}`,
  27. `- 同 brief 名称重复组:${count(audit.sameBriefName)}`,
  28. `- 全局 URL 重复组:${count(audit.globalUrl)}`,
  29. `- 全局平台+名称重复组:${count(audit.globalNamePlatform)}`,
  30. '',
  31. markdownTable(rows)
  32. ];
  33. fs.writeFileSync(output, `\uFEFF${lines.join('\n')}`);
  34. console.log(JSON.stringify({ input, output, rows: rows.length - 1 }, null, 2));
  35. }
  36. function arg(name) {
  37. const index = process.argv.indexOf(name);
  38. return index >= 0 ? process.argv[index + 1] : '';
  39. }
  40. function readSummary(input) {
  41. const summaryPath = input.replace(/\.csv$/i, '.summary.json');
  42. if (!fs.existsSync(summaryPath)) return {};
  43. return JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
  44. }
  45. function count(value) {
  46. return Array.isArray(value) ? value.length : 0;
  47. }
  48. function countDuplicateGroups(audit) {
  49. if (!audit) return 0;
  50. return Object.values(audit).reduce((sum, groups) => sum + count(groups), 0);
  51. }
  52. function markdownTable(rows) {
  53. const [header, ...body] = rows;
  54. const lines = [
  55. `| ${header.map(markdownCell).join(' | ')} |`,
  56. `| ${header.map(() => '---').join(' | ')} |`
  57. ];
  58. for (const row of body) lines.push(`| ${row.map(markdownCell).join(' | ')} |`);
  59. return lines.join('\n');
  60. }
  61. function markdownCell(value) {
  62. return String(value ?? '').replace(/\|/g, '|').replace(/\r?\n/g, ' ').trim();
  63. }
  64. function parseCsv(text) {
  65. const rows = [];
  66. let row = [];
  67. let cell = '';
  68. let quoted = false;
  69. for (let index = 0; index < text.length; index += 1) {
  70. const char = text[index];
  71. if (char === '\r') continue;
  72. if (char === '"' && quoted && text[index + 1] === '"') {
  73. cell += '"';
  74. index += 1;
  75. } else if (char === '"') {
  76. quoted = !quoted;
  77. } else if (char === ',' && !quoted) {
  78. row.push(cell);
  79. cell = '';
  80. } else if (char === '\n' && !quoted) {
  81. row.push(cell);
  82. rows.push(row);
  83. row = [];
  84. cell = '';
  85. } else {
  86. cell += char;
  87. }
  88. }
  89. if (cell || row.length) {
  90. row.push(cell);
  91. rows.push(row);
  92. }
  93. return rows;
  94. }
  95. main();