图片、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 负责扫描件,识别结果必须过正则、完整性与合计交叉校验,并用人工抽检估出真实准确率;图片用内容哈希去重;版权与隐私是这类数据的最大风险点。

笔记加载中…