2
0

diff.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. <?php
  2. /**
  3. * 两张 related 表的逐段落差异报告。
  4. *
  5. * 迭代的收敛判据是「每一条 diff 都能归因」——要么是修好了,要么是已知留空,不允许
  6. * 有说不清来源的差异。所以这里不只给总数,还按类型分桶并各留样本,供逐轮核对。
  7. *
  8. * 用法:
  9. * php scripts/related/diff.php --old=related_paragraphs_bak_20260909 --new=related_paragraphs_v1
  10. * php scripts/related/diff.php --new=related_paragraphs_v1 --book=98 --samples=40
  11. */
  12. require __DIR__.'/lib/boot.php';
  13. use Illuminate\Support\Facades\DB;
  14. [$options] = rp_args($argv);
  15. $old = rp_assert_table($options['old'] ?? 'related_paragraphs_bak_20260909');
  16. $new = rp_assert_table($options['new'] ?? 'related_paragraphs_v1');
  17. $sampleLimit = (int) ($options['samples'] ?? 15);
  18. $onlyBooks = isset($options['book']) ? array_map('intval', explode(',', $options['book'])) : null;
  19. $log = new RpLogger('diff', rp_report_dir());
  20. $log->info("旧:$old");
  21. $log->info("新:$new");
  22. $bookFilter = $onlyBooks ? 'and book in ('.implode(',', $onlyBooks).')' : '';
  23. $log->info('比对范围:'.($onlyBooks ? implode(',', $onlyBooks) : '全库'));
  24. // 每个 (book, para) 压成一行「书名:cs 列表」的签名,逐段落比对。
  25. $sql = "
  26. with o as (
  27. select book, para,
  28. string_agg(distinct book_name || ':' || cs_para, ',' order by book_name || ':' || cs_para) as sig,
  29. count(*) as rows
  30. from $old where true $bookFilter group by book, para
  31. ),
  32. n as (
  33. select book, para,
  34. string_agg(distinct book_name || ':' || cs_para, ',' order by book_name || ':' || cs_para) as sig,
  35. count(*) as rows
  36. from $new where true $bookFilter group by book, para
  37. )
  38. select
  39. coalesce(o.book, n.book) as book,
  40. coalesce(o.para, n.para) as para,
  41. o.sig as old_sig, n.sig as new_sig,
  42. coalesce(o.rows, 0) as old_rows, coalesce(n.rows, 0) as new_rows,
  43. case
  44. when o.book is null then 'only_in_new'
  45. when n.book is null then 'only_in_old'
  46. when o.sig = n.sig then 'same'
  47. else 'changed'
  48. end as verdict
  49. from o full outer join n on n.book = o.book and n.para = o.para
  50. ";
  51. $log->info('比对中…');
  52. $buckets = [];
  53. $samples = [];
  54. $perBook = [];
  55. $total = 0;
  56. foreach (DB::cursor($sql) as $row) {
  57. $total++;
  58. $verdict = $row->verdict;
  59. if ($verdict === 'changed') {
  60. // 细分:只是 cs 变了,还是书名也变了
  61. $oldNames = [];
  62. $newNames = [];
  63. foreach (explode(',', (string) $row->old_sig) as $pair) {
  64. $oldNames[substr($pair, 0, strrpos($pair, ':'))] = true;
  65. }
  66. foreach (explode(',', (string) $row->new_sig) as $pair) {
  67. $newNames[substr($pair, 0, strrpos($pair, ':'))] = true;
  68. }
  69. $verdict = array_keys($oldNames) === array_keys($newNames) ? 'cs_changed' : 'name_changed';
  70. }
  71. $buckets[$verdict] = ($buckets[$verdict] ?? 0) + 1;
  72. if ($verdict !== 'same') {
  73. $perBook[$row->book][$verdict] = ($perBook[$row->book][$verdict] ?? 0) + 1;
  74. if (count($samples[$verdict] ?? []) < $sampleLimit) {
  75. $samples[$verdict][] = [
  76. 'book' => (int) $row->book, 'para' => (int) $row->para,
  77. 'old' => $row->old_sig, 'new' => $row->new_sig,
  78. ];
  79. }
  80. }
  81. }
  82. ksort($buckets);
  83. $log->info('---- 差异分桶(按段落计) ----');
  84. foreach ($buckets as $verdict => $n) {
  85. $log->info(sprintf(' %-14s %8d %5.1f%%', $verdict, $n, $total ? $n / $total * 100 : 0));
  86. }
  87. foreach ($samples as $verdict => $items) {
  88. $log->info("---- $verdict 样本 ----");
  89. foreach ($items as $s) {
  90. $log->info(sprintf(' book %-3d para %-6d 旧[%s] → 新[%s]',
  91. $s['book'], $s['para'], $s['old'] ?? '—', $s['new'] ?? '—'));
  92. }
  93. }
  94. // 差异最集中的书优先看
  95. $ranked = [];
  96. foreach ($perBook as $book => $b) {
  97. $ranked[] = ['book' => (int) $book, 'total' => array_sum($b)] + $b;
  98. }
  99. usort($ranked, fn ($a, $b) => $b['total'] <=> $a['total']);
  100. $log->info('---- 差异最多的 20 本 ----');
  101. foreach (array_slice($ranked, 0, 20) as $r) {
  102. $log->info(sprintf(' book %-3d 共 %-6d %s', $r['book'], $r['total'],
  103. json_encode(array_diff_key($r, ['book' => 1, 'total' => 1]), JSON_UNESCAPED_UNICODE)));
  104. }
  105. $path = rp_report_dir()."/diff.$new.json";
  106. rp_json_write($path, [
  107. 'generated_at' => date('c'),
  108. 'old_table' => $old, 'new_table' => $new,
  109. 'scope' => $onlyBooks ?? 'all',
  110. 'paragraphs_compared' => $total,
  111. 'buckets' => $buckets,
  112. 'per_book' => $ranked,
  113. 'samples' => $samples,
  114. ]);
  115. $log->info("报告:$path");
  116. $log->finish();