Skip to main content

statement2csv

CLI tool to convert bank statement PDF to CSV.

The statement layout is described by TOML config files (see hsbc.toml, barclays.toml for reference). So, adding a new bank means writing a config rather than changing any code. The extracted table is checked against the totals printed on page 1 for sanity check.

Requirements

Python 3.12 or newer.

Install

git clone https://github.com/fenwickyduck/statement2csv
cd statement2csv
pipx install .
mkdir -p ~/.config/statement2csv && cp *.toml ~/.config/statement2csv/

Only statement2csv.py is packaged into the wheel - the default bank configs (hsbc, barclays), if one wishes to use them, have to be copied out of the repo by hand.

Usage

usage: statement2csv [-h] [-o OUTPUT_PATH] [--config-dir CONFIG_DIR] [--bank BANK] [--no-verify] pdf_path

extract the transaction table out of a bank statement PDF

positional arguments:
  pdf_path              the statement to parse

options:
  -h, --help            show this help message and exit
  -o OUTPUT_PATH, --output_path OUTPUT_PATH
                        where to write the CSV (default: print to stdout)
  --config-dir CONFIG_DIR
                        directory to look for bank *.toml configs in first (default: the current directory, next to this script, then
                        ~/.config/statement2csv)
  --bank BANK           use the config with this bank name instead of detecting the bank
  --no-verify           write the CSV without checking it against the totals on page 1

Relative paths resolve against the directory you run the command from, not wherever the script is installed. Log messages go to stderr, so piping is safe:

statement2csv statement.pdf | csvlook

Exit codes

code meaning
0 success
1 could not extract the table (no config matched, missing file, anchor not found)
2 table extracted, but it does not reconcile with the page 1 totals

Exit 2 still writes the CSV; the output is there to inspect, it just does not add up.

Configs

Each config is a *.toml file. Every directory below is searched in order, and within each one files are read in alphabetical order; the first config whose detect string appears on page 1 wins.

  1. --config-dir, if given
  2. the current directory
  3. the directory holding statement2csv.py
  4. ~/.config/statement2csv/ (or $XDG_CONFIG_HOME/statement2csv/)

Use --bank NAME to match on bank.name instead, which is useful when auto-detection picks the wrong config or the statement has no detectable marker.

Writing a config for a new bank

The extractor works by locating text on the page and cropping to it, so a config is mostly a set of phrases to search for. Copy barclays.toml as a starting point.

[bank]
name = "barclays"      # what --bank matches on
detect = "BUKBGB22"    # a string that appears on page 1 of this bank's statements
currency = "£"         # stripped before parsing amounts

[region]
page_has_table = "Description"        # if absent from a page, that page is skipped
top_anchor = "Description"            # the table starts at this phrase
bottom_anchor = ["Continued", "Balance carried forward"]   # any one ends the table
left_anchor = "Date"                  # leftmost column header
right_anchor = "Balance £"            # rightmost column header

[[columns]]                           # one block per column, left to right
label = "Date"                        # the column header text, matched on the page
[[columns]]
label = "Description"
[[columns]]
label = "Money out £"
role = "payments"                     # subtracted from the running balance
[[columns]]
label = "Money in £"
role = "receipts"                     # added to the running balance
[[columns]]
label = "Balance £"
role = "balance"                      # compared against the running balance

[verification]                        # phrases on page 1; the amount that
start_balance = "Start balance"       # follows each one is read as the total
money_out = "Money out"
money_in = "Money in"
end_balance = "End balance"

Anchors are matched as whole words in reading order, so "Balance £" matches the two words Balance and £ sitting next to each other on the same line.

Configs are validated before any pages are parsed, so a mistake fails immediately rather than after a long extraction. The payments, receipts and balance roles are all required. The [verification] section is required unless you pass --no-verify.

Verification

After the table is parsed, the running balance is recomputed from the transaction rows and compared against the four totals printed on page 1.

Each row's stated balance is also checked as it goes, so a mismatch names the row that broke:

statement2csv: running balance (1039.89)!= stated balance (1096.00) in [...]

Amounts are parsed as Decimal, not floating point, so the comparisons are exact.

Pass --no-verify to skip the whole check.

Output format

The CSV mirrors the printed table rather than reinterpreting it.

Three caveats:

  • A transaction whose description wraps produces one row per printed line. The date and amounts appear on the first row only; continuation rows carry the rest of the description with every other field empty. Group them yourself if you want one record per transaction.
  • Each page contributes its own header row, so on a multi-page statement the column headers recur partway through the file rather than appearing only once.
  • Files written with -o are UTF-8 with a BOM, so Excel opens the £ signs correctly. Output sent to stdout has no BOM.

Metadata

Release files for statement2csv 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for statement2csv 0.1.0
File Size Uploaded
statement2csv-0.1.0.tar.gz 7.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for statement2csv 0.1.0
File Interpreter ABI Platform
statement2csv-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 15.8 kB

Release files / statement2csv-0.1.0.tar.gz

Download URL statement2csv-0.1.0.tar.gz
Size 7.7 kB
Tags Source
SHA-256 checksum
How to use checksums
246693b248db8bd6d1267859c6fdcf2bba1d263d851c6bc63364491d9bb47c52
BLAKE2b-256 checksum
How to use checksums
68009282300de5090cb3e32861ba19bb05038bf75f6cb82e9f5dbd7cfc9d6066
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / statement2csv-0.1.0-py3-none-any.whl

Download URL statement2csv-0.1.0-py3-none-any.whl
Size 8.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba46c039e4d132686be4009a8faf1d8ba284a92195e0e2352a88625b257d8088
BLAKE2b-256 checksum
How to use checksums
beddb7cdfdf9f5e4841cb537e8140fda2000f40058169f8fc5f9e3365f817bc9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page