Extracting Tables from Financial PDFs

By StatementSheet Engineering Team

Extracting tables from financial PDFs looks simple — until you try doing it at scale.

Invoices, bank statements, brokerage reports, and payment summaries all contain structured financial data. Yet the moment you attempt to extract these tables programmatically, you discover something surprising:

PDFs don’t actually contain tables.

They contain positioned characters on a page.

At StatementSheet, we learned quickly that extracting financial tables reliably requires rebuilding structure from visual signals rather than reading plain text.

This article explains the engineering approach we use to detect and reconstruct financial tables from PDFs.

The illusion of tables in PDFs

When you open a financial statement, you see something like this:

Date Description Debit Credit Balance
03/14 Amazon Marketplace 49.90 950.10

It looks like a normal table.

But inside the PDF the data actually looks closer to this:

X Y Text
42 510 03/14
120 510 Amazon Marketplace
460 510 49.90
540 510 950.10

The PDF format simply describes where text should be drawn on the page. There is no native concept of rows, columns, cells, or tables.

Which means extracting tables is really about reconstructing layout from geometry.

A real-world problem we encountered

At StatementSheet, we discovered that many financial documents break naive table extraction. One common issue is description overflow: a merchant description may extend into numeric columns.

Example extracted line:

03/14   AMAZON MARKETPLACE ORDER 29384 REFUND    49.90   950.10

A naive parser may incorrectly produce:

Date Description Debit
03/14 AMAZON MARKETPLACE ORDER 29384 REFUND

Clearly wrong. The parser mistook part of the description for a financial value. This is why financial table extraction requires layout analysis, not just text parsing.

The pipeline for extracting financial tables

Through multiple iterations we converged on a reliable pipeline.

PDF page ↓ Extract positioned text ↓ Rebuild visual lines ↓ Detect table regions ↓ Identify column boundaries ↓ Normalize rows ↓ Validate financial data

Each stage solves a different part of the problem.

Step 1 — Extract text with coordinates

Using libraries such as Apache PDFBox, we extract text elements along with their positions.

Example data structure:

public record TextElement(
    int page,
    String text,
    float x,
    float y,
    float width,
    float height
) {}

Each fragment of text now has a coordinate on the page. This allows us to reconstruct visual structure.

Step 2 — Rebuild visual rows

PDF text is often split into multiple fragments. We group fragments into rows based on vertical proximity.

Example Java logic:

public class LineBuilder {

    private static final float Y_TOLERANCE = 3f;

    public List<List<TextElement>> group(List<TextElement> elements){

        List<List<TextElement>> lines = new ArrayList<>();

        for(TextElement el : elements){

            boolean added = false;

            for(List<TextElement> line : lines){

                TextElement ref = line.get(0);

                if(Math.abs(ref.y() - el.y()) < Y_TOLERANCE){
                    line.add(el);
                    added = true;
                    break;
                }
            }

            if(!added){
                List<TextElement> newLine = new ArrayList<>();
                newLine.add(el);
                lines.add(newLine);
            }
        }

        return lines;
    }
}

This step reconstructs something close to the visual lines humans see.

Step 3 — Detect table regions

Not all text blocks are tables. Financial documents often contain:

We identify table regions using signals such as:

A table usually contains repeating column patterns across multiple rows.

Step 4 — Identify financial columns

Once a table region is detected, we identify column types such as:

We detect these using heuristics.

Dates:

Pattern.compile("\d{2}/\d{2}/\d{4}")

Amounts:

Pattern.compile("-?\d+[.,]\d{2}")

Numeric columns often share right-aligned positions, which is another strong signal.

How We Determined Column Thresholds

Column detection looked simple on paper, but threshold tuning turned out to be one of the most important parts of the extraction pipeline.

We tested different grouping thresholds while clustering X coordinates across rows.

In practice, a threshold that is too small treats the same column as multiple separate columns. A threshold that is too large collapses nearby numeric columns into one.

public class ColumnClusterer {

    private static final float VARIANCE_THRESHOLD = 8f;

    public List<Float> cluster(List<Float> xPositions){

        List<Float> columns = new ArrayList<>();

        for(float x : xPositions){

            boolean added = false;

            for(int i = 0; i < columns.size(); i++){

                float existing = columns.get(i);

                if(Math.abs(existing - x) < VARIANCE_THRESHOLD){
                    columns.set(i, (existing + x) / 2);
                    added = true;
                    break;
                }
            }

            if(!added){
                columns.add(x);
            }
        }

        return columns;
    }
}

Layout Variations We Observed

Across statements from different banks we observed substantial layout variations, even when the documents looked similar at first glance.

At first we assumed column detection would be trivial. But a single long merchant description broke the entire parser.

Multi-bank layout example

Across statements from different banks we observed several column layouts. For example, Canadian bank BMO may appear in more than one statement format.

BMO Bank A
Date Description Debit Credit Balance
BMO Bank B
Date Description Amount

This means the extractor cannot assume a single universal schema. It must infer the layout from the page itself.

Step 5 — Normalize multi-line rows

Financial tables frequently contain multi-line entries.

03/14   AMAZON MARKETPLACE
        ORDER 29384
                49.90

We merge continuation rows until a new transaction date appears.

Example model:

public record TransactionRow(
    String date,
    String description,
    String debit,
    String credit,
    String balance
) {}

This allows us to produce consistent structured output.

Parsing financial values

Financial documents contain multiple numeric formats:

49.90
1,203.45
(49.90)
-49.90
49,90 €

We normalize these formats before assigning them to structured fields.

public class ValueParser {

    private static final Pattern AMOUNT_PATTERN =
        Pattern.compile("[-(]?\\d{1,3}(?:[.,]\\d{3})*(?:[.,]\\d{2})[)]?");

    public BigDecimal parse(String text){

        Matcher matcher = AMOUNT_PATTERN.matcher(text);

        if(!matcher.find()){
            return null;
        }

        String value = matcher.group()
                .replace(",", "")
                .replace("(", "-")
                .replace(")", "");

        return new BigDecimal(value);
    }
}

Mini business validation

After extraction we apply a lightweight business validation rule to catch obvious parsing errors.

Balance validation rule:

previous_balance - debit + credit = next_balance

This is not always available on every statement, but when balance values are present it provides a useful sanity check.

Edge Cases

Edge cases encountered in production include:

Debug story

One of our earliest bugs came from a long merchant description that spilled across the entire row. The parser initially treated part of the continuation text as a numeric field, which corrupted every following row.
03/14   AMAZON MARKETPLACE ORDER 29384 REFUND
        ONLINE PURCHASE
                49.90

Fixing this forced us to combine line reconstruction, column clustering, and row normalization rather than treating them as separate isolated steps.

From raw PDF to structured financial data

Once tables are reconstructed, they can be exported as structured datasets.

Date,Description,Debit,Credit,Balance
2024-03-14,Amazon Marketplace,49.90,,950.10
At StatementSheet, this extraction layer powers a product that converts financial PDFs into structured Excel and CSV datasets.

This data can then power:

Known limitations

Even with strong heuristics, financial PDFs remain inconsistent. Some common challenges include scanned documents requiring OCR, irregular layouts, merged cells, rotated tables, and institution-specific formatting.

For these cases, OCR fallback and document-specific tuning may still be necessary.

Final thoughts

Extracting tables from financial PDFs is not simply a parsing task.

It is a document layout reconstruction problem.

Reliable systems combine:

Once these pieces are in place, messy financial PDFs can be transformed into structured data ready for analysis.

← Previous article How We Built a Reliable PDF Bank Statement Parser
Next article → Detecting Transaction Tables in Bank Statements