图片、PDF 与表格数据提取
有一类数据不以网页形式存在:招标公告是 PDF 附件、统计年报是扫描图片、白皮书里的表格只有图片版本。这类场景的关键区别在于没有 HTML 结构可依赖,只能靠文本抽取与 OCR,而且必须接受「不可能 100% 准确」这个前提,用校验与抽样把风险控制在可接受范围。
三类场景与对应工具
| 数据形态 | 特征 | 首选工具 | 准确率预期 |
|---|---|---|---|
| 电子版 PDF | 文字可选可复制 | pdfplumber / PyPDF2 | 高,接近 100% |
| 扫描版 PDF | 整页是图片、文字不可选 | pdfplumber 转图 + OCR | 中,取决于清晰度 |
| 图片表格 | 表格以截图形式发布 | PaddleOCR 表格识别 | 中偏低,需人工抽检 |
| 图片中文字 | 单行文字、票据 | pytesseract + 中文语言包 | 中,需正则校验 |
判断顺序:能拿到 HTML 表格就不要 OCR,能拿到电子版 PDF 就不要扫描识别。每退一步,成本与错误率都会上升一个台阶。
PDF 文本与表格提取
# pdf_extract.py —— Python 3.10+,依赖 pdfplumber
import csv, json, pathlib
import pdfplumber
from PyPDF2 import PdfReader, PdfWriter
def extract_pdf(path: str) -> dict:
"""提取每页文本与表格;扫描件会得到空文本,据此决定是否走 OCR"""
pages, tables = [], []
with pdfplumber.open(path) as pdf:
for index, page in enumerate(pdf.pages, start=1):
text = page.extract_text() or ""
pages.append({"page": index, "text": text})
for table in page.extract_tables():
header = [(c or "").strip() for c in table[0]] if table else []
for row in table[1:]: # 首行当表头,空单元格统一成空字符串
tables.append(dict(zip(header, [(c or "").strip() for c in row])))
return {"pages": pages, "tables": tables,
"is_scanned": all(not p["text"].strip() for p in pages)}
if __name__ == "__main__":
data = extract_pdf("sample.pdf")
print(f"共 {len(data['pages'])} 页,表格 {len(data['tables'])} 行,扫描件={data['is_scanned']}")
with open("output/pdf/text.jsonl", "w", encoding="utf-8") as f:
for page in data["pages"]:
f.write(json.dumps(page, ensure_ascii=False) + "\n")
if data["tables"]:
with open("output/pdf/tables.csv", "w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(data["tables"][0].keys()))
writer.writeheader(); writer.writerows(data["tables"])
# 只需首页时可裁剪:writer.add_page(PdfReader("sample.pdf").pages[0])
extract_tables() 依赖 PDF 里的线条与对齐关系:有框线的表格效果好,仅靠空格对齐的表格经常串列,需要按行长度做一致性校验。
扫描件走 OCR
# ocr_demo.py —— 依赖 pytesseract、Pillow,另需系统安装 tesseract 与 chi_sim 语言包
import re
import pytesseract
from PIL import Image
# Windows 需显式指定路径;Linux 用包管理器安装后一般无需设置
# pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
TEXT = pytesseract.image_to_string(Image.open("scan.png"), lang="chi_sim+eng")
# 识别结果必须校验:金额、日期用正则兜一道,不符合规则的丢弃
AMOUNT = re.compile(r"[¥¥]?\s?(\d{1,3}(?:,\d{3})*(?:\.\d{2})?)")
DATE = re.compile(r"\d{4}\s?[-/年]\s?\d{1,2}\s?[-/月]\s?\d{1,2}\s?日?")
print("金额候选:", AMOUNT.findall(TEXT)[:5])
print("日期候选:", DATE.findall(TEXT)[:3])
| OCR 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
pytesseract | 安装简单、依赖 Tesseract 生态 | 中文排版适应性一般,需装 chi_sim | 少量图片、文字规整 |
PaddleOCR | 中文效果好,自带检测+识别+表格结构 | 依赖较重,首次需下载模型 | 中文扫描件、表格图片批量处理 |
| 云 OCR 服务 | 准确率最高、免运维 | 按量付费,涉及数据出境与隐私评估 | 有预算且敏感度可控 |
用云服务前务必评估:把扫描件上传给第三方属于数据处理行为,涉及《个人信息保护法》《数据安全法》与合同约定,需先确认合规性。
图片下载与去重
# image_store.py
import hashlib, pathlib
import requests
SAVE_DIR = pathlib.Path("output/images"); SAVE_DIR.mkdir(parents=True, exist_ok=True)
SEEN_HASH = set()
def download_image(url: str) -> str | None:
"""按内容哈希去重:内容相同即使 URL 不同也只存一份"""
try:
resp = requests.get(url, timeout=15, headers={"User-Agent": "MyResearchBot/1.0"})
if resp.status_code != 200 or len(resp.content) < 1024: # 过滤占位图与图标
return None
except requests.RequestException as exc:
print(f"下载失败 {url}:{exc}")
return None
digest = hashlib.sha256(resp.content).hexdigest()
if digest in SEEN_HASH:
return None
SEEN_HASH.add(digest)
path = SAVE_DIR / f"{digest[:16]}{pathlib.Path(url.split('?')[0]).suffix or '.jpg'}"
path.write_bytes(resp.content)
return str(path)
识别结果怎么校验
| 校验手段 | 做法 | 作用 |
|---|---|---|
| 正则约束 | 金额、日期、编号必须匹配预期格式 | 挡掉明显乱码 |
| 字段完整性 | 必填字段为空则整条丢弃 | 防止半截数据入库 |
| 交叉验证 | 表内合计 = 各行之和 | 检出串列与漏识别 |
| 置信度阈值 | PaddleOCR 返回置信度,低于阈值标记待复核 | 优先人工看高风险行 |
| 人工抽检 | 每批抽 5%~10% 逐条核对 | 估出真实错误率 |
实践建议:先跑 200 条样本算出字段级准确率;低于 95% 就别急着上量,先改进图像预处理(去噪、二值化、放大到 300dpi)或换引擎。
性能、成本与常见坑
| 维度 | 做法 | 备注 |
|---|---|---|
| 批量处理 | 按页/按文件分片,处理完就落盘 | 中断可续跑 |
| 并发 | PDF 文本提取可多进程;OCR 用 2~4 并发 | OCR 是 CPU 密集,多线程无用 |
| GPU 加速 | PaddleOCR 支持 GPU 推理 | 首次加载模型慢,适合长驻服务 |
| 缓存 | 相同文件哈希直接复用结果 | 重复提交很常见 |
| 现象 | 原因 | 处理 |
|---|---|---|
extract_text() 返回空 | 文件是扫描件,没有文本层 | 用 is_scanned 判断并转 OCR |
| 表格串列 | 表格靠空格对齐、无边框 | 先按 page.crop() 裁出表格区域再识别 |
| 中文识别成乱码 | 未安装 chi_sim 语言包 | 补装语言包并指定 lang="chi_sim+eng" |
| 数字 0 与 O、1 与 l 混淆 | OCR 固有误差 | 用正则约束字段格式并做上下文校正 |
| 图片重复占用空间 | 只按 URL 去重 | 改用内容哈希去重 |
合规提示:PDF 与图片往往包含版权内容与个人信息,是风险最高的数据形态。只处理自己有权获取与使用的文件,尊重来源方的版权声明与授权范围,遵守目标站 robots.txt 与服务条款,控制下载频率与并发;不采集个人隐私信息(身份证、银行卡、手机号、简历、证件照等),不绕过登录、验证码与付费墙。识别结果的使用与留存需符合《个人信息保护法》《数据安全法》《著作权法》,扫描件原文不应长期无授权留存,优先使用官方发布的结构化数据与开放数据集。
小结:先判断数据形态再选工具——能拿 HTML 表格就不要 OCR,能拿电子版 PDF 就不要做扫描识别;pdfplumber 负责文本与表格、pytesseract/PaddleOCR 负责扫描件,识别结果必须过正则、完整性与合计交叉校验,并用人工抽检估出真实准确率;图片用内容哈希去重;版权与隐私是这类数据的最大风险点。