AIAditya Uke

Bank Statement PDF to CSV: Why Generic PDF-to-Excel Converters Break

Converting a bank statement PDF to CSV isn't a generic PDF-to-Excel problem. Here's why converters fail and what a statement-aware pipeline does differently.

The CSV looks clean, the headers line up, and the numbers are still wrong.

Every accountant has run this experiment, feeding a statement into Adobe, Smallpdf, or iLovePDF and expecting a clean bank statement PDF to CSV conversion in one click.

The first page looks fine, but then a description wraps to two lines and the tool splits it into two transactions. By page three, the column headers are gone and the balance column has merged with the amount column.

Nobody notices until the books don’t reconcile.

At Systenics, we run bankstatementscsv.com and cardstatementcsv.com in production, converting real bank and card statements for accountants and bookkeepers every day.

We’ve also written about the broader problem in production document AI. This post covers why statements specifically, not invoices or contracts, break generic tools, and what actually fixes it.

Why statements are a different problem

Generic PDF-to-Excel converters do one thing: they detect visual columns on a page and dump the text into a grid. That works fine for a simple table, but a bank statement isn’t a simple table.

It’s a sequence of transactions. Each one is a logical record that might span two or three visual lines, and the converter has no concept of “transaction.” It only sees ink on a page.

GENERIC CONVERTER VIEW OF A STATEMENT:

Page 1: [Date] [Description] [Debit] [Credit] [Balance]   <- headers detected
         03/02  AMAZON MKTPLACE          45.20            1,204.80
         03/03  CHECK #1042 PAYMENT      120.00            1,084.80
                TO JOHN SMITH FOR                                     <- orphan line, no columns
                INVOICE 3321
         03/05  PAYROLL DEPOSIT                  2,500.00  3,584.80

Page 2: (no headers repeated)
         03/06  ATM WITHDRAWAL            60.00            3,524.80   <- columns may shift here
         03/08  WIRE TRANSFER INTL                          412.10   3,112.70

Result: orphan lines become garbage rows, column positions drift,
        sign of debit/credit gets lost, balance column misaligns.

Five things cause the breakage, every time.

Digital vs. scanned PDFs. A digital statement has a real text layer, while a scanned one is a picture of paper, skewed a few degrees, with a coffee ring in the margin. Converters tuned for digital PDFs fall apart on scans, and vice versa.

Wrapped descriptions. Long merchant names or memo lines wrap onto a second or third line, and a column-detection tool treats each wrapped line as a new row. Now you have three “transactions” for one payment.

Headers vanish after page one. Most statement templates only print column headers on the first page, so page two onward is just numbers in roughly the same x-position. The tool guesses, and guesses wrong when spacing shifts even slightly.

Debit and credit sit in different columns per bank. Chase puts debits and credits in separate columns, another bank uses one “amount” column with a minus sign, and a third uses parentheses for negatives. A generic tool has no bank-specific logic, so sign handling is a coin flip.

Running balance correctness. Only a statement-aware tool actually checks that opening balance plus the sum of transactions equals the closing balance. Generic converters don’t know a “balance” column exists conceptually, so they never validate it.

Comparing the options

Capability Generic Converters (Adobe/Smallpdf/iLovePDF class) Excel “Get Data from PDF” Open-Source Table Extractors (pdfplumber, Camelot, Tabula) General OCR + LLM Statement-Specific Tools
Digital PDFs Decent Decent Good Good Good
Scanned PDFs Poor Very poor Poor without preprocessing Good with the right pipeline Good, built for it
Multi-line descriptions Breaks rows apart Breaks rows apart Needs custom rules per bank Handles it, if prompted correctly Handles it natively
Running balance correctness Not checked Not checked Not checked Not checked unless you add it Checked and reconciled
Page 2+ without headers Fails silently Fails silently Needs per-template config Handles it with layout memory Handles it natively
Debit/credit sign handling Inconsistent Inconsistent Manual per bank Inconsistent without rules Bank-aware mapping
Batch volume One file at a time, mostly One file at a time Scriptable, but you build it Scriptable, costs scale with pages Built for batches
Price Free to ~$15/mo Included in Office Free, but engineering time isn’t Token costs plus dev time Priced per statement/month

Open-source table extractors deserve credit. Pdfplumber, Camelot, and Tabula are solid building blocks, but they aren’t finished products.

You still have to write the logic yourself. It has to turn “text at these coordinates” into “this is a transaction row,” bank by bank.

General OCR plus an LLM gets closer. It reads messy scans and can reason about context. But without a reconciliation step, it will confidently invent or drop a row and you won’t know until the totals are off.

A statement-aware pipeline

Here’s the shape of a pipeline that actually holds up in production.

Raw PDF (digital or scanned)
        │
        ▼
  1. Detect layout
     - digital text layer vs. scanned image
     - deskew and denoise if scanned
     - identify bank template if known
        │
        ▼
  2. Segment transactions
     - group wrapped lines into one logical record
     - carry column positions forward past page 1
        │
        ▼
  3. Extract fields
     - date, description, debit/credit, running balance
     - normalize each bank's sign convention
        │
        ▼
  4. Reconcile
     - opening balance + sum(transactions) == closing balance?
        │
        ▼
  5. Flag mismatches for review
     - anything that doesn't reconcile goes to a human, not to the CSV

Step 4 is the part almost nobody builds. It’s also the cheapest check that catches the most damage.

Worked example: the reconciliation check

Here’s a minimal Python check you can run against any extracted set of rows, regardless of which tool produced them.

from decimal import Decimal

def reconcile(opening_balance, transactions, closing_balance, tolerance=Decimal("0.01")):
    """
    transactions: list of dicts like {"amount": Decimal, "type": "debit"|"credit"}
    amount is always positive; type says which direction it moves the balance.
    """
    running = opening_balance
    for i, txn in enumerate(transactions):
        if txn["type"] == "credit":
            running += txn["amount"]
        elif txn["type"] == "debit":
            running -= txn["amount"]
        else:
            raise ValueError(f"Row {i} has no debit/credit type: {txn}")

    diff = abs(running - closing_balance)
    if diff > tolerance:
        return {
            "ok": False,
            "expected_closing": closing_balance,
            "computed_closing": running,
            "difference": diff,
        }
    return {"ok": True, "computed_closing": running}

# Example usage
opening = Decimal("1204.80")
closing = Decimal("3112.70")
rows = [
    {"amount": Decimal("45.20"), "type": "debit"},
    {"amount": Decimal("120.00"), "type": "debit"},
    {"amount": Decimal("2500.00"), "type": "credit"},
    {"amount": Decimal("60.00"), "type": "debit"},
    {"amount": Decimal("412.10"), "type": "debit"},
]

result = reconcile(opening, rows, closing)
print(result)

This check catches what no visual inspection will. If a wrapped description got split into a phantom extra row, the sum drifts and ok comes back False.

If a debit got misread as a credit, the drift is roughly double the transaction amount, an easy pattern to flag. If a page-two row got dropped entirely because headers vanished, the running total falls short and the mismatch shows up immediately.

A person scanning a CSV by eye won’t catch any of this. Numbers look plausible one row at a time. Reconciliation is the only check that looks at the whole statement as a closed system.

What usually goes wrong

Trusting row count as a sanity check. A file with 40 transactions producing 42 rows looks “close enough” to some reviewers, but it isn’t. Two of those rows are garbage from a split description, and they’re throwing off every downstream report.

Assuming one bank’s format generalizes. A script tuned for Chase statements will silently misparse a credit union statement with a different column order. Debit and credit swap, and nothing errors out, it just produces wrong numbers.

Skipping OCR preprocessing on scans. Feeding a skewed, low-contrast scan straight into extraction, digital or AI-based, degrades accuracy fast. Deskewing and cleaning the image first is unglamorous work, but it matters more than which extraction library you pick.

No human review path. Even a good pipeline will hit an edge case: a statement format it’s never seen, a corrupted PDF, a bank that changed its layout. Without a flagged-for-review queue, that edge case becomes a silent bad number in someone’s ledger.

A quick summary

Bank statements aren’t generic tables, they’re sequences of transactions wrapped across lines and pages, with sign conventions and layouts that differ bank to bank. Column-detection tools, whether that’s Adobe, Excel, or a spreadsheet macro, have no concept of a transaction. They just see text on a grid.

The fix isn’t a smarter single conversion step. It’s a pipeline that segments transactions before extracting fields. It also reconciles opening balance against closing balance before anything reaches a CSV.

That check is cheap to build. It catches hallucinated rows, dropped rows, and sign errors that no eyeball review will.

We built bankstatementscsv.com and cardstatementcsv.com around exactly this pain, and csvnormalize.com to clean up tables other tools already mangled.

If you’ve got a handful of statements to convert, try the tools directly. For high volume or a custom pipeline wired into your own systems, book a free Diagnosis call and we’ll look at your document AI needs.

More on this topic:AI