#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OCR完整性检查 / OCR Integrity Check ================================== 物理依赖链第一环:OCR → 建表 检查OCR后的.md文件是否有未补全的乱码/占位符。 如果有,拒绝生成checkpoint,建表脚本将因此中止。 用法: python3 ocr-integrity-check.py <工作目录> 检查模式: 1. 公司名乱码:连续3+英文字母出现在中文语境中(如"HAT"代替公司名) 2. 地址残缺:【…】、〔…〕、"了 B}"等占位符模式 3. 关键数字乱码:字母代替数字(如"5 1 te"代替"号1幢") 4. OCR标记残留:OCR模糊、OCR无法识别、待补全等 通过 → 生成 step1.verified checkpoint(含签名) 失败 → 列出所有需要vision补全的字段,不生成checkpoint 退出码:0=通过,1=有问题需补全 🔴 安全机制:checkpoint文件包含HMAC签名,防止手动创建 """ import sys, os, re, json, glob, hmac, hashlib from datetime import datetime # checkpoint签名密钥(固定值,脚本内置) CHECKPOINT_SECRET = b'ocr-integrity-check-v1-2026-06-29' def generate_checkpoint_signature(workdir, md_files, timestamp): """生成checkpoint签名""" # 签名内容:工作目录+文件列表+时间戳 sign_content = f"{workdir}|{','.join(sorted(md_files))}|{timestamp}" sig = hmac.new(CHECKPOINT_SECRET, sign_content.encode('utf-8'), hashlib.sha256).hexdigest() return sig[:16] # 取前16位 def check_ocr_file(md_path): """检查单个.md文件的OCR完整性""" issues = [] with open(md_path, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') # 模式1:公司名乱码 - 连续3+大写字母或混合大小写英文(中文语境中) # 例如:"HAT CP RRA)" 应该是公司名 company_garble = re.finditer(r'[A-Z]{3,}(?:\s+[A-Z]{2,}){0,2}', content) for m in company_garble: # 排除常见的合理英文(如OCR、PDF、API等) if m.group() not in ['OCR', 'PDF', 'API', 'URL', 'HTTP', 'HTTPS', 'JSON', 'XML', 'LOGO', 'EMS', 'WPS', 'PAGE', 'REMARK', 'NOTE', 'TODO', 'FIXME', 'HACK', 'XXX', 'EOF']: # 排除统一社会信用代码中的字母部分(前后有数字的字母序列) pre = content[max(0, m.start()-2):m.start()] post = content[m.end():min(len(content), m.end()+2)] if (pre and pre[-1:].isdigit()) or (post and post[:1].isdigit()): continue # 信用代码中的字母,跳过 line_num = content[:m.start()].count('\n') + 1 context_start = max(0, m.start() - 20) context_end = min(len(content), m.end() + 20) context = content[context_start:context_end].replace('\n', ' ') issues.append({ 'type': '公司名乱码', 'line': line_num, 'pattern': m.group(), 'context': f"...{context}...", 'fix': '用vision看原图,补全公司全称' }) # 模式2:地址残缺占位符 placeholder_patterns = [ (r'【\.{1,5}】', '中文省略号占位符'), (r'【…】', '中文省略号占位符'), (r'〔\.{1,5}〕', '方括号省略号占位符'), (r'了\s*[A-Za-z0-9]}', '地址残缺模式(如"了 B}")'), ] for pat, desc in placeholder_patterns: for m in re.finditer(pat, content): line_num = content[:m.start()].count('\n') + 1 context_start = max(0, m.start() - 20) context_end = min(len(content), m.end() + 20) context = content[context_start:context_end].replace('\n', ' ') issues.append({ 'type': desc, 'line': line_num, 'pattern': m.group(), 'context': f"...{context}...", 'fix': '用vision看原图,补全完整地址' }) # 模式3:OCR标记残留 ocr_markers = [ r'OCR模糊', r'OCR无法识别', r'OCR乱码', r'OCR识别失败', r'待补全', r'〔待核PDF〕', r'〔待核〕', ] for marker in ocr_markers: for m in re.finditer(marker, content): line_num = content[:m.start()].count('\n') + 1 context_start = max(0, m.start() - 15) context_end = min(len(content), m.end() + 15) context = content[context_start:context_end].replace('\n', ' ') issues.append({ 'type': 'OCR标记残留', 'line': line_num, 'pattern': m.group(), 'context': f"...{context}...", 'fix': '用vision看原图补全,删除标记' }) # 模式4:关键字段中的字母代替数字 # 例如:"5 1 te 202" 应该是 "号1幢202" # 检测:中文+空格+单个字母+空格+数字 的模式 digit_garble = re.finditer(r'[\u4e00-\u9fff]\s+[a-zA-Z]\s+\d{2,4}', content) for m in digit_garble: line_num = content[:m.start()].count('\n') + 1 context_start = max(0, m.start() - 10) context_end = min(len(content), m.end() + 10) context = content[context_start:context_end].replace('\n', ' ') issues.append({ 'type': '数字乱码(字母代替)', 'line': line_num, 'pattern': m.group(), 'context': f"...{context}...", 'fix': '用vision看原图,确认正确数字' }) # 模式5:严重乱码段落(连续3+英文无义词,如"peMet iy ecsapae"、"Fak"开头) # 表明该区域OCR完全失败,不可用于模版比对 garble_patterns = re.finditer(r'(?:[a-zA-Z]{3,}\s+){2,}[a-zA-Z]{2,}', content) for m in garble_patterns: # 排除已知英文短语 text = m.group().strip() if any(kw in text.lower() for kw in ['page', 'total', 'remark', 'note']): continue line_num = content[:m.start()].count('\n') + 1 context_start = max(0, m.start() - 15) context_end = min(len(content), m.end() + 15) context = content[context_start:context_end].replace('\n', ' ') issues.append({ 'type': '严重乱码段落(OCR完全失败)', 'line': line_num, 'pattern': text[:40], 'context': f"...{context[:60]}...", 'fix': '该区域OCR不可读,必须vision核实原文后才能写入比对报告' }) return issues def main(): if len(sys.argv) < 2: print("用法: python3 ocr-integrity-check.py <工作目录>") print("例: python3 ocr-integrity-check.py /tmp/人民中路") sys.exit(1) workdir = sys.argv[1] if not os.path.isdir(workdir): print(f"❌ 工作目录不存在: {workdir}") sys.exit(1) # 查找所有OCR产出的.md文件 md_files = glob.glob(os.path.join(workdir, '*.md')) md_files += glob.glob(os.path.join(workdir, 'md', '*.md')) md_files += glob.glob(os.path.join(workdir, 'ocr', '*.md')) if not md_files: print(f"❌ 未找到OCR产出的.md文件: {workdir}") sys.exit(1) print(f"检查 {len(md_files)} 个OCR文件的完整性...\n") all_issues = [] for md_file in md_files: fname = os.path.basename(md_file) issues = check_ocr_file(md_file) if issues: all_issues.append((fname, issues)) checkpoint_path = os.path.join(workdir, 'step1.verified') if all_issues: print("❌ OCR完整性检查失败\n") for fname, issues in all_issues: print(f" {fname}: {len(issues)} 个问题") for i, issue in enumerate(issues[:5], 1): # 只显示前5个 print(f" {i}. [{issue['type']}] 第{issue['line']}行") print(f" {issue['context']}") print(f" → {issue['fix']}") if len(issues) > 5: print(f" ... 还有 {len(issues) - 5} 个问题") print(f"\n⛔ 未生成checkpoint: {checkpoint_path}") print(" 建表脚本将拒绝执行,直到所有问题用vision补全。") # 写入问题清单供后续处理 issues_log = os.path.join(workdir, 'step1.issues.json') with open(issues_log, 'w', encoding='utf-8') as f: json.dump({ 'timestamp': datetime.now().isoformat(), 'issues': {fname: issues for fname, issues in all_issues} }, f, ensure_ascii=False, indent=2) print(f" 问题清单已保存: {issues_log}") sys.exit(1) else: print("✅ OCR完整性检查通过\n") for md_file in md_files: print(f" ✓ {os.path.basename(md_file)}") # 生成checkpoint(含签名) timestamp = datetime.now().isoformat() signature = generate_checkpoint_signature(workdir, md_files, timestamp) with open(checkpoint_path, 'w', encoding='utf-8') as f: f.write(f"OCR完整性检查通过\n") f.write(f"时间: {timestamp}\n") f.write(f"文件数: {len(md_files)}\n") for md_file in md_files: f.write(f" - {os.path.basename(md_file)}\n") f.write(f"签名: {signature}\n") print(f"\n✓ checkpoint已生成: {checkpoint_path}") print(f" 签名: {signature}") sys.exit(0) if __name__ == '__main__': main()