Na začátku jara 2025 jsem dostal na první pohled jednoduché zadání. Klient nahraje výpis z bankovního účtu v PDF a my z něj potřebujeme dostat údaje o účtu, počáteční a konečný zůstatek a seznam transakcí. Žádná věda, řekl jsem si. Sice jsem o PDF věděl, že to nebude taková sranda, jak to na první pohled vypadalo. Co je na tom, PDF otevřu, text přečtu, pár regulárních výrazů a hotovo. Dnes, o rok a půl později, podporujeme přes třicet různých podob výpisů českých a slovenských bank a já vím, že „pár regulárních výrazů“ byl jeden z nejoptimističtějších odhadů mé kariéry.
Tímto článkem začínám krátký seriál o tom, jak taková aplikace vznikala. Nemohu popisovat žádné interní detaily, ale spíš přístupy, slepé uličky a poučení, která by se mohla hodit i vám, pokud budete někdy z PDF něco „jenom vytahovat“. Aplikace dnes běží v rámci GoodVeri, kde pomáhá s ověřováním příjmů a finanční situace klientů.
PDF je obrázek, který umí mluvit
Jak už jsem naznačil v úvodu, o PDF jsem věděl, že to není jasně strukturovaný dokument, jako třeba XML nebo JSON. Takže vytěžit jej nebude taková hračka, ale co jsem musel nakonec nastudovat a jak k tomu přistoupit? PDF je ve skutečnosti popis stránky pro tiskárnu. Obsahuje instrukce typu „na souřadnice 56,4 × 712,8 napiš tímto písmem text Počáteční zůstatek“. Nic víc. Žádné řádky tabulky, žádné buňky, žádné „tohle je částka k té transakci“. Často není zaručené ani pořadí, a jedno slovo může být klidně složené z několika kousků, protože generátor PDF se tak zrovna rozhodl.
Když z takového dokumentu vytáhnete „text“ nějakou běžnou knihovnou, dostanete sice všechna písmena, ale v pořadí, které odpovídá spíš náladě generátoru než tomu, co vidí člověk. Datum transakce se ocitne o tři řádky níž než její částka, dvousloupcová hlavička se promíchá do jednoho řádku a poznámka k platbě se přilepí k názvu protistrany. A to ještě nezmiňuji to množství knihoven, které se ke generování PDF používají.
A tak jsem velmi rychle opustil představu, že budu parsovat text. Místo toho parsuji stránku.
Proč ne OCR?
Tuhle otázku jsem slyšel snad nejčastěji. Proč nepoužít OCR nebo nějaký model, který se na výpis „podívá“ a řekne, co tam je? Odpověď je jednoduchá. Výpisy, které zpracováváme, generuje přímo banka. Text v nich je, jen je potřeba ho správně poskládat. OCR by znamenalo převést dokonale přesná data na obrázek a pak je z obrázku zase odhadovat. Tím bych si dobrovolně přidal chyby, které tam původně nebyly.
U finančních dat navíc nechci „skoro správně“. Když se jedna částka přečte špatně, nesedí zůstatek a celý výpis je k ničemu. Deterministický přístup, kdy stejný soubor vždy dá stejný výsledek, se mi osvědčil mnohem víc. A má ještě jednu výhodu, ke které se dostanu v dalších dílech — dá se poctivě testovat.
Souřadnice, řádky a bloky
Základem celé aplikace je tedy převod PDF na vlastní model stránky. K získání dat z PDF souboru používám knihovnu, která umí z PDF vytáhnout jednotlivá písmena i s jejich polohou, velikostí a písmem. Nad tím pak skládám slova do řádků a řádky do bloků. Pro rozdělení stránky na bloky existuje známý algoritmus Docstrum, který jsem si nakonec musel trochu upravit, protože bankovní výpisy mají svá specifika — hodně malého písma, hodně čísel zarovnaných doprava a tabulky, které se tváří jako odstavce.
Výsledkem je hierarchie dokument → stránka → blok → řádek textu, kde každý řádek ví, kde přesně na stránce leží. A s tím se už dá pracovat. Když vím, že na výpisech jedné banky je číslo účtu vždy vpravo nahoře zhruba na určité pozici, stačí se zeptat „jaký text leží tady, s tolerancí pár bodů?“.
Zjednodušeně to vypadá asi takto:
var iban = page.LineAt(left: 390, top: 742, tolerance: 4)?.Text;
Tolerance je důležitá. Stejná banka vygeneruje dva výpisy a text je jednou o bod vlevo, podruhé o bod vpravo. Delší jméno majitele posune celý blok o řádek níž. Víceřádková adresa posune všechno pod ní. Naučil jsem se, že pevné souřadnice fungují přesně do chvíle, kdy přijde výpis od klienta, který má dlouhé příjmení.
Zarovnání doleva, zarovnání doprava
Jedna z prvních věcí, která mě překvapila, bylo zarovnání. Text zarovnaný doleva poznáte podle levého okraje. Jenže částky jsou skoro vždy zarovnané doprava — a 1 250,00 začíná úplně jinde než -125 000,00. Pokud hledáte částku podle levého okraje, jednou ji najdete a podruhé ne.
A tak má každá poloha v aplikaci i informaci, podle kterého okraje se má měřit. Zní to banálně, ale tahle drobnost vyřešila v začátcích obrovské množství chyb, které jsem do té doby „opravoval“ čím dál širší tolerancí. A široká tolerance je zrádná — dřív nebo později do ní spadne i sousední sloupec.
Transakce nejsou řádky
Hlavička výpisu je relativně snadná. Opravdová zábava začíná u transakcí. Jedna transakce může mít jeden řádek, ale klidně i pět — datum, název protistrany, číslo účtu, zpráva pro příjemce, variabilní symbol. Někde je datum na prvním řádku, jinde je na prvním řádku částka. Některé banky oddělují transakce čarou, jiné šedým pruhem, jiné vůbec ničím. A pak přijde konec stránky a transakce pokračuje na další, kde je samozřejmě znovu hlavička tabulky - nebo není.
Postupně jsem tedy došel k tomu, že potřebuji odpovědět na několik otázek:
- kde začíná tabulka transakcí a kde končí,
- podle čeho poznám, že na řádku začíná nová transakce,
- do kterého sloupce patří který kousek textu,
- co je na stránce jen „šum“ — patička, číslo stránky, reklama na hypotéku.
Každá banka na ně odpovídá jinak. A každá banka má často několik verzí výpisu. Ze začátku jsem to všechno psal jako kód, extraktor po extraktoru. Fungovalo to, ale po desáté bance jsem viděl, že píšu pořád totéž, jen s jinými čísly. O tom, jak jsem se z toho dostal, bude druhý díl.
Co mi první měsíce daly
Kdybych měl shrnout poučení z prvních měsíců, bylo by to asi tohle:
- Nečtěte text, čtěte stránku. Poloha je u PDF stejně důležitá informace jako obsah.
- Každý layout je jiný svět. Nesnažte se jedním kódem obsloužit všechny banky, ale ani jednu banku s pěti různými výpisy.
- Tolerance není řešení. Když potřebujete čím dál větší toleranci, nejspíš měříte od špatného konce, nebo se díváte na špatnou věc.
- Ověřujte výsledek. Počáteční zůstatek plus všechny transakce musí dát konečný zůstatek. Je to nejlevnější a nejúčinnější test, jaký znám.
Ten poslední bod se ukázal jako zásadní i z jiného pohledu. Když výpis nesedí, buď jsem ho špatně přečetl, nebo s ním někdo něco dělal. Ale o tom až ve čtvrtém díle.
Máte vlastní zkušenost s vytahováním dat z PDF? Šli jste cestou souřadnic, nebo jste to vzdali a sáhli po OCR? Budu rád, když se podělíte.