跳到正文
DEV Community· Damin Mutti·· 1 小時前AI 評分46

銀行月結單 OCR 數字辨識正確、版面判讀出錯:如何識別問題

Bank statement OCR gets the digits right and the layout wrong. Here is how to catch it.

AI 導讀

銀行月結單 OCR 的錯誤往往不在數字辨識,而在行項分類、負數符號、日期年份及帳戶歸屬;核對月結單列印的結餘可找出多數問題。結餘核對應逐個帳戶進行,並在銀行列出每日結餘時核對流水結餘;日期年份則應按月結單期間判定。

正文

The mistakes that survive bank statement OCR are rarely misread digits anymore. They are layout mistakes: a balance line read as a transaction, a subtotal counted twice, a trailing minus dropped, a December row put in the wrong year. Most of them can be caught with the statement's own printed balances, which is how ZeraBooks checks each statement it converts, and the rest need one rule about dates and one about accounts. (I founded ZeraBooks, so weigh that, but the code below works without it.)

Plenty of tools advertise "99% accuracy" and still hand back a month that doesn't tie. When you dig into why, the characters are usually fine. The model just didn't understand what kind of line it was looking at.

What does a layout error look like on a real statement?

Take a PNC-style business checking month that starts overdrawn. The bank prints negative balances with the minus after the number:

Beginning balance            612.40-
06/02  ACH Credit Brookline Freight      2,480.00
06/03  Overdraft Item Fee                   36.00
06/05  3214 Debit Card Purchase Pilot      418.22
06/10  Corporate ACH Truck Lease         1,150.00
06/16  ACH Credit Brookline Freight      2,915.50
06/19  3214 Debit Card Purchase Love's     397.06
Ending balance             2,781.82

Every digit there is easy to read. Now look at what a generic extractor tends to hand back:

  • Opening balance 612.40 with the trailing minus gone. The whole month is now off by 1,224.80, twice the overdraft.
  • 3214 (the card's last four digits) glued onto the amount on the debit card lines.
  • The two fee rows in the "Overdraft and Returned Item Fee Summary" table, including a year-to-date total, imported as transactions.
  • The Daily Balance table read as a list of deposits, because it's a date next to a dollar amount.

None of that is an OCR failure in the narrow sense. The text was read perfectly.

Which errors does the printed balance catch?

Most of them, if you check at the right level. Here's how the gap maps to a cause:

Gap between your rows and the printed balance What usually happened
Exactly twice one row's amount That row's sign is flipped (trailing minus or CR lost)
Exactly twice the opening balance Opening balance sign lost, common on overdrawn accounts
Exactly one printed balance or subtotal A balance line or section subtotal was read as a transaction
Equal to one real transaction A row was dropped, usually at a page break
A multiple of 9 Two digits swapped

That's a lookup, so it can be code.

The check, in about 20 lines

Use Decimal. Pass in the rows, the printed opening and closing balance, and any printed balances and subtotals you have.

from decimal import Decimal as D

def diagnose(rows, opening, closing, printed=()):
    """rows: list of (date, description, amount) with money out negative."""
    total = sum((D(a) for _, _, a in rows), D("0"))
    gap = (D(opening) + total) - D(closing)
    if gap == 0:
        return "ties"
    hints = []
    for date, desc, amt in rows:
        if abs(gap) == abs(D(amt)) * 2:
            hints.append(f"sign flipped on {date} {desc} ({amt})")
        if abs(gap) == abs(D(amt)):
            hints.append(f"{date} {desc} ({amt}) may be a duplicate or a total read as a row")
    if abs(gap) == abs(D(opening)) * 2:
        hints.append("opening balance sign is wrong (look for a trailing minus)")
    for p in printed:
        if abs(gap) == abs(D(p)):
            hints.append(f"a printed balance or subtotal of {p} was read as a transaction")
    if gap % 9 == 0 and abs(gap) < 1000:
        hints.append("two digits swapped somewhere")
    return f"off by {gap}: " + ("; ".join(hints) or "compare rows since the last good balance")

rows = [("06/02", "ACH Credit", "2480.00"), ("06/03", "Overdraft Item Fee", "-36.00"),
        ("06/05", "Debit Card Pilot", "-418.22"), ("06/10", "ACH Truck Lease", "-1150.00"),
        ("06/16", "ACH Credit", "2915.50"), ("06/19", "Debit Card Love's", "-397.06")]

print(diagnose(rows, "612.40", "2781.82"))   # trailing minus lost on the opening
print(diagnose(rows, "-612.40", "2781.82"))  # ties

The first call prints off by 1224.80: opening balance sign is wrong (look for a trailing minus). The second prints ties.

Which errors does a balance check miss?

This is the part people skip, and it matters more than the code.

A closing balance check can't see an error that doesn't change the total. On bank statements those are:

  • Wrong year on a right date. A U.S. Bank statement that runs Dec 22 through Jan 21 prints "Dec 29" with no year. If the extractor stamps it 2026 instead of 2025, every number still ties. Fix it by taking the year from the statement period, not from the run date.
  • Right rows, wrong account. A Citi relationship statement can combine checking and savings in one PDF. If a $1,000 transfer lands under savings twice and checking zero times, the combined total can still look fine. Tie each account to its own printed opening and closing balance, never the file as a whole.
  • Two errors that cancel. Rare, but a dropped $86.00 fee and a $500.00 deposit read as $586.00 net to zero. The running balance catches it where the statement prints one per day. Where it doesn't, nothing does.
  • Descriptions. "AMZN MKTP" read as "AMZN MKIP" ties perfectly. It also breaks your categorization rules. That one needs a person or a vendor list.

So the rule I'd give anyone building this: check per account, check the running balance wherever the bank printed one, and take the year from the period. The closing balance alone gives you false confidence.

What this looks like in a converter

Here's the PNC-style month from the top of this post, as a PDF. I made it for this post (the footer says so), following the layout PNC typically uses: a Business Checking Summary, an overdraft fee table that isn't activity, Activity Detail split by kind, and a daily balance table with the trailing minus.

Fictional PNC-style business checking statement that opens overdrawn at 612.40-

And this is what the check looks like once it's built into a converter. ZeraBooks Convert, on a Chase-style business checking sample from our test set: every row read, and the proof at the top saying opening plus transactions equals the printed closing balance.

ZeraBooks Convert showing a converted business checking statement proven to its printed closing balance

The converter has no special case per bank. It reads amounts as printed, including trailing minus, CR and parentheses, takes the year from the statement period, and won't call a statement done until each account ties.

If you're writing your own pipeline, steal the gap table. If you'd rather not, ZeraBooks' bank statement OCR page walks through the same checks with a worked repair. Pricing is public: $79 a month billed annually or $149 month to month, 1,000 pages a month, with a 7-day trial.

The digits were never the hard part.

來源:DEV Community · dev.to