Every month-end, the same ritual: a stack of reimbursement invoices on the finance desk — VAT special invoices, general invoices, printed e-invoices, some stamped in red. Someone opens each file, types every field into Excel by hand, and by midnight the total column is still thirty rows short. That work can be handed to a script: point it at a folder, let OCR batch-process every invoice, and end up with one summary spreadsheet. This article walks through two fully reproducible routes, from installing dependencies to exporting an .xlsx file.
One boundary note first: our earlier DeepSeek-OCR deployment tutorial covers getting an OCR model running — it is tool-oriented. This piece covers turning a stack of invoices into one Excel sheet — it is task-oriented, and we will not repeat the deployment details.
Choose Your Route
There are two mainstream approaches: a cloud API route and a local open-source route. One table to see the difference:
| Dimension | Route A: Cloud API | Route B: Local PaddleOCR |
|---|---|---|
| Structured fields | API returns named fields directly (invoice number, tax, etc.) | The open-source release has no invoice-specific model; you write the field-mapping rules yourself |
| Data leaves machine | Images are uploaded to the vendor | Nothing leaves your machine |
| Cost | Baidu AI Cloud offers a monthly free quota | Free locally, runs on CPU |
| Effort | Install dependencies and call | Install the stack and write regex rules |
| Best for | Dozens to a thousand invoices a month, minimal fuss | Sensitive documents, large volume, willing to maintain a script |
Short version: pick A for convenience, B for sensitive data or high volume. A sensible compromise is to prototype with A's free quota, then migrate to B.
Before touching code, organize the files: create an "invoices" folder, save the original PDF e-invoices there, photograph paper invoices flat and away from direct light, and keep invoice dates in the filenames so you can trace missed detections later. No strict layout is required; the scripts below assume that folder plus one output spreadsheet.
Route A: Cloud API in Six Steps
We use Baidu AI Cloud's VAT invoice recognition as the example. The official documentation defines the endpoint as POST https://aip.baidubce.com/rest/2.0/ocr/v1/vat_invoice, accepting exactly one of image, url, pdf_file, or ofd_file, in that priority order. A PDF invoice can be passed directly as pdf_file — no image conversion required. The base64-encoded and urlencoded payload must stay under 8M, image edges between 15px and 4096px; for multi-page PDFs, set pdf_file_num to pick the page, defaulting to page 1.
Step 1: install dependencies. One command covers all four libraries:
pip install pymupdf pandas openpyxl requestsOne licensing note: PyMuPDF ships under a dual AGPL/commercial license — free for personal projects and AGPL-compatible open source, but closed-source commercial use requires a paid license from the vendor. If this runs inside a company tool, check the licensing boundary first; if that is a concern, switch to pdf2image (which leans on GPL components, so verify that too) or skip local rendering entirely by sending PDFs straight to the cloud API's pdf_file parameter.
Step 2: activate the service and manage keys. Create an OCR application in the Baidu AI Cloud console to get an API Key and Secret Key. Do not hardcode them; put them in environment variables. The official SDK guide says exactly that — never write your AccessKey into project code.
Step 3: exchange the keys for an access_token. Before calling the recognition endpoint, trade the two keys for a token and attach it as a URL parameter:
import os, requests
def get_token():
# Keys live in environment variables, never in source code
resp = requests.get(
"https://aip.baidubce.com/oauth/2.0/token",
params={
"grant_type": "client_credentials",
"client_id": os.environ["BAIDU_OCR_API_KEY"],
"client_secret": os.environ["BAIDU_OCR_SECRET_KEY"],
},
)
return resp.json()["access_token"]Step 4: confirm invoice types and parameters. With the default type=normal, the endpoint covers VAT general invoices, special invoices, e-invoices (including fully digitalized ones), and blockchain invoices; pass type=roll for roll-type VAT invoices; seal_tag=true adds stamp-detection output. Mind the boundary: fixed-amount invoices, general machine-printed invoices, and medical invoices are outside this endpoint's scope — sending them fails or returns missing fields. For those, switch to Baidu's mixed-invoice endpoint multiple_invoice, which splits 13 common receipt types in one call.
Step 5: collect files and run the batch script. Use the standard library pathlib or glob to walk the folder for PDF, JPG, and PNG files — no third-party dependency needed. Render PDFs to 300dpi PNG with PyMuPDF: PDFs default to 72dpi, 150dpi is fine for web previews but too coarse for OCR on small text, 300dpi is the recommended input, and PNG beats JPG because compression artifacts smear thin strokes. Call the API per file, pack the words_result fields into a dict, and record failures without breaking the batch:
import os, time, base64, glob
import fitz # PyMuPDF
import pandas as pd
import requests
VAT_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/vat_invoice"
def pdf_to_png(pdf_path, dpi=300):
# Render PDF pages at 300dpi for reliable small-text recognition
doc = fitz.open(pdf_path)
pix = doc[0].get_pixmap(matrix=fitz.Matrix(dpi / 72, dpi / 72))
return pix.tobytes("png")
def recognize(token, img_bytes):
resp = requests.post(
VAT_URL,
params={"access_token": token},
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"image": base64.b64encode(img_bytes).decode()},
)
return resp.json()
def main():
token = get_token()
rows = []
files = glob.glob("invoices/*.pdf") + glob.glob("invoices/*.jpg") + glob.glob("invoices/*.png")
for path in files:
row = {"file": os.path.basename(path), "status": "ok"}
try:
if path.lower().endswith(".pdf"):
img = pdf_to_png(path)
else:
img = open(path, "rb").read()
w = recognize(token, img).get("words_result", {})
row.update({
"invoice_number": w.get("InvoiceNum", ""),
"invoice_date": w.get("InvoiceDate", ""),
"seller_tax_id": w.get("SellerRegisterNum", ""),
"amount": w.get("TotalAmount", ""),
"tax": w.get("TotalTax", ""),
"total_with_tax": w.get("AmountInFiguers", ""),
})
except Exception as e:
row["status"] = f"failed: {e}"
rows.append(row)
time.sleep(0.6) # free tier is capped at 2 QPS; serialize requests
pd.DataFrame(rows).to_excel("invoice_summary.xlsx", index=False)
if __name__ == "__main__":
main()Note that converting to PNG first keeps the input uniform and leaves local copies for auditing; if you want the fewest steps, the endpoint accepts pdf_file directly and the conversion can be skipped.
Step 6: know your quota and rate limits. Per Baidu AI Cloud's official free-tier page (page updated 2026-10-22; verified 2026-10-30 for this article): VAT invoice recognition gets 1,000 calls/month for individually verified accounts and 2,000 calls/month for enterprise-verified accounts, granted monthly with no carry-over. The same page states explicitly that both successful and failed calls consume the free quota — a blurry test image still costs a call, so do not burn quota on junk inputs. For QPS, follow the official QA figure: 2 QPS before enabling payment, 10 QPS after; community claims of 5 or 3 QPS contradict the official wording and are not adopted here. At 2 QPS your script must serialize with time.sleep or back off exponentially on throttling errors — that is exactly what sleep(0.6) does above.
Other vendors as reference points: Tencent Cloud's mixed-invoice endpoint handles 14 standard reimbursement invoice types in one image at a default 5 requests/second; Alibaba Cloud's RecognizeInvoice covers VAT special, general, and e-invoices with the widest format support (including OFD and TIFF) and self-reports 98% overall accuracy — a vendor claim, not an independent benchmark. Tencent Cloud's free quota is described inconsistently across sources, so check the actual resource-package list in your console before committing.
Route B: Local PaddleOCR
Why bother: PaddleOCR is among the most-starred open-source OCR projects on GitHub (91,929 stars as measured on 2026-10-30), under the Apache-2.0 license. Its latest release, v3.7.0, shipped the PP-OCRv6 model family in tiny, small, and medium tiers, all runnable on CPU — the release notes cite a 5.2x CPU inference speedup via OpenVINO, so CPU-only batch processing is entirely viable, just slower than GPU, and with no VRAM pressure at all since no graphics card is needed.
pip install paddlepaddle paddleocrEnvironment caveat: the repository's commit history mentions a bug in PaddlePaddle 3.3.0's native CPU inference affecting paddleocr models, and the docs once suggested rolling the version back. Pinning the PaddlePaddle version during setup is common practice; when inference misbehaves, check version compatibility before suspecting your code.
The key insight, and the trap this article most wants you to avoid: the open-source PaddleOCR ships no VAT-invoice-specific model. The correct usage is to run PP-OCRv6 as a general-purpose text detector and recognizer, get back a list of text blocks, then map fields with your own regex rules. That gap in engineering effort — versus a cloud API that returns named fields like InvoiceNum — is the core argument for choosing between the two routes. When you need table or layout structure, the PaddleOCR-VL models introduced in v3.6.0 work with the PP-StructureV3 pipeline, which supports table recognition and stamp detection toggles.
import re, glob
import pandas as pd
from paddleocr import PaddleOCR
ocr = PaddleOCR(ocr_version="PP-OCRv6", device="cpu") # CPU is enough
def extract_fields(texts):
# Rule-based field extraction is the real workload of the local route
row = {}
for t in texts:
if re.fullmatch(r"\d{8}", t): # 8-digit invoice number
row["invoice_number"] = t
m = re.search(r"(\d{4}年\d{1,2}月\d{1,2}日)", t)
if m:
row["invoice_date"] = m.group(1)
m = re.search(r"(?:价税合计|合\s*计).*?([\d,]+\.\d{2})", t)
if m:
row.setdefault("total_with_tax", m.group(1))
return row
rows = []
for path in glob.glob("invoices/*.png") + glob.glob("invoices/*.jpg"):
result = ocr.predict(path)
texts = result[0]["rec_texts"] # generic text blocks
rows.append({"file": path, **extract_fields(texts)})
pd.DataFrame(rows).to_excel("invoice_summary_local.xlsx", index=False)The sample regex covers only the three most common fields. Real invoices are messier: fully digitalized e-invoice layouts, roll invoices, and creased scans all cause missed matches, so keep a manual-review status column in the local route's output.
Field Mapping and Excel Export
The cloud API returns English field names, so map them before the table lands in finance's hands. The key fields from Baidu AI Cloud's official documentation:
| Returned field | Meaning | Suggested column |
|---|---|---|
| InvoiceNum | Invoice number | invoice_number |
| InvoiceDate | Issue date | invoice_date |
| SellerRegisterNum | Seller's taxpayer ID | seller_tax_id |
| TotalAmount | Amount before tax | amount |
| TotalTax | Total tax | tax |
| AmountInFiguers | Total incl. tax (numeric) | total_with_tax |
| CommodityTaxRate | Tax rate (inside the item-details array) | tax_rate |
Fields in the item-details array, such as CommodityName and CommodityPrice, can be expanded into a child table as needed. For export, think pandas for the data and openpyxl for the styling: pandas' to_excel writes .xlsx through the openpyxl engine underneath, and the current pandas stable release, 3.0.6 (published 2026-09-18), requires Python 3.11+. A little office polish:
from openpyxl import load_workbook
from openpyxl.styles import Font
wb = load_workbook("invoice_summary.xlsx")
ws = wb.active
for cell in ws[1]:
cell.font = Font(bold=True) # bold header row
ws.freeze_panes = "A2" # freeze header
for col in "FGH": # thousands separator on amount columns
for cell in ws[col][1:]:
cell.number_format = "#,##0.00"
wb.save("invoice_summary.xlsx")Rule of thumb: think in tables, use pandas; think in cells and styles, use openpyxl. Note that openpyxl does not evaluate formulas — if the sheet contains them, open it in Excel or LibreOffice, let it recalculate and save, before reading values with load_workbook(data_only=True).
LLM Route: A Supplement, Not the Main Line
GLM-OCR (open-sourced by Zhipu, 0.9B parameters) supports text, formula, table, and custom modes; the custom mode takes a JSON-schema prompt for structured extraction, and community developers have published receipt extractions pulling invoice numbers, dates, line items, and totals. State the caveat in full, though: this rests on the vendor's own claims plus third-party general-purpose tests, with no invoice-specific public benchmark; the testers themselves concluded that structured extraction only stabilizes once the prompt constrains it explicitly. At 0.9B parameters its VRAM footprint is tiny, and it is worth trying when volume is low and one prompt can replace a pile of regex. For a production pipeline, the two routes above are more dependable. The full resource list lives in our GLM-OCR article (see related reading below).
Common Failures and Fixes
Separate "recognition failed" from "recognition wrong": the former means an API error or empty result, usually image quality; the latter means results with misaligned fields, usually layout and semantics. Work through four checks in order: image quality, layout and language, engine parameters, business fallback.
High-frequency capture-side causes: glare on laminated invoices under direct light, tilted shots distorting fields, creases and stains fading text, the red invoice stamp covering the tax ID or amount, and screenshot filters or over-compression destroying fine detail. Fixes: diffuse light and shoot flat, flatten creases, adjust the angle when a stamp blocks a field, and prefer the original PDF e-invoice over re-photographed screenshots. For a sense of scale: one Baidu AI Cloud technical article's experiments put a 23% accuracy drop at 15 degrees of tilt, with case studies showing stamp coverage corrupting 35% of amount fields — those are that article's own experimental figures, not industry-wide numbers.
On the API side, vendor error-code tables are your checklist (Tencent Cloud's documentation as an example; the logic is similar everywhere): ImageBlur means reshoot or preprocess; ImageDecodeFailed means verify the file really is PNG/JPG/PDF and uncorrupted; oversized files must be compressed or split. Invoice-type mismatches — fixed-amount or machine-printed invoices forced through the VAT endpoint — go to the mixed-invoice endpoints instead.
The LLM route has three signature failure modes worth knowing (per Tencent Cloud's official article): field shuffle (buyer and seller names swapping tax IDs), amount shuffle (net amount, tax, and gross total read out of order), and line-item truncation (multi-row details reduced to the first few). Professional OCR engines return field coordinates, which is why shuffle does not happen there.
Compliance Boundaries
Invoices carry business-sensitive data — tax IDs, buyer and seller names — so before uploading to a cloud API, review the provider's data agreement (Baidu AI Cloud's terms state that your business data belongs to you); for authenticity verification, the only official online channel is the National VAT Invoice Verification Platform — five checks per invoice per day, invoices issued within the last five years, same-day invoices verifiable the same day; and AI-assisted review supports but never replaces professional judgment.
Related reading
- DeepSeek-OCR Local Deployment SOP: that piece covers getting an OCR model running; this one covers turning a stack of invoices into one Excel sheet.
- Open-Source OCR Models Comparison: six open-source engines compared side by side — read it before picking an engine.
- GLM-OCR Open-Source Resource: the complete resource list for the small-model structured-extraction route.
FAQ
Q1: Why does invoice OCR recognition fail?
Most often image quality: glare, blur, tilt, a stamp covering key fields, or over-compression. Second is invoice-type mismatch — fixed-amount and general machine-printed invoices are outside the VAT invoice endpoint's scope, so switch to a mixed-invoice endpoint. Cloud APIs in the free tier also enforce QPS caps, and excessive concurrency gets throttled.
Q2: How do I batch-recognize invoices into Excel?
A three-stage pipeline: collect PDF/JPG/PNG files with pathlib or glob; render PDFs to 300dpi images with PyMuPDF; call the OCR API or local model per file, pack fields into dicts, and export once with pandas' to_excel. The main script in Route A is a complete, runnable example.
Q3: Can PDF e-invoices be recognized directly, without converting to images first?
Baidu AI Cloud's endpoint accepts pdf_file directly, so no conversion is required. Still, rendering every PDF to a 300dpi PNG keeps inputs uniform and leaves local copies for auditing. The same goes for scans: prefer the original PDF over re-captured screenshots.
Q4: Can open-source PaddleOCR extract invoice fields out of the box?
No. The open-source release has no VAT-invoice-specific model; it returns generic text blocks, and you write the regex rules mapping invoice numbers, dates, and amounts yourself. If you want named fields out of the box, the cloud API route takes far less engineering.
Q5: How do I verify the authenticity of recognized invoices?
OCR only reads the data; verification is a separate step. The only official online channel is the National VAT Invoice Verification Platform — five checks per invoice per day, covering invoices issued within the last five years. Cloud vendors offer invoice-verification APIs for batch spot checks, but results are still anchored to the tax authority's platform.