PDF 混合提取(pdfplumber + glm-ocr)
Overview
年报类 PDF 多为"有文字层"的排版文件,pdfplumber 抽文字/表格又快又准;但它在三处会翻车:扫描/图片页(无文字层)、合并单元格的复杂财报表格、乱码页。盲目全页 OCR 既慢又易把表格结构读丢。
正确姿势:pdfplumber 优先,glm-ocr 只兜底失败页,并缓存 OCR 结果。本技能自带脚本 scripts/annual_report_ocr.py。
运行方式
依赖装在被隔离的 venv(pdfplumber / pypdfium2 / requests):
- venv Python:C:\Users\wangs\.workbuddy\binaries\python\envs\default\Scripts\python.exe
- Ollama 本地服务:http://localhost:11434,模型 glm-ocr:q8_0(智谱,1.1B/Q8/128K/vision)
PY="C:/Users/wangs/.workbuddy/binaries/python/envs/default/Scripts/python.exe"
$PY scripts/annual_report_ocr.py <年报.pdf> # 默认:pdfplumber 优先 + 仅失败页 OCR
$PY scripts/annual_report_ocr.py <年报.pdf> --ocr-all # 强制每页 OCR(最准但慢)
$PY scripts/annual_report_ocr.py <年报.pdf> --ocr-tables # 精:正文 pdfplumber + 仅表格区域 OCR(推荐)
$PY scripts/annual_report_ocr.py <年报.pdf> --pages 5,6 # 仅指定页走 OCR
$PY scripts/annual_report_ocr.py <年报.pdf> --dpi 250 --max-tokens 8192 # 密报表页可加分辨率/上限
输出(与 PDF 同目录):<名>.md(干净正文,带 [pN|pdfplumber|ocr] 标记)+ <名>.json(结构化,含每页来源与表格)。可直接喂给"年报财务数据提取"技能或人工核对。
兜底判定逻辑(page_needs_ocr)
满足任一即走 OCR:① 文本字符数 < 阈值(稀疏/图片页);② 含财报表头(合并资产负债表/利润表/现金流量表等)但 pdfplumber 没抽到表格;③ 乱码(替换字符占比过高);④ 用户用 --ocr-all / --pages 显式指定。
精:表格区域 OCR(--ocr-tables,推荐)
最实用的模式:正文走 pdfplumber、仅表格区域走 OCR。做法:
page.find_tables(vertical_strategy="text", horizontal_strategy="text")定位每张表精确包围盒(按文字对齐检测,财报表不依赖格子线也能整张检出);- pypdfium2 渲染整页 → 按 bbox 裁出表区域 → glm-ocr(
num_ctx=16384); - 过滤:宽 > 200pt 且面积 > 页面 1/8 才送 OCR,避免误判小列片段。
实测(农夫山泉 2025 年报 230 页,5.5 分钟):损益表 1193、财务状况表 2021、现金流量表 2527 字符(比整页 OCR 982/1551/2094 更全,因为模型专注表格区)。适合年报/招股书/财报——表格多且对数字准确性高的场景。
⚠️ 两处必须知道的 glm-ocr 坑(否则产出是垃圾)
调用 Ollama /api/generate 时:
- 必须显式放大
num_ctx(上下文窗口)——这是密表早停的真正根因。glm-ocr 默认num_ctx极小,图像编码占满后几乎无生成余量,模型在表头就 EOS(实测密表只出 ~129 字符表头,且num_predict调到 16384 都无效)。固定options={"num_ctx": 16384, "num_predict": 4096, "temperature": 0}。财报三大表实测num_ctx=16384时单页完整输出 982~2094 字符;不放大num_ctx则永远不全。 num_predict也要放大。默认生成上限极小,不设会被截断成残破 HTML(实测 ~87 字符就停)。4096 起,密表可 8192。- prompt 极度敏感:
- 空 prompt → 不输出任何内容(模型需要 prompt);
- 英文 / 过长中文 prompt(如"严格保留原有排版顺序…不要增删")→ 触发 HTML 格式 且截断;
- ✅ 只有简短中文请完整转录图片中的所有文字,保留表格结构。能稳定输出完整、制表符分隔的结果。
- 缓存键已包含图像+模型+prompt+num_predict+num_ctx,换 prompt/参数不会命中旧(可能残破的)结果。
架构要点
- pdfplumber 负责文字层抽取与表格结构(快、无损);pypdfium2 仅负责把"需 OCR 的页"渲染成 PNG(不参与文字抽取)。
- OCR 是瓶颈且昂贵 → 只给失败页调用,并按页图像哈希缓存,同一份 PDF 重跑或调参后重跑命中页免费。
- 输出为制表符/空格对齐文本,下游易解析为表格。
实战经验:文字层乱码年报(小米 1810.HK,2026-09-09 实测)
小米集团 2021-2025 全部年报 PDF 的字体 ToUnicode 映射损坏,中英文提取出来是"伪汉字"(如「䌪䪍㟺㔺ⷭ」),但数字编码正常。文字层提取(pdfplumber/pdfkit/pymupdf)全部失效,只能 OCR。几个关键经验:
- 乱码类型判定:分两种——① 替换符
\ufffd占比高(pdfplumber 检测不到字符);② CID 映射损坏(伪汉字,主要是 CJK 扩展 A U+3400-U+4DBF 与私有区 U+E000-U+F8FF)。脚本is_garbled已兼容两种(② 类单独统计罕见字符占比)。 - 页码定位改用 PDF 书签:乱码时
search_text搜不到关键词,用 PyMuPDFdoc.get_toc()读书签(结构化元数据不受乱码影响)定位「合併損益表/合併權益變動表/合併現金流量表」等章节页码。 - 数字层可交叉验证:乱码 PDF 的数字通常正常,用
page.get_text("words")提取数字做 OCR 结果的核对/补漏(实测靠它确认了 OCR 漏读的「因攤薄 (12,117)」)。 - 全 PDF 乱码时用
--pages指定报表页:is_garbled判乱码后会自动 OCR,但脚本仍会遍历全部分页跑 pdfplumber(慢);已知报表页码范围时直接--pages a-b只 OCR 目标页,可大幅提速(五份年报共 45 页 OCR 约 40 分钟)。 - OCR 输出格式不统一:同一批年报里,损益表可能输出 Markdown 表格(
| 收入 | 5 | 457,286,687 | ... |),而 OCI/BS/CF 输出空格分隔文本流(逐行或同行混合)。下游解析器须兼容两种格式。 - temperature=0 下偶发重复生成:同一内容块会重复输出 1-8 次(整块以 ``` 分隔,或行级重叠),需按"最长实质块 + 条目级整段去重"处理。
- 系统性误读:常见如「權益法入賬」读成「法人賬」。跨年对照(年报 N 的上年列 vs 年报 N-1 的当年列)能兜底这类问题。
未做(v2 候选,按需扩展)
- 单元格级 OCR:pdfplumber 切出表格单元格包围盒 → 逐格裁图送 glm-ocr,把 pdfplumber 的"结构"与 OCR 的"字符准确率"结合,进一步提财报三大表准确率。
- 包成 WorkBuddy 连接器/技能触发器,从对话直接调起。
```