
引言
文档数字化这事,说起来简单做起来烦。PDF文字提取、扫描件OCR识别、表格结构化解析,每个环节都有成熟的方案,但要把它们串在一起,依赖的堆叠和环境的配置经常让人头大。
最近发现了一个pdf-image-text-extractor,技术路线跟常见的方案不太一样。它不做本地OCR,不依赖pdfplumber,核心运行依赖只有pymupdf和requests两个Python包。花时间读了一遍源码,从技术实现的角度聊聊它的架构和设计。
整体架构设计
这工具的设计遵循一个核心原则:能用AI视觉解决的问题,不在本地装OCR引擎。架构分三层:
-
鉴权层:record.py调用远程接口校验权限
-
处理层:根据输入类型(图片/PDF/目录)路由到对应的模块
-
输出层:统一输出Markdown或JSON,支持文件保存和终端打印
每层只管自己的事,要改也容易。
环境配置
依赖安装
项目的核心依赖只有两个包:
bash
pip install pymupdf requests
pymupdf负责PDF解析、表格提取和页面渲染三项核心工作。requests用于与鉴权服务器的HTTP通信。
API Key配置
使用前需要配置API Key,推荐通过环境变量注入:
bash
# 方式一:环境变量
export REDFOX_API_KEY="ak_xxxx..."
鉴权脚本
每次使用前需要执行鉴权:
bash
python3 scripts/record.py
鉴权成功后输出确认信息。如果未配置Key或接口返回异常(错误码3106/3107),脚本以非零退出码终止,需要重新获取Key。
核心功能实现分析
图片文字提取
图片文字提取不依赖任何本地脚本,直接由AI视觉模型识别。实现上需要注意以下几个要点:
- 识别准确率受图片清晰度、字体、背景等因素影响
- 对数字字符需要双重识别:全局识别和聚焦校验
- 易混淆字符(6/8/9/0/3)需基于视觉形状特征判断
- 无法完全确认的字符用?标注并给出备选
核心校验逻辑的伪代码如下:
第一轮:全局识别,提取所有文字
第二轮:聚焦数字区域,逐位描述形状特征
- 8: 上下两个封闭圆圈
- 6: 下方一个封闭圆圈,顶部向左弯弧
- 9: 上方一个封闭圆圈,底部向下竖线
- 0: 完整封闭椭圆,无开口
- 3: 右侧开口,两个弧形朝右
两轮一致 → 确认结果
两轮不一致 → 标注[待确认]
PDF文字提取
这是项目最核心的功能模块,涉及三个技术环节。
文字层提取
使用pymupdf的get_text方法获取页面文本对象。实现中通过blocks模式逐块解析:
python
def _extract_impl(pdf_path, extract_tables=True, render_scan=True,
scan_dir=None, scan_threshold=20, dpi=200):
doc = fitz.open(pdf_path)
md_parts = []
ocr_images = []
for page_num in range(page_count):
page = doc[page_num]
# 提取文字层,保留标题结构
blocks = page.get_text("dict")["blocks"]
for block in blocks:
if block.get("type") != 0:
continue # 跳过非文字块(如图片块)
for line in block["lines"]:
for span in line["spans"]:
# 字号大于16且字体包含bold视为标题
if span["size"] > 16 and "bold" in span["font"].lower():
# 输出为 ### 格式
else:
# 输出为正文段落
扫描页处理
检测到文字层稀少(默认阈值<20字符)的页面,自动渲染为PNG图片:
python
def _render_page_to_png(page, out_path, dpi=200):
mat = fitz.Matrix(dpi / 72.0, dpi / 72.0)
pix = page.get_pixmap(matrix=mat)
pix.save(out_path)
渲染后的图片路径汇总到ocr_images字段,由AI视觉模型逐张识别。这意味着整个流程不依赖任何本地OCR引擎。
表格结构化提取
调用pymupdf内置的find_tables方法提取表格:
python
def _extract_tables(doc):
tables_list = []
md_parts = []
for page_idx, page in enumerate(doc):
finder = page.find_tables()
for t_idx, table in enumerate(finder.tables):
data = table.extract()
# 将二维列表转换为Markdown表格
md = table_to_markdown(data)
表格的Markdown转换实现:
python
def _table_to_markdown(rows):
if not rows:
return ''
cleaned = [
[str(cell).replace('\n', ' ').strip() if cell is not None else ''
for cell in row]
for row in rows
]
cleaned = [r for r in cleaned if any(c for c in r)]
max_cols = max(len(r) for r in cleaned)
for r in cleaned:
while len(r) < max_cols:
r.append('')
header, body = cleaned[0], cleaned[1:]
lines = [
'| ' + ' | '.join(header) + ' |',
'| ' + ' | '.join(['---'] * max_cols) + ' |',
]
lines += ['| ' + ' | '.join(row) + ' |' for row in body]
return '\n'.join(lines)
批量处理
批量处理是实际使用中最频繁的场景。脚本会扫描指定目录下的所有PDF和图片文件,按文件名字母序逐个处理:
bash
python3 scripts/batch_extractor.py ./documents/ -o result.md
批量模式下图片不做本地OCR,统一汇入ocr_images清单,由AI逐张识别。输出支持三种格式:
-
合并Markdown报告(默认)
-
保存到文件(-o参数)
-
JSON结构化数据(--json参数)
进阶参数调优
扫描页渲染参数
bash
# 提高渲染分辨率(默认200,建议范围150-300)
python3 scripts/pdf_text_extractor.py ./scan.pdf --dpi 300
# 自定义扫描页输出目录
python3 scripts/pdf_text_extractor.py ./scan.pdf --scan-dir ./ocr_pages
# 调整扫描判定阈值(默认20字符)
python3 scripts/pdf_text_extractor.py ./scan.pdf --threshold 30
# 禁用扫描页渲染
python3 scripts/pdf_text_extractor.py ./scan.pdf --no-render
表格提取控制
bash
# 跳过表格提取,仅提取文字
python3 scripts/pdf_text_extractor.py ./document.pdf --no-tables
批量处理控制
bash
# 输出JSON结构化数据
python3 scripts/batch_extractor.py ./documents/ --json
# 自定义扫描页目录
python3 scripts/batch_extractor.py ./documents/ --no-render --scan-dir ./ocr_pages
踩坑经验总结
问题一:pymupdf版本兼容
pymupdf从1.24版本开始改变了导入方式,旧版使用import fitz,新版推荐import pymupdf。项目中做了兼容处理:
python
try:
import pymupdf as fitz
except ImportError:
try:
import fitz
except ImportError:
# 输出错误提示,退出
建议直接安装最新版pymupdf,少一些兼容问题。
问题二:扫描页渲染效果不佳
DPI设置直接影响AI识别的准确率。默认200DPI对大多数扫描件够用,但遇到小字号文字时可以提高到300DPI。DPI翻倍图片面积变成原来的4倍,处理时间和磁盘占用都会涨上去。
问题三:复杂表格识别失败
pymupdf的find_tables依赖表格边框线来定位表格边界。对于以下情况的识别效果不稳定:
-
无线框表格(靠缩进排版模拟的「伪表格」)
-
合并单元格跨越多行或多列
-
表格内嵌图片或复杂格式
对于这些情况,可以考虑通过ocr_images拿到渲染后的图片,人工查看或由AI视觉模型整体识别。
问题四:大文件处理性能
当PDF文件超过50MB时,拆分成小文件再处理比较稳妥。实测100MB以上的文件,pymupdf的打开和渲染时间会明显增加,极端情况下可能因内存不足触发异常。
问题五:stdout输出污染
pymupdf在解析某些PDF时,可能会往标准输出打印非JSON的提示信息。为了确保管道输出的纯净性,提取函数使用contextlib.redirect_stdout将输出重定向到stderr:
python
@contextlib.contextmanager
def extract_text_from_pdf(*args, **kwargs):
with contextlib.redirect_stdout(sys.stderr):
return _extract_impl(*args, **kwargs)
设计亮点
零依赖策略背后的权衡
项目里反复强调"零额外依赖",我一开始也以为是营销话术。看完代码觉得不是,这确实是经过权衡的技术决策。传统的PDF文字提取方案的依赖链长这样:
文字提取 → 需要pdfplumber
表格提取 → 需要另一个库
扫描识别 → 需要tesseract/rapidocr
模型管理 → 需要下载语言包
每个依赖都有自己的版本兼容问题、安装环境差异、维护更新成本。我在实际项目里搞这种环境配置,有时候光"搞定环境"就要花掉总时间的三分之一。
这个方案把所有依赖压缩到两个基础包,用AI视觉API替代本地OCR引擎。说白了就是把运维成本换成了API调用成本,对于非高频使用的场景来说,我觉得挺值的。
JSON契约式设计
脚本之间靠JSON文件通信,不做相互import。好处是每个脚本可以单独测,输出格式清楚,也方便接其他工具链。这套做法不算新鲜,但在这个小项目里用得很到位。
适用场景和边界
推荐使用场景
-
偶尔需要处理PDF/图片文字提取的办公场景
-
扫描版合同、报告的文字数字化
-
PDF中规则表格的结构化提取
-
需要批量处理文档目录的场景
-
不想装OCR引擎或没有OCR环境的情况
不推荐的场景
-
高频OCR需求(每分钟数十次以上),API调用成本可能高于本地OCR
-
特殊格式PDF(加密PDF、极度复杂的嵌套表格)
-
需要离线处理的环境
-
对识别速度有极致要求的实时场景
结语
PDF和图片文字提取提供了一种轻量化的思路:用AI视觉替代本地OCR,把5个依赖压缩成2个。图片文字识别、PDF文字提取、扫描页识别、表格结构化提取、批量处理,五个场景一条pip命令搞定。
下次你要把一摞扫描件转成文字的时候,可以先试试这个------说不定装完环境比你想象的要快。