運營和財務團隊每天要花大量時間,把發票、採購訂單、送貨單和供應商報價單手工錄入表格。藉助 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、系統版本、所用模型和報錯輸出,如有可能再附一份不含敏感資訊的示例單據,方便工程師快速定位。