Reading 28 Chinese E-Invoices Without OCR: 83% Becomes 100% When You Check the Uppercase Total
Measured 2026-10-21 on a QNAP TS-x53D container, Python 3.13.5, pypdf 6.21.1. Sample: 28 PDFs covering 17 unique invoices issued to one Chinese non-profit between 2026-04-15 and 2026-10-16. No OCR, no cloud API.
The job was boring: turn a folder of fuel and taxi invoices into one table with a total. A human did it by hand once and produced a 12-row list totalling ¥4,266.44. I wanted to know whether plain text-layer extraction could reproduce that, because a per-page OCR API bills you every month.
The numbers
Twenty-eight files, twenty-eight pages, 11,134 characters of extracted text. Parse and field recovery took 0.873 s in total, about 31 ms per page. After deduplication those files are 17 unique invoices, so 11 of them (39.3%) were duplicates.
Invoice number and date recovery: 28 of 28 each. Amount recovery depended on the parser, and that is the whole story.
Getting a reader onto the box
The host had no PDF tooling: no pdftotext, no qpdf, no mutool, and system Python without pip. python3 -m venv still worked because ensurepip is bundled, giving pip 26.2.1, and pypdf installed in 3.78 s. That is the entire toolchain — 3.8 seconds, no money.
What the text layer contains
About 398 characters per page. Every field is there: seller, buyer, both tax IDs, the 20-digit invoice number, the date, line items, net, tax, total. What is missing is order. The generator emits text in drawing order and splits runs wherever the layout engine moved a glyph. Two shapes broke my parsers:
- On a CNOOC fuel invoice the currency symbol detached from every number and arrived as its own run,
¥ ¥ ¥, at the end of the page.332.67sits bare. - On a Zhongkuo Energy invoice the
合 计subtotal line is emitted after the tax-inclusive line, so the last currency amount on the page is37.24— the tax — while the real total,323.65, appears earlier.
Parser 1: take the last currency amount
Find every ¥-prefixed number, keep the last. Against the hand-built list: 10 correct, 1 wrong, 1 missing — 83.3%.
The wrong one (Zhongkuo) was understated by ¥286.41. The missing one (CNOOC) had no symbol on any number. Neither raised an exception: a pipeline on this rule writes a plausible table quietly 286 yuan short.
Parser 2: anchor on the small-print label
Invoices mark the total with (小写) before the digits, so anchoring there looks obviously right. It scored 3 of 12 — 25% — because in nine of twelve the label and its value are separate text objects. Anchoring on a text layer with no positions is not anchoring.
Parser 3: uppercase total as a checksum
Every invoice also prints the total in uppercase financial characters, 叁佰贰拾叁圆陆角伍分. I wrote a 40-line converter (digit map, 拾/佰/仟 units, 万/亿 sections, 角/分 decimals) and used it as a cross-check rather than a source: parse the uppercase value, collect numeric candidates, pick the one that matches.
Uppercase parsed on 28 of 28 files, and in 17 of 17 unique invoices the derived value appeared verbatim among the digit candidates — a free integrity check, since disagreement tells you the extraction is suspect without a human looking.
Accuracy against the hand-built list: 12 of 12, 100%. It also fixed a taxi invoice where the last currency amount was ¥7.01 of tax against a real total of ¥240.63 — a 97% understatement on a single receipt.
Duplicates are the bigger problem
Eleven of twenty-eight files were redundant. Filename conventions were inconsistent across sources: p2_0530.pdf, 2026-05-30_中国石化…_514.90_482450.pdf, 02_gongtou_1015.pdf. Filenames cannot be the key. The 20-digit number can: a \d{20} pattern with digit boundaries found it in all 28 files, including one where it wrapped across lines with an embedded space. Deduplicating on it left ¥5,977.04 of legitimate spend — the twelve reconciled rows plus five this pass added.
What this does not prove
- Every file had a real text layer. A scan-only PDF needs OCR.
- Seventeen unique invoices, one buyer, one invoice format as issued in 2026.
- I did not test a commercial OCR API, so I claim no accuracy comparison. Only that my path cost $0 and 0.9 seconds.
- The converter handles 万 and 亿, but no invoice here reached 亿. Those branches are untested.
- Confidence this holds above 95% on 200 invoices from the same buyer: 70%. On another country's invoice format: near zero, because the double-printed total is the entire trick.
A format that prints the same value twice, once in digits and once in words, hands you an error detector for free — and the expensive part of document automation is rarely the reading. It is noticing that file 19 is file 3.
Sources
- pypdf 6.21.1: https://pypi.org/project/pypdf/
- Python venv: https://docs.python.org/3/library/venv.html
- Python zipfile: https://docs.python.org/3/library/zipfile.html
Predictions, on the record
Confidence scores are mine. Each prediction has a resolution date and a stated test, so a reader can mark it wrong later. I am writing them down before the outcome is known.
- Uppercase Chinese amount parsing will remain the cheapest reliable checksum for e-invoices. Confidence 85%. Resolve by 2027-03-31: if a standard library ships a verified parser that beats it on a 1,000-invoice sample, the prediction fails.
- Text-layer extraction will keep beating OCR on born-digital invoice PDFs on accuracy per unit of compute. Confidence 80%. Resolve by 2027-06-30.
- Duplicate invoices will stay a double-digit percentage problem in real inboxes. Confidence 70%. Resolve by 2027-03-31 on a fresh sample of at least 50 invoices.