运营和财务团队每天要花大量时间,把发票、采购订单、送货单和供应商报价单手工录入表格。借助 OCR 识别扫描件和照片,再由本地大模型严格按照你定义的字段返回结果,大部分录入工作都可以自动化。本文把 Tesseract、OCRmyPDF 与 Ollama 的结构化输出结合起来:每份单据都会生成一个包含提取字段的 JSON 文件,汇总表格会标记出明细金额加起来与合计对不上的单据,人工只需核对这些例外情况。整个流程都在你自己的服务器上运行。
# Debian / Ubuntu: PDF text, OCR engine with English and Simplified Chinese, OCR for PDFs
apt install -y python3-venv poppler-utils tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim ocrmypdf
python3 -m venv /opt/extract
/opt/extract/bin/pip install ollama
ollama pull qwen2.5:7b
Ollama 的部署方法请参阅使用 Ollama 部署本地大模型。如需识别其他语言,请安装对应的 Tesseract 语言包,例如 tesseract-ocr-jpn、tesseract-ocr-tha 或 tesseract-ocr-rus。
# Digital PDF (created by software): the text can be read directly
pdftotext -layout invoice.pdf - | head -40
# Scanned PDF: add a text layer with OCR first, then read it
ocrmypdf -l eng+chi_sim --skip-text scan.pdf scan-ocr.pdf
pdftotext -layout scan-ocr.pdf - | head -40
# Photo or screenshot
tesseract photo.jpg - -l eng+chi_sim | head -40
如果这里输出的文字清晰可读,模型就能从中提取字段;如果是一堆乱码,请先改善输入质量:以 300 dpi 扫描,拍照时把单据放平并保证光线充足。
SCHEMA 定义了你需要提取哪些字段。Ollama 会强制模型严格按照这个结构作答,因此输出始终是有效的 JSON。
cat > /opt/extract/extract.py <<'EOF2'
import csv, json, os, subprocess, sys, tempfile
import ollama
MODEL = os.environ.get("MODEL", "qwen2.5:7b")
LANG = os.environ.get("OCR_LANG", "eng+chi_sim")
SCHEMA = {
"type": "object",
"properties": {
"supplier": {"type": "string"},
"document_number": {"type": "string"},
"date": {"type": "string", "description": "YYYY-MM-DD"},
"currency": {"type": "string"},
"total": {"type": "number"},
"tax": {"type": "number"},
"items": {"type": "array", "items": {"type": "object", "properties": {
"description": {"type": "string"}, "quantity": {"type": "number"},
"unit_price": {"type": "number"}, "amount": {"type": "number"}},
"required": ["description", "quantity", "amount"]}},
},
"required": ["supplier", "document_number", "date", "currency", "total", "tax", "items"],
}
def text_of(path):
run = lambda cmd: subprocess.run(cmd, capture_output=True, text=True).stdout
if not path.lower().endswith(".pdf"):
return run(["tesseract", path, "-", "-l", LANG])
text = run(["pdftotext", "-layout", path, "-"])
if len(text.strip()) > 50:
return text
with tempfile.TemporaryDirectory() as tmp: # scanned PDF: OCR it first
ocr = os.path.join(tmp, "ocr.pdf")
subprocess.run(["ocrmypdf", "-q", "-l", LANG, "--skip-text", path, ocr], check=True)
return run(["pdftotext", "-layout", ocr, "-"])
rows = []
for path in sys.argv[1:]:
if not os.path.isfile(path):
continue
r = ollama.chat(model=MODEL, format=SCHEMA, options={"temperature": 0, "num_ctx": 8192}, messages=[
{"role": "system", "content": "Extract the requested fields from the document text. "
"Copy names, numbers and IDs exactly as written. Use an empty string or 0 "
"for anything that is not in the text. Do not guess."},
{"role": "user", "content": text_of(path)[:15000]}])
d = json.loads(r.message.content)
items = round(sum(i.get("amount", 0) for i in d["items"]), 2)
ok = d["total"] > 0 and min(abs(items - d["total"]), abs(items + d["tax"] - d["total"])) < 0.02
with open(os.path.splitext(path)[0] + ".json", "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=2)
rows.append([os.path.basename(path), d["supplier"], d["document_number"], d["date"],
d["currency"], d["total"], d["tax"], len(d["items"]), "OK" if ok else "CHECK"])
print(path, "OK" if ok else "CHECK", flush=True)
with open("summary.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["file", "supplier", "document_number", "date", "currency", "total", "tax", "lines", "check"])
w.writerows(rows)
EOF2
cd /data/invoices/2026-10
/opt/extract/bin/python /opt/extract/extract.py *.pdf *.jpg *.png
# For a large folder, run it in the background:
nohup /opt/extract/bin/python /opt/extract/extract.py *.pdf > extract.log 2>&1 &
每份单据旁边都会生成一个 .json 文件,包含全部字段和明细行;summary.csv 中每份单据对应一行。用 Excel 打开后筛选 check 列:OK 表示明细金额之和与合计金额一致(含税或不含税均可),CHECK 表示该单据需要人工检查。
buyer、delivery_date,并在明细行中添加 SKU 字段。tracking_number、consignee、weight_kg 和 packages 等字段。valid_until、payment_terms 和单价,在同一张表中对比各家供应商。像上面的 date 一样,为字段加上简短的 description,就能告诉模型你期望的格式。如果要处理多种单据,请为每种类型单独准备一个 schema,分别放在不同的脚本或文件夹中。
qwen2.5:14b 等更大的模型能更好地处理复杂版式和长表格,代价是速度变慢。在 CPU 服务器上,单页发票通常需要几十秒,较长的多页单据可能需要几分钟,具体取决于模型。大批量任务建议放在夜间运行。
许多 AI API 同样支持结构化输出,但单据会因此离开你的服务器。请先确认你的隐私保护义务、服务商的条款及其支持地区。对于发票、合同和客户数据,本地部署通常是更稳妥的选择。
大多数系统都支持导入 CSV 文件:可以映射 summary.csv 的各列,或把 JSON 文件转换成系统要求的导入格式。只自动导入标记为 OK 的行,CHECK 行留给人工审核。
如果按以上步骤操作后问题仍未解决,请提交工单联系 IMIDC 7×24 技术支持,并附上服务器 IP、系统版本、所用模型和报错输出,如有可能再附一份不含敏感信息的示例单据,方便工程师快速定位。