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.
--config-dir, if given- the current directory
- the directory holding
statement2csv.py ~/.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
-oare 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)
| File | Size | Uploaded | |
|---|---|---|---|
| statement2csv-0.1.0.tar.gz | 7.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|