Scanned PDF Extractor(扫描版 PDF 文字提取)
从扫描版/纯图片 PDF 提取文字:逐页渲染为 PNG,用配置的 OCR 引擎识别,输出结构化 Markdown。v3.0.2 SKILL.md 全面中文化;v3.0.1 修复 paddle 引擎输出顺序 bug(blocks 按 y 重排摧毁多栏阅读顺序)并新增 OCR 重复行去重;v3.0 新增多栏阅读顺序还原、OCR 阶段断点续传、deskew 加固、自检脚本;v2.0 新增自动纠偏、可插拔 OCR 引擎适配层、可选表格结构化还原。
适用场景
以下任一信号成立即使用本技能:
- 用户明确说 PDF 是扫描件,或要求从一份看起来是扫描件的 PDF(无可选中文字)提取文字。
pdfplumber对每一页都返回空字符串。markitdown返回空的text_content。PyMuPDF(fitz)的page.get_text()每页都返回"",而page.get_images()每页 ≥ 1 张图。- 直接用 Read 工具读 PDF 报错 "Cannot display content of binary file"。
- 用户一次要处理多个扫描 PDF(批量)。
- 上次 OCR 中断,需要从断点继续(v3.0:OCR 阶段也支持续传,不再只有渲染阶段)。
- 扫描页明显歪斜,需要先纠偏再识别(v2.0)。
- 用户希望表格区域还原为结构化 Markdown 表格而非自由文本(v2.0,需 PaddleOCR)。
- 文档是双栏/多栏排版,阅读顺序重要(v3.0)。
工作流程
Step 1 — 探测 + 渲染 + 纠偏:scripts/extract.py
运行内置脚本(支持批量、续传、纠偏):
python <skill_dir>/scripts/extract.py <pdf1> [pdf2 ...] [options]
常用参数:
- --out DIR — 自定义输出目录(默认 <cwd>/.pdf_ocr_work/,私有隐藏目录)。
- --dpi N — 覆盖渲染 DPI(默认 200;中文最低 150,高于 300 无收益)。
- --resume / --no-resume — 启用/关闭续传(默认读配置)。
- --no-deskew — 关闭页面纠偏(v2.0,默认开启)。
- --probe-only — 只探测是否扫描件,不渲染。
- --ocr-engine {vision|paddle|tesseract} — 选择 OCR 引擎(v2.0,默认 vision)。
- --table-restore {auto|on|off} — 表格结构化还原(v2.0,默认 auto)。
- --config PATH — 指定 config.yaml;缺省自动加载脚本旁边的。
- --ocr-status <page_dir> —(v3.0)报告哪些页已有 ocr_page_NNN.md、哪些还差。
- --merge-ocr <page_dir> —(v3.0)把已有 ocr_page_NNN.md 合并为 ocr_result.md,不执行 OCR。
脚本对每页做的事:
1. 探测 — 统计 text_len 与 images;标记扫描页。
2. 渲染 — 按 --dpi 输出 <out>/<pdf_stem>/page_NNN.png。
3. 纠偏(v2.0,v3.0 加固) — 启用时通过降采样灰度图的投影剖面方差最大化估计旋转角(±15° 范围),再将全分辨率图片转正。v3.0 旋转前过三道门:(a) 页面须像文字页(前景密度 + 短水平行程计数);(b) 最佳方差须比 0° 方差提升 ≥1.3 倍;(c) 上/下半页独立估角须与整页估角一致(差 ≤2.5°)。照片、图表、空白页静默跳过。旋转角度与逐页日志写入 manifest.json。
4. 续传 — 已渲染页通过 .progress.json 跳过。
5. OCR 调度(v2.0,v3.0 起始终输出) — 渲染完成后总是调用引擎适配层(scripts/ocr_engine.py)。vision 模式打印调度提示(含已有 ocr_page_NNN.md 的页数);paddle/tesseract 真正执行 OCR,逐页写 ocr_page_NNN.md + .ocr_progress.json,最后合并 ocr_result.md。
非扫描 PDF 会被跳过并提示——那些用普通 pdfplumber/markitdown 处理。
脚本不可用或未装 PyMuPDF(pip install pymupdf)时,降级为内联 Python:用 page.get_text() / page.get_images() 探测,page.get_pixmap(dpi=200) 渲染到工作目录。装 opencv-python-headless 启用纠偏;装 paddlepaddle paddleocr "paddlex[ocr]"(Python 3.12 venv)启用 paddle 引擎与表格还原。
Step 2 — OCR
默认引擎 vision — 对每张 page_NNN.png 调 Read 工具,每条消息批量 batch_size 页(默认 5)并行识别以省往返。Read 返回识别的文字,含中文、表格与版面。
v3.0 vision 会话续传约定:每完成一页的 OCR,把结果写入 <page_dir>/ocr_page_NNN.md(与 PNG 同名,.png→.md)。中断后重跑 Step 1,extract.py --ocr-status <page_dir> 会报告哪些页已有结果、哪些还差;全部完成后 extract.py --merge-ocr <page_dir> 合并为 ocr_result.md。这样 vision 模式也获得了和本地引擎一样的断点续传能力。
若启用了 paddle 引擎,脚本已逐页产出 ocr_page_NNN.md 并合并好 ocr_result.md — 直接检查该文件,不必重跑 Read。重跑脚本会跳过 .ocr_progress.json 里已记录的页。
大文档(>20 页)逐批 OCR、结果流式写入 ocr_page_NNN.md,避免上下文溢出。
Step 3 — 核验
OCR 永远不是 100%——交付前必须核验:
- 关键字段(编号、日期、金额、姓名、印章/签字)不确定时,回读对应 PNG 区域复核。
- 低置信或无法辨认的片段,内联标注
[OCR疑点: <最可能内容>]——绝不静默猜测。 - 涉及数字的法务/财务文档,抽查数字与原页图片是否一致。
paddle引擎开启table_restore: on|auto时,表格单元格内容须对照 PNG 核验——PP-StructureV3 对复杂合并单元格偶发错列(同表不同 run 结果可能不同;属模型上限,非代码 bug)。
Step 4 — 写结构化 Markdown
每份文档写一个 Markdown 文件,包含:
- 文档元信息头(标题、编号、签发日期、签发机构等可见信息)
- 原始分页标记(<!-- 第 N 页 -->)
- 表格重建为 Markdown 管道表格(paddle 引擎自动;vision 引擎靠提示工程)
- 脚注/印章/签字明确标注,[OCR疑点] 标记全部保留
保存到 <out>/<pdf_stem>/ 目录,.md 扩展名,文件名与文档主题一致。
Step 5 — 呈现结果
用 present_files 工具:
1. Markdown 文件(主交付物)
2. PNG 页图(辅助,供目视核验)
对话回复用紧凑表格总结文档要点——条目/要点式,不要全文复述。
配置(config.yaml)
| 键 | 默认值 | 含义 |
|---|---|---|
dpi |
200 |
渲染分辨率(合理区间 150–300) |
language |
zh |
OCR 语言提示:zh、en、zh-en |
output_format |
md |
md、txt 或 both |
export_png |
true |
写完 Markdown 后保留渲染 PNG |
batch_size |
5 |
Read 工具每轮识别的页数 |
resume |
true |
渲染中断后续传 |
privacy_dir |
true |
输出到私有目录 <cwd>/.pdf_ocr_work/ |
deskew (v2.0) |
true |
渲染前自动纠正页面旋转 |
ocr_engine (v2.0) |
vision |
vision / paddle / tesseract |
table_restore (v2.0) |
auto |
auto(装了 PaddleOCR 即开)/ on / off |
ocr_resume (v3.0) |
true |
OCR 阶段续传:逐页 ocr_page_NNN.md + .ocr_progress.json |
multi_column (v3.0) |
true |
多栏阅读顺序还原(paddle/tesseract) |
OCR 引擎
| 引擎 | 依赖 | 优势 | 限制 |
|---|---|---|---|
vision(默认) |
无(用 Read 工具) | 零安装、中文准确率高、天然处理版面与顺序 | 大文档较慢、无结构化表格输出;续传为会话级(靠 --merge-ocr) |
paddle |
pip install paddlepaddle paddleocr "paddlex[ocr]"(仅 Python 3.12) |
本地高速、表格结构化还原(PP-StructureV3)、多栏顺序、逐页 OCR 续传 | 安装重(约 1 GB + 模型),需 Python 3.12 venv(不支持 3.13) |
tesseract |
系统 tesseract + pip install pytesseract |
轻量,image_to_data 给词级坐标 → 多栏顺序 + 续传 | 中文弱、无表格还原 |
用 config.yaml: ocr_engine: paddle 或 --ocr-engine paddle 切换。PaddleEngine 按 PaddleOCR 3.7.0 实测编写(实测通过);其他版本遇 API 不匹配,查 scripts/ocr_engine.py:PaddleEngine。
隐私
扫描版 PDF 常是合同/证件/敏感文件:
- 全程本地处理;渲染与输出都留在用户机器上。
- 默认输出到私有隐藏目录(
.pdf_ocr_work/),不进桌面/下载。 - 除非用户要求,不要把全文贴进对话——对话只给摘要,全文进文件。
- 永远不修改原始 PDF。
参考文档
内置参考文档(按需读取——不要一次全加载):
references/quick_start.md— 3 分钟最小路径 + 命令速查 + 12 条触发句references/faq_and_edge_cases.md— 7 条 FAQ、6 条反模式、7 类边界案例及处理references/fact_discipline.md— OCR 输出信息分级纪律(合同/票据/证件场景刚需):金额/日期/证号逐字核对规则、[OCR疑点]标注规范、交付前自检清单exemplars/two-column-contract.md— 双栏扫描合同真实示例:输入描述 → 实际命令 → 未修改输出 → 要点
内置资源
scripts/extract.py— 探测 + 批量渲染 + 续传 + 纠偏(v3.0 加固)+--ocr-status/--merge-ocr(需pymupdf;纠偏需opencv-python-headless)。scripts/ocr_engine.py— OCR 引擎适配层(v3.0.1):调度 vision / paddle / tesseract;共享reorder_columns(多栏)+interleave_blocks(文字+表格统一排序)+ 逐页 OCR 续传 + 合并 + 重复行去重。paddle 路径需paddleocr;tesseract 路径需pytesseract+ 系统tesseract。scripts/selftest.py(v3.0.1)— 自检脚本:运行时生成 4 类 fixture(正/倾斜/照片/模糊),跑 probe+deskew+resume+多栏+文字表格交织+OCR续传+引擎调度全链路。发版前必跑:python scripts/selftest.py [PYTHON_BIN]。references/+exemplars/— 见上文参考文档。
关键陷阱
- 不要假设 PDF 有文字层。 永远先用
fitz.get_text()探测——很多中文企业 PDF 是纸质扫描件。 pip install markitdown[pdf]不等于有 OCR。 markitdown 只提取嵌入文字;扫描件仍需 PyMuPDF + 视觉 OCR。pdfplumber对扫描件同样无能为力。 原因相同——没有文字层。- DPI 很重要。 低于 150 DPI 中文必乱码。默认 200。
- 上下文预算。 超过 10 页的文档渐进写入 Markdown;不要把整个 OCR 输出原样倾倒进对话。
- 核验而非轻信。 OCR 错误是静默的——关键字段走 Step 3;不确定性标
[OCR疑点]。 - v3.0 — OCR 续传覆盖范围。
paddle/tesseract引擎在 OCR 阶段续传(逐页ocr_page_NNN.md)。vision续传是会话级:agent 必须把每页结果写到ocr_page_NNN.md,最后--merge-ocr合并。agent 忘写逐页文件,vision 续传就形同虚设。 - v3.0 — 多栏是启发式。
reorder_columns用正文行 x 聚类检测双栏(两侧各 ≥4 行、≥80% 干净分离)。三栏以上、窄栏距、大量跨栏标题会退回全局 y 序。vision 引擎天然处理顺序,不走这段代码。表格节标题若 x 在左半,会被归入左栏(无害;内容不丢失)。 - v3.0.1 — paddle 输出顺序。 文字行与表格统一走
interleave_blocks(统一重排):栏内表格按 y 归入对应栏;跨栏表格作分段分隔符。修改此路径时,严禁把重排后的输出再按 y 排序——那正是本次修复的 v3.0.0 bug(blocks.sort 摧毁阅读顺序)。 - v3.0 — deskew 加固是保守策略。 三道门(文字页判定、方差提升 ≥1.3 倍、半页交叉验证)意味着轻微倾斜但文字稀疏的页可能被跳过。这是有意的:漏纠 1° 损失很小,照片被错误旋转 5° 就毁了。
- v2.0 — PaddleOCR 仅支持 Python 3.12。 默认
vision引擎 3.13 可用;paddle需独立 3.12 venv。别跟版本较劲——用适配层。 - v2.0 —
find_tables()不能替代table_restore。 PyMuPDF 的find_tables()读 PDF 矢量结构;扫描页没有结构,返回空。表格还原只有paddle引擎可用(PP-StructureV3 读图)。 - v2.0 — deskew 角度符号。 脚本报告检测到的倾角;施加到图片的旋转恰为其相反数(即转回水平)。不要照日志再"二次旋转"。
触发句示例
- "提炼这个PDF" / "summarize this PDF"
- "这个PDF是扫描件" / "this PDF is a scan"
- "PDF文字提取不出来" / "cannot extract text from PDF"
- "帮我OCR这个PDF" / "OCR this PDF"
- "批量提取这几个扫描PDF" / "batch OCR these scanned PDFs"
- "上次OCR没跑完,继续" / "continue the interrupted OCR"
- "这个扫描件是歪的" / "the scan is tilted"(触发纠偏)
- "PDF里的表格提出来" / "extract tables from PDF"(触发 table_restore + paddle)
- "双栏PDF" / "two-column document"(v3.0 多栏)