105 lines
4.9 KiB
Python
105 lines
4.9 KiB
Python
import camelot
|
|
import pandas as pd
|
|
import re
|
|
|
|
def extract_bank_data(file_path):
|
|
print(f"Reading {file_path}... processing all pages.")
|
|
|
|
# We use 'stream' flavor for the NLB PDF layout
|
|
tables = camelot.read_pdf(file_path, pages='1-end', flavor='stream')
|
|
|
|
final_transactions = []
|
|
current_tx = None
|
|
|
|
# Precise Regex Patterns
|
|
iban_pattern = r'SI56\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}'
|
|
amount_pattern = r'=(\d+[\.,]\d{2})'
|
|
date_pattern = r'(\d{2}\.\d{2}\.\d{2})'
|
|
|
|
for table in tables:
|
|
df = table.df
|
|
for _, row in df.iterrows():
|
|
cells = [str(c).strip() for c in row]
|
|
row_text = " ".join(cells)
|
|
|
|
# 1. TRIGGER: A new transaction starts with an amount (=xx,xx)
|
|
amount_match = re.search(amount_pattern, row_text)
|
|
date_match = re.search(date_pattern, row_text)
|
|
|
|
if amount_match and date_match:
|
|
# Save previous transaction
|
|
if current_tx:
|
|
final_transactions.append(current_tx)
|
|
|
|
# Assign Debit/Credit based on the column index of the '=' sign
|
|
amt_val = amount_match.group(1)
|
|
is_credit = any('=' in cells[i] for i in range(len(cells)) if i >= 2)
|
|
|
|
current_tx = {
|
|
"value date": date_match.group(1),
|
|
"Creditor / Debtor": cells[0].split('\n')[0], # Only take the first line of text
|
|
"account": "",
|
|
"payment details": "",
|
|
"debit": "" if is_credit else amt_val,
|
|
"credit": amt_val if is_credit else "",
|
|
"is_collecting_details": False
|
|
}
|
|
|
|
# Check if IBAN is in this row
|
|
iban_search = re.search(iban_pattern, cells[0])
|
|
if iban_search:
|
|
current_tx["account"] = iban_search.group(0)
|
|
|
|
elif current_tx:
|
|
# 2. CONTINUATION: Extract info from rows following the amount row
|
|
text_line = cells[0]
|
|
if not text_line:
|
|
continue
|
|
|
|
# Check for IBAN
|
|
iban_search = re.search(iban_pattern, text_line)
|
|
if iban_search:
|
|
current_tx["account"] = iban_search.group(0)
|
|
# Once we hit the IBAN, everything following it is usually 'Payment Details'
|
|
current_tx["is_collecting_details"] = True
|
|
remaining = re.sub(iban_pattern, '', text_line).strip()
|
|
# Filter out BIC/Internal Routing codes
|
|
remaining = re.sub(r'[A-Z]{8,11}', '', remaining).strip()
|
|
if remaining:
|
|
current_tx["payment details"] += " " + remaining
|
|
continue
|
|
|
|
# Identify if this is a Payment Detail row (Reference numbers or purpose text)
|
|
# We filter out common address keywords (Cesta, Ulica, Trg, Postcodes)
|
|
is_address = any(kw in text_line.upper() for kw in ["CESTA", "ULICA", "TRG", " LJUBLJANA", " LOGATEC"])
|
|
|
|
if current_tx["is_collecting_details"] or ("SI00" in text_line or "SI12" in text_line):
|
|
# Filter out routing codes like BACXSI...
|
|
clean_text = re.sub(r'[A-Z]{8,11}', '', text_line).strip()
|
|
if clean_text:
|
|
current_tx["payment details"] = (current_tx["payment details"] + " " + clean_text).strip()
|
|
elif not is_address and not current_tx["account"]:
|
|
# If it's not an address and we don't have an IBAN yet, it might be Name continuation
|
|
if len(current_tx["Creditor / Debtor"]) < 20: # Only append if the name is very short
|
|
current_tx["Creditor / Debtor"] += " " + text_line
|
|
|
|
# Append the last transaction
|
|
if current_tx:
|
|
final_transactions.append(current_tx)
|
|
|
|
# 3. Clean up and Export
|
|
df_final = pd.DataFrame(final_transactions)
|
|
if "is_collecting_details" in df_final.columns:
|
|
df_final = df_final.drop(columns=["is_collecting_details"])
|
|
|
|
# Final text cleaning (remove double spaces, routing codes)
|
|
df_final["payment details"] = df_final["payment details"].str.replace(r'[A-Z]{8,11}', '', regex=True)
|
|
df_final["payment details"] = df_final["payment details"].str.replace(r'\s+', ' ', regex=True).str.strip()
|
|
|
|
# Export
|
|
output_file = "bank_export_fixed_v3.csv"
|
|
df_final.to_csv(output_file, sep=';', index=False, encoding='utf-8-sig')
|
|
print(f"Success! Exported {len(df_final)} transactions to {output_file}")
|
|
|
|
if __name__ == "__main__":
|
|
extract_bank_data("MSDataReport (1).pdf") |