Skip to content

真实 PDF、扫描页与网页抽取 ​

前一篇把坐标变成阅读顺序;本篇让坐标来自实际 PDF 文件。PDF 的内容流是绘制命令,既不保证段落顺序,也不保证可见文字具有文本层。表格也往往只是线条加文字,而不是行列对象。

下面自己生成可复现的多栏 PDF、带网格的价格表,再把页面光栅化成扫描图和只有图片的 PDF。文本 PDF 可以直接提取;扫描 PDF 的 get_text 为空,必须 OCR。坐标单位还发生变化:PDF 使用点,3倍渲染后的 OCR 使用像素,回写 PDF 来源框要除以3。

首次完整实验 ​

Python 3.12 新虚拟环境安装 pymupdf==1.26.7 pillow==12.1.0 beautifulsoup4==4.14.3 rapidocr-onnxruntime==1.4.4。将代码保存为 parse_docs.py,在空目录执行 python parse_docs.py;会生成4个夹具文件,OCR使用包内ONNX模型,无付费API。

python
from pathlib import Path
import pymupdf
from bs4 import BeautifulSoup
from rapidocr_onnxruntime import RapidOCR

# Generate actual PDF bytes. Content stream deliberately interleaves columns.
doc = pymupdf.open(); p = doc.new_page(width=500,height=400)
for point,text in [((40,40),'Left first'),((280,40),'Right first'),((40,70),'Left second'),((280,70),'Right second')]:
    p.insert_text(point,text,fontsize=14)
for x in (40,180,320):p.draw_line((x,120),(x,210))
for y in (120,150,180,210):p.draw_line((40,y),(320,y))
for point,text in [((50,140),'Item'),((190,140),'Price'),((50,170),'Book'),((190,170),'12.50'),((50,200),'Pen'),((190,200),'2.00')]:p.insert_text(point,text)
doc.save('fixture.pdf');doc.close()
with pymupdf.open('fixture.pdf') as doc:
    p = doc[0]
    blocks = p.get_text('blocks',clip=pymupdf.Rect(0,0,500,100))
    # Restrict by column before sorting vertically; global y/x sort interleaves columns.
    columns = ['\n'.join(line for line in p.get_text('text',clip=box,sort=True).splitlines() if line.strip()) for box in
               [pymupdf.Rect(0,0,250,100),pymupdf.Rect(250,0,500,100)]]
    assert columns == ['Left first\nLeft second','Right first\nRight second']
    table = p.find_tables().tables[0].extract()
    assert table == [['Item','Price'],['Book','12.50'],['Pen','2.00']]
    pix = p.get_pixmap(matrix=pymupdf.Matrix(3,3));pix.save('scan.png')
    # Image-only PDF does not inherit the source text layer.
    scan = pymupdf.open();sp = scan.new_page(width=500,height=400)
    sp.insert_image(sp.rect, filename='scan.png');scan.save('scan.pdf');scan.close()
with pymupdf.open('scan.pdf') as scan:assert scan[0].get_text().strip() == ''
ocr = RapidOCR()
result, elapsed = ocr('scan.png')
assert result
recognized = [{'box':box,'text':text,'confidence':float(score),'source':'scan.pdf','page':1} for box,text,score in result]
assert any('Book' in r['text'] for r in recognized)
print('columns',columns,'table',table,'ocr',recognized)

html = '''<html><body><nav>login</nav><main><h1>Refunds</h1><p>Within 7 days.</p><script>document.write('hidden')</script></main></body></html>'''
Path('fixture.html').write_text(html)
soup = BeautifulSoup(Path('fixture.html').read_text(),'html.parser')
for node in soup.select('script,style,nav'):node.decompose()
assert soup.main.get_text(' ',strip=True) == 'Refunds Within 7 days.'
# A client-rendered placeholder has no evidence; never synthesize a document from its script.
spa = BeautifulSoup('<main id="app"></main><script src="app.js"></script>','html.parser')
assert not spa.main.get_text(strip=True)
print('PDF, OCR and HTML parsed; empty SPA routed to browser rendering')

输出先包含两栏的独立文本,再得到 Item/Price 三行表格,OCR 输出每块的四点坐标、识别文字、置信度及来源。OCR 来自真实扫描图,不预填识别结果;模型首次加载可能稍慢。手工夹具字体清晰,因此一次成功不能证明低分辨率、倾斜或中文扫描件也可用。

全页按 y 再 x 排序会交叉阅读左右两栏。本例已知栏边界,可以先 clip 再排序;未知布局要先检测栏与标题/图注,再建立块顺序。表格检测利用网格线;无框表和合并单元格会破坏规则,需要记录跨度,空单元格不自动向前填充,否则价格可能被复制到另一商品。

低置信度 OCR 数字不能直接入账:保留原图框,检查金额格式与行列约束,失败时分流人工复核。文本层和 OCR 同时存在时不要双重写入;先判断文本层是否完整且与页面一致,再选择提取路径。最后HTML实验去掉脚本、导航,保留主内容;SPA 空容器被明确识别,下一步需要浏览器等待目标内容后读取 DOM,而不是把脚本源码当正文。动态采集还要固定 URL、抓取时间、最终 DOM 哈希和加载完成条件。

练习 ​

将网格表 Price 表头改为 Amount,解析断言必须随真实结果变化;把扫描图缩到一半分辨率后重新OCR,记录识别框/置信度差异,不预设低分辨率必然失败。对空 SPA 输出 requires_render=True。

完成后查看本章答案。

下一章:OCR、表格与结构校验。

用心记录每一步成长