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:
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.
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.
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.
Through multiple iterations we converged on a reliable pipeline.
Each stage solves a different part of the problem.
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.
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.
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.
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.
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;
}
}
Across statements from different banks we observed substantial layout variations, even when the documents looked similar at first glance.
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.
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.
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);
}
}
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 encountered in production include:
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.
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
This data can then power:
For these cases, OCR fallback and document-specific tuning may still be necessary.
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.