_tmp_pairing_audit.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. # -*- coding: utf-8 -*-
  2. """
  3. 配对审计 — 验证 Excel 每行的 (店名, 药品标题, 价格) 是否真的来自同一张卡片
  4. =============================================================================
  5. 方法: 取行的来源截图 → PP-OCR 独立重读 → 几何枚举卡片(标题/价格/店铺) →
  6. 行的三元组与图上卡片逐一配对(价格精确 + 店名容错 + 标题重叠)。
  7. 当前图配不上时,依次尝试该批次的时间戳备份图(防批次覆盖)。
  8. 用法: python _tmp_pairing_audit.py [样本行数,默认60]
  9. """
  10. import random
  11. import re
  12. import sys
  13. from pathlib import Path
  14. ROOT = Path(__file__).parent
  15. sys.path.insert(0, str(ROOT))
  16. from audit_ocr import load_excel, norm, price_num, shop_match, enumerate_cards
  17. from ai_helper_vision1 import VisionParser1
  18. def title_overlap(a, b, min_len=6):
  19. """最长公共子串 >= min_len 视为同一商品标题(容忍前后缀差异)"""
  20. a, b = norm(a), norm(b)
  21. best = 0
  22. prev = [0] * (len(b) + 1)
  23. for ca in a:
  24. cur = [0]
  25. for j, cb in enumerate(b, 1):
  26. cur.append(prev[j - 1] + 1 if ca == cb else 0)
  27. best = max(best, cur[-1])
  28. prev = cur
  29. return best >= min_len
  30. def main():
  31. n_sample = int(sys.argv[1]) if len(sys.argv) > 1 else 60
  32. rows = load_excel(ROOT / "采集数据" / "20260909.xlsx")
  33. src_rows = [r for r in rows if r["src"]]
  34. # 分层抽样: 按来源排序后等距取,保证4台设备/各药品都覆盖
  35. src_rows_sorted = sorted(src_rows, key=lambda r: r["src"])
  36. step = max(1, len(src_rows_sorted) // n_sample)
  37. sample = src_rows_sorted[::step][:n_sample]
  38. print(f"可追溯行 {len(src_rows)}, 抽样 {len(sample)} 行进行配对审计\n")
  39. parser = VisionParser1()
  40. card_cache = {} # 图片路径 -> 枚举卡片
  41. confirmed = unconfirmed = 0
  42. fail_rows = []
  43. def cards_of(dev, batch):
  44. """该批次所有候选图(当前图+时间戳副本)的枚举卡片(带缓存)"""
  45. out = []
  46. sdir = ROOT / "screenshots" / dev / "step3"
  47. cands = [sdir / f"step3_b{batch}.png"]
  48. cands += sorted(sdir.glob(f"step3_b{batch}_*.png"), key=lambda p: p.stat().st_mtime)
  49. for img in cands:
  50. if not img.exists():
  51. continue
  52. key = str(img)
  53. if key not in card_cache:
  54. try:
  55. card_cache[key] = enumerate_cards(parser._ppocr_blocks(key, 0))
  56. except Exception as e:
  57. print(f" [OCR失败] {img.name}: {e}")
  58. card_cache[key] = []
  59. out.append((key, card_cache[key]))
  60. return out
  61. for idx, r in enumerate(sample, 1):
  62. dev, batch = r["src"].split("/step3_b")
  63. pn = price_num(r["price"])
  64. print(f"[{idx}/{len(sample)}] {r['src']} ¥{pn} {r['shop'][:12]} | {r['title'][:22]}", flush=True)
  65. hit = False
  66. tried = 0
  67. for key, cards in cards_of(dev, batch):
  68. tried += 1
  69. for c in cards:
  70. if (c["price"] == pn and shop_match(c["shop"], r["shop"])
  71. and title_overlap(c["title"], r["title"])):
  72. hit = True
  73. break
  74. if hit:
  75. break
  76. if hit:
  77. confirmed += 1
  78. print(f" ✓ 配对确认 (试了{tried}张候选图)")
  79. else:
  80. unconfirmed += 1
  81. fail_rows.append(r)
  82. print(f" ✗ 未能在来源图上找到配对卡片")
  83. if idx % 10 == 0:
  84. print(f" --- 进度: 确认{confirmed} 未确认{unconfirmed} ---", flush=True)
  85. n = confirmed + unconfirmed
  86. print("\n" + "=" * 70)
  87. print(f"配对审计结果: {confirmed}/{n} = {confirmed/n*100:.1f}% 的行 (店名+药品+价格) 三元组能在来源截图的同一张卡上得到验证")
  88. if fail_rows:
  89. print("未确认行(需人工看图/可能是批次覆盖或真错配):")
  90. for r in fail_rows:
  91. print(f" [{r['sheet']}] {r['title'][:30]} {r['price']} 店:{r['shop'][:16]} 来源:{r['src']}")
  92. if __name__ == "__main__":
  93. main()