Skip to content

Instantly share code, notes, and snippets.

@majabojarska
Created September 27, 2025 00:24
Show Gist options
  • Save majabojarska/4ae5c7ce056fecddb923d8a577ab7bc4 to your computer and use it in GitHub Desktop.
Save majabojarska/4ae5c7ce056fecddb923d8a577ab7bc4 to your computer and use it in GitHub Desktop.
"""
This script processes the bank transaction CSV export from the Polish ING bank site
into an encoding and structure ready to import into Actual (https://www.actualbudget.com/).
What this does:
- Reads and decodes the in_file CSV as 'windows-1250'.
- Trims legal cruft and account owner data from the top and bottom of the in_file CSV.
- Changes field separators from ';' to ','.
- Reorders and renames fields to match Actual's import process.
- Encodes the output as UTF8.
"""
import argparse
import csv
from pathlib import Path
from typing import Mapping, IO
import sys
ENCODING_IN: str = "windows-1250"
ENCODING_OUT: str = "utf8"
DELIMITER_IN: str = ";"
DELIMITER_OUT: str = ","
OFFSET_TOP: int = 18
OFFSET_BOTTOM: int = 3
# Keys define output order w.r.t. original naming.
# Values define the new column names, corresponding to the keys.
FIELD_NAME_MAP: Mapping[str, str] = {
"Data transakcji": "Date",
"Dane kontrahenta": "Payee",
"Tytuł": "Notes",
"": "Category",
"Kwota transakcji (waluta rachunku)": "Amount",
}
def _get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"This script processes the bank transaction CSV export from the Polish ING bank site\n"
"into an encoding and structure that's ready to import into Actual (https://www.actualbudget.com/).\n"
"\n"
"What this does:\n"
"* Reads and decodes the in_file CSV as 'windows-1250'.\n"
"* Trims legal cruft and account owner data from the top and bottom of the in_file CSV.\n"
"* Changes field separators from ';' to ','.\n"
"* Reorders and renames fields to match Actual's import process.\n"
"* Encodes the output as UTF8.\n"
),
epilog="Writes output to STDOUT.",
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=True,
)
parser.add_argument(
"csv",
help="path to CSV file exported from Polish ING website",
type=Path,
)
return parser
def _process_ing_csv(reader: IO, writer: IO) -> None:
# Trim cruft at the top and bottom of the file, leaving only
# the CSV header and actual data.
lines: int = reader.readlines()[
# Careful with off by ones!
OFFSET_TOP : (-OFFSET_BOTTOM + 1)
]
reader = csv.DictReader(
lines,
delimiter=DELIMITER_IN,
quoting=csv.QUOTE_MINIMAL,
)
# Write translated header
print(*FIELD_NAME_MAP.values(), sep=DELIMITER_OUT, file=writer)
csv.DictWriter(
sys.stdout,
fieldnames=FIELD_NAME_MAP,
restval="",
extrasaction="ignore",
quoting=csv.QUOTE_MINIMAL,
).writerows(reader)
def main():
parsed = _get_parser().parse_args()
with open(parsed.csv, "r", newline="", encoding=ENCODING_IN) as in_file:
_process_ing_csv(reader=in_file, writer=sys.stdout)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment