#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 把意見書 PDF 的文字抽出並填入筆記中的 OPINION_TEXT_PLACEHOLDER。 品質檢查:中文字比例過低者視為抽取失敗,改走 OCR(tesseract chi_tra), 仍失敗則在筆記中明白標示「PDF 文字層無法擷取」,絕不留下空白或假內容。 """ import json, re, subprocess, sys, os, shutil, tempfile from pathlib import Path CJK = re.compile(r'[一-鿿]') def pdftotext(p): try: r = subprocess.run(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', str(p), '-'], capture_output=True, timeout=180) return r.stdout.decode('utf-8', 'replace') except Exception: return '' def ocr(p): if not shutil.which('tesseract') or not shutil.which('pdftoppm'): return '' with tempfile.TemporaryDirectory() as td: subprocess.run(['pdftoppm', '-r', '200', '-png', str(p), td + '/pg'], capture_output=True, timeout=900) chunks = [] for img in sorted(Path(td).glob('pg*.png')): r = subprocess.run(['tesseract', str(img), 'stdout', '-l', 'chi_tra'], capture_output=True, timeout=300) chunks.append(r.stdout.decode('utf-8', 'replace')) return '\n'.join(chunks) def quality(t): t2 = re.sub(r'\s', '', t) if len(t2) < 80: return 0.0 return len(CJK.findall(t2)) / len(t2) def clean(t): t = t.replace('\r\n', '\n').replace('\x0c', '\n') t = re.sub(r'\n{3,}', '\n\n', t) return t.strip() def main(pdfdir, notesroot, manifest_path): pdfdir, notesroot = Path(pdfdir), Path(notesroot) manifest = json.load(open(manifest_path, encoding='utf-8')) by_file = {} for key, items in manifest.items(): for it in items: by_file[it['檔名']] = (key, it['下載id'], it['標題']) notes = {p.stem: p for p in list((notesroot / '10_釋字').glob('*.md')) + list((notesroot / '20_憲判').glob('*.md'))} # key(釋字0742)→ 筆記檔 def note_for(key): if key.startswith('釋字'): return notes.get('釋字第%04d號' % int(key[2:])) m = re.match(r'憲判(\d+)-(\d+)', key) if m: return notes.get('%d年憲判字第%02d號' % (int(m.group(1)), int(m.group(2)))) return None cache, stats = {}, {'ok': 0, 'ocr': 0, 'fail': 0, 'missing_note': 0} pdfs = sorted(pdfdir.rglob('*.pdf')) for i, p in enumerate(pdfs, 1): meta = by_file.get(p.name) if not meta: continue key, dlid, title = meta txt = clean(pdftotext(p)) q = quality(txt) if q < 0.25: o = clean(ocr(p)) if quality(o) >= 0.25: txt, q = o, quality(o) txt += '\n\n' stats['ocr'] += 1 else: stats['fail'] += 1 txt = '' else: stats['ok'] += 1 cache.setdefault(key, {})[dlid] = (txt, q) if i % 200 == 0: print(f' {i}/{len(pdfs)}', flush=True) for key, m in cache.items(): np_ = note_for(key) if not np_: stats['missing_note'] += 1 continue s = np_.read_text(encoding='utf-8') for dlid, (txt, q) in m.items(): ph = f'' if ph not in s: continue body = txt if txt else '> [!failure] 本篇意見書之 PDF 無可擷取文字層,且 OCR 亦未通過品質門檻。請點上方原始檔連結查閱官網 PDF。' s = s.replace(ph, body) np_.write_text(s, encoding='utf-8') print(json.dumps(stats, ensure_ascii=False)) if __name__ == '__main__': main(sys.argv[1], sys.argv[2], sys.argv[3])