一键导入
add-bank-parser
Add a new bank-specific account statement PDF parser. Use when adding support for parsing a new bank's savings/current account statement PDF.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a new bank-specific account statement PDF parser. Use when adding support for parsing a new bank's savings/current account statement PDF.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | add-bank-parser |
| description | Add a new bank-specific account statement PDF parser. Use when adding support for parsing a new bank's savings/current account statement PDF. |
This skill is interactive. It requires running Python/Bash to extract PDF data, iterating on the parser, and testing. Do not run this in the background. If you need tool permissions, ask for them.
Arguments: $ARGUMENTS — bank slug and path to sample PDF.
Read these files to understand the patterns:
bank_statement_parser/models.py — output schema (see Output Schema section below)bank_statement_parser/parsers/base.py — BankStatementParser ABC plus the shared _post_process / _build_statement helpers every parser usesbank_statement_parser/parsers/generic.py — GenericBankStatementParser (the class your parser will extend) and the default extract-tables-then-fall-back-to-wordlines flowbank_statement_parser/parsers/metadata.py — MetadataExtractor; most bank parsers subclass it and override regex class attributes (HDFC, Slice opt out and do manual extraction)bank_statement_parser/parsers/registry.py — canonical parser registrationbank_statement_parser/parsers/utils/ and bank_statement_parser/parsers/extractors/ — reusable helpers you should prefer over reinventing (dates, amounts, channel detection, counterparty extraction, table parsing, word-line grouping, column thresholds)Don't read a specific parser yet — wait until step 2 reveals whether the PDF uses tables or word-positioned text, then pick the most relevant existing parser to study.
MANDATORY. Do not write any parser code before completing this step.
The visual PDF layout and what pdfplumber extracts are often very different. You must run code to see what the extraction library actually produces. Run this via uv run python -c "..." from the bank-statement-parser directory:
from bank_statement_parser.extractor import extract_raw_pdf
from pathlib import Path
raw = extract_raw_pdf(Path("<pdf_path>"), include_blocks=False, password=None)
Then examine the output interactively — this may take multiple rounds:
from bank_statement_parser.parsers.extractors.wordlines import group_words_into_lines
lines = group_words_into_lines(raw["pages"][N]["words"])
w["x0"]) of the header line and a few data lines to determine column boundaries.If the PDF is encrypted, ask the user for the password.
Based on what you find, read the most relevant existing parser:
uboi.py or kotak.py (kotak shows clean tables plus a separate "Account Summary" table for opening/closing balances and a Chq/Ref column used as a reference fallback)indusind.pyidfc.pyicici.py or slice.py\n-delimited cells) → study hdfc.pyCreate bank_statement_parser/parsers/{bank}.py. Base your implementation on the actual pdfplumber output from step 2.
GenericBankStatementParser, override parse(), and set bank = "{slug}" and metadata_extractor = ... as class attributesMetadataExtractor with bank-specific regexes (account_number_pattern, period_pattern, name_pattern, opening_balance_pattern, closing_balance_pattern) — see icici.py IciciMetadataExtractor for an example that also overrides extract_period() for non-numeric date formats. If the layout is too irregular for the regex hooks (HDFC, Slice), do manual metadata extraction in parse() instead — both patterns are acceptedparse(), call self._post_process(transactions, raw_data) (assigns transaction_id zero-padded and zero-based: {bank}_txn_0000, _0001, …) and return via self._build_statement(...) so totals/counts formatting stays consistentparsers/utils/, parsers/extractors/, parsers/metadata.py, and parsers/reconciliation.pyparsers/utils/dates.py for all date parsing; output must stay DD/MM/YYYY. Pass bank-specific format_hints=[...] to parse_date_text() rather than editing the shared default list, unless the format is genuinely industry-widecounterparty via extract_counterparty(narration, channel) from parsers/utils/counterparty.py when the bank's structured narration layout matches one the helper already understands (UPI, IMPS, NEFT/RTGS, BIL/INFT|ONL — modelled on ICICI savings). It returns None when the layout doesn't match, which is the correct value to leave on the field. If your bank uses a different layout, leave counterparty unset rather than guessingparsers/registry.py: import the class and add one entry to PARSER_REGISTRY (keys are the slugs the CLI accepts via --bank)cli.py: add the slug to the BankOption StrEnum. The CLI module has a runtime sync check (if tuple(option.value for option in BankOption) != _SUPPORTED_BANK_SLUGS: raise RuntimeError(...)) that fires at import time, so registering only in PARSER_REGISTRY will break every CLI invocation with BankOption enum is out of sync with parser registryfactory.py already routes through the registry — no change needed thereRun: uv run bank-statement-parser <pdf_path> --bank {bank}
Check:
reconciliation.balance_delta is 0.00 — this is the critical checkchannel and reference_number populated for UPI/IMPS/NEFT/RTGS/netbanking rowscounterparty populated where structured narrations exist (UPI/IMPS/NEFT/RTGS/netbanking) — None for everything else is correctuv run ruff check bank_statement_parser/uv run ty check bank_statement_parser/If delta is not 0.00 or transactions are missing, go back to step 2 to examine the raw data more closely, fix the parser, and re-test. This is iterative.
Every parser must return a ParsedBankStatement (from models.py):
ParsedBankStatement:
file: str # PDF filename
bank: str # bank slug
account_holder_name: str | None
account_number: str | None
statement_period_start: str | None # DD/MM/YYYY
statement_period_end: str | None # DD/MM/YYYY
opening_balance: str | None # amount WITHOUT Cr/Dr suffix
closing_balance: str | None # amount WITHOUT Cr/Dr suffix
debit_count: int
credit_count: int
debit_total: str # formatted "1,234.56"
credit_total: str # formatted "1,234.56"
transactions: list[BankTransaction]
reconciliation: BankReconciliation | None
Each BankTransaction:
BankTransaction:
date: str # DD/MM/YYYY (REQUIRED)
narration: str # transaction description
amount: str # "1,234.56"
transaction_type: "debit" | "credit"
balance: str | None # running balance after this txn
reference_number: str | None # UTR/UPI ref
channel: str | None # upi, neft, rtgs, imps, atm, card, interest, netbanking, etc.
counterparty: str | None # cleaned merchant/payee from structured narrations
value_date: str | None # DD/MM/YYYY
transaction_id: str # "{bank}_txn_0000", "{bank}_txn_0001", … — zero-padded, zero-based, assigned by _post_process()
BankReconciliation:
BankReconciliation:
opening_balance: str
closing_balance: str
parsed_debit_total: str
parsed_credit_total: str
computed_closing_balance: str # opening + credits - debits
balance_delta: str # closing - computed (MUST be "0.00")
transaction_count: int
debit_count: int
credit_count: int
group_words_into_lines() and x-position thresholds to classify amounts." / "₹" characters get prepended by extraction. extract_amount()` strips backtick and ₹; for anything else, clean the token before calling it.extract_amount() (from parsers/utils/amounts.py) handles this.format_hints=[...] to parse_date_text() / parse_date() at the call site (this is what every existing parser does). Only edit _DEFAULT_FORMAT_HINTS in parsers/utils/dates.py if the format is genuinely industry-wide. Do not create another bank-local _parse_*_date.\n. Replace with spaces.detect_channel() uses standard prefixes (UPI/, IMPS/, NEFT/, BIL/INFT, NACH, etc.). Some banks use non-standard prefixes. Extend _CHANNEL_PATTERNS in parsers/utils/channels.py only when a pattern is genuinely cross-bank; otherwise do bank-specific tagging in your parser.extract_reference_number(narration, channel) returns None when channel is None or unknown — this is deliberate, to avoid pulling random digit runs out of cheque numbers, dates, or account fragments. Always run detect_channel() first and pass the result; don't bypass it by parsing digits yourself.extract_counterparty() only handles UPI, IMPS, NEFT/RTGS, and BIL/INFT|ONL netbanking layouts (modelled on ICICI savings). For other channels or unknown layouts it returns None, which is the correct behaviour — leave the field unset rather than guessing.ColumnThresholds. More robust than token counting.\n (e.g., cell[0] = "01 Mar 26\n02 Mar 26\n03 Mar 26\n..."). Split each cell on \n and align by index. Narrations span multiple lines and typically end with a marker like "Value Dt DD/MM/YYYY" — use this to split the joined narration back into per-transaction strings.TOTAL: / DATE / PAGE checks in icici.py.If you discover new patterns, edge cases, or pitfalls while building a parser, update this skill file with what you learned.