rehan953 commited on
Commit
6fbc105
·
verified ·
1 Parent(s): b8e9752

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -0
app.py CHANGED
@@ -1586,6 +1586,150 @@ def _extract_fused_desc_amount(grid):
1586
  return new_grid
1587
 
1588
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1589
  def normalize_html_tables(text: str) -> str:
1590
  """
1591
  For every <table>...</table>:
@@ -1612,6 +1756,13 @@ def normalize_html_tables(text: str) -> str:
1612
  p = TableGridParser()
1613
  p.feed(table_html)
1614
  grid = _build_grid(p.rows)
 
 
 
 
 
 
 
1615
  grid = _drop_truly_empty_columns(grid)
1616
  grid = _merge_blank_header_text_columns(grid)
1617
 
@@ -1626,6 +1777,9 @@ def normalize_html_tables(text: str) -> str:
1626
  # Fix 2: misplaced header row promotion (keyword-score guard — safe on clean PDFs)
1627
  grid = _promote_misplaced_header_row(grid)
1628
 
 
 
 
1629
  # Fix 6: merge split rows where description wraps to next line
1630
  # (Hardin County Bank and similar monospace statement formats)
1631
  grid = _merge_split_rows(grid)
 
1586
  return new_grid
1587
 
1588
 
1589
+ def _is_junk_table(grid):
1590
+ """
1591
+ Detect and suppress known junk tables that are OCR artifacts from
1592
+ page headers/footers, not real transaction data.
1593
+
1594
+ Current patterns:
1595
+ 1. CUSTOMER INFORMATION fragment tables (First Horizon Bank):
1596
+ Single-column table with header "CUSTOMER INFORMATION" and
1597
+ data rows that are short numeric fragments (e.g. "254", "24")
1598
+ from the partial account number / year printed in the header band.
1599
+ """
1600
+ if not grid or len(grid) < 2:
1601
+ return False
1602
+ ncols = len(grid[0])
1603
+ if ncols != 1:
1604
+ return False
1605
+ header_text = str(grid[0][0] or "").strip().upper()
1606
+ if "CUSTOMER INFORMATION" in header_text:
1607
+ # All data rows should be short (< 10 chars) numeric/alphanumeric fragments
1608
+ data_rows = grid[1:]
1609
+ if all(len(str(r[0] or "").strip()) <= 10 for r in data_rows):
1610
+ return True
1611
+ return False
1612
+
1613
+
1614
+ def _split_fused_multicolumn_header(grid):
1615
+ """
1616
+ Fix OCR artifact where multiple column headers are fused into one <th> cell.
1617
+
1618
+ Example (First Horizon Bank alternating pages):
1619
+ Fused: ['DATE DESCRIPTION CARD #', 'DEPOSIT', 'WITHDRAWAL']
1620
+ Correct: ['DATE', 'DESCRIPTION', 'DEPOSIT', 'WITHDRAWAL', 'CARD #']
1621
+
1622
+ The data rows also have date + description + card# all in col 0:
1623
+ Fused: ['01/16 PURCHASE - UBER TRIP... 4919', '', '$21.02']
1624
+ Correct: ['01/16', 'PURCHASE - UBER TRIP...', '', '$21.02', '4919']
1625
+
1626
+ Detection: first header cell contains 2+ keyword terms separated by spaces,
1627
+ AND remaining header cells are valid money column keywords.
1628
+ """
1629
+ if not grid or len(grid) < 2:
1630
+ return grid
1631
+
1632
+ ncols = len(grid[0])
1633
+ if ncols < 2:
1634
+ return grid
1635
+
1636
+ first_header = str(grid[0][0] or "").strip()
1637
+ # Split first header cell into parts and check if multiple are keywords
1638
+ parts = first_header.split()
1639
+ kw_hits = [p for p in parts if _HEADER_KW_EXACT_RE.match(p)]
1640
+ if len(kw_hits) < 2:
1641
+ return grid # not a fused multi-keyword header
1642
+
1643
+ # The remaining header cells must all be keyword columns
1644
+ rest_headers = [str(c or "").strip() for c in grid[0][1:]]
1645
+ if not all(_HEADER_KW_EXACT_RE.match(h) for h in rest_headers if h):
1646
+ return grid
1647
+
1648
+ # Build the new expanded header:
1649
+ # Split the fused first cell into individual keyword columns
1650
+ # Keep non-keyword trailing parts (e.g. "CARD #") as the last new col
1651
+ new_header_cols = []
1652
+ non_kw_parts = []
1653
+ for p in parts:
1654
+ if _HEADER_KW_EXACT_RE.match(p):
1655
+ if non_kw_parts:
1656
+ new_header_cols.append(" ".join(non_kw_parts))
1657
+ non_kw_parts = []
1658
+ new_header_cols.append(p)
1659
+ else:
1660
+ non_kw_parts.append(p)
1661
+ if non_kw_parts:
1662
+ new_header_cols.append(" ".join(non_kw_parts))
1663
+
1664
+ # Full new header = expanded first cell + remaining cells
1665
+ new_header = new_header_cols + rest_headers
1666
+ new_ncols = len(new_header)
1667
+ extra_cols = new_ncols - ncols # how many new columns were added
1668
+
1669
+ # Identify column roles in the new header
1670
+ _DATE_COL_RE = re.compile(r"\bDATE\b", re.IGNORECASE)
1671
+ _CARD_COL_RE = re.compile(r"\bCARD\b", re.IGNORECASE)
1672
+ _DESC_COL_RE = re.compile(r"\b(DESCRIPTION|DETAILS?|NARRATION|PARTICULARS?)\b", re.IGNORECASE)
1673
+ _MONEY_COL_RE = re.compile(r"\b(DEPOSIT|WITHDRAWAL|DEBIT|CREDIT|AMOUNT|ADDITION|SUBTRACTION)S?\b", re.IGNORECASE)
1674
+
1675
+ new_date_col = next((i for i,h in enumerate(new_header) if _DATE_COL_RE.search(h)), None)
1676
+ new_desc_col = next((i for i,h in enumerate(new_header) if _DESC_COL_RE.search(h)), None)
1677
+ new_card_col = next((i for i,h in enumerate(new_header) if _CARD_COL_RE.search(h)), None)
1678
+
1679
+ if new_date_col is None or new_desc_col is None:
1680
+ return grid # can't map columns
1681
+
1682
+ # Re-split each data row: col 0 had "date description... card#" fused together
1683
+ _DATE_PREFIX = re.compile(r"^(\d{1,2}/\d{2}(?:/\d{2,4})?)\s+(.*)", re.DOTALL)
1684
+ _CARD_SUFFIX = re.compile(r"^(.*?)\s+(\d{4})$", re.DOTALL) # 4-digit card number at end
1685
+
1686
+ def pad(row, n):
1687
+ r = list(row)
1688
+ while len(r) < n: r.append("")
1689
+ return r[:n]
1690
+
1691
+ new_grid = [pad(new_header, new_ncols)]
1692
+ for row in grid[1:]:
1693
+ cells = [str(c or "").strip() for c in row]
1694
+ while len(cells) < ncols: cells.append("")
1695
+
1696
+ fused_cell = cells[0]
1697
+ rest_cells = cells[1:]
1698
+
1699
+ # Extract date from front
1700
+ date_val = ""
1701
+ desc_val = fused_cell
1702
+ card_val = ""
1703
+
1704
+ dm = _DATE_PREFIX.match(fused_cell)
1705
+ if dm:
1706
+ date_val = dm.group(1)
1707
+ desc_val = dm.group(2).strip()
1708
+
1709
+ # Extract card# from end (4-digit number)
1710
+ if new_card_col is not None:
1711
+ cm = _CARD_SUFFIX.match(desc_val)
1712
+ if cm:
1713
+ desc_val = cm.group(1).strip()
1714
+ card_val = cm.group(2)
1715
+
1716
+ # Build new row
1717
+ new_row = [""] * new_ncols
1718
+ new_row[new_date_col] = date_val
1719
+ new_row[new_desc_col] = desc_val
1720
+ if new_card_col is not None:
1721
+ new_row[new_card_col] = card_val
1722
+ # Fill remaining money columns from rest_cells
1723
+ money_col_indices = [i for i,h in enumerate(new_header) if _MONEY_COL_RE.search(h)]
1724
+ for mi, ri in enumerate(range(len(rest_cells))):
1725
+ if mi < len(money_col_indices):
1726
+ new_row[money_col_indices[mi]] = rest_cells[ri]
1727
+
1728
+ new_grid.append(new_row)
1729
+
1730
+ return new_grid
1731
+
1732
+
1733
  def normalize_html_tables(text: str) -> str:
1734
  """
1735
  For every <table>...</table>:
 
1756
  p = TableGridParser()
1757
  p.feed(table_html)
1758
  grid = _build_grid(p.rows)
1759
+
1760
+ # Suppress known junk tables (OCR artifacts from page headers)
1761
+ if _is_junk_table(grid):
1762
+ out.append("")
1763
+ last = m.end()
1764
+ continue
1765
+
1766
  grid = _drop_truly_empty_columns(grid)
1767
  grid = _merge_blank_header_text_columns(grid)
1768
 
 
1777
  # Fix 2: misplaced header row promotion (keyword-score guard — safe on clean PDFs)
1778
  grid = _promote_misplaced_header_row(grid)
1779
 
1780
+ # Fix 8: split fused multi-keyword header cell (First Horizon Bank)
1781
+ grid = _split_fused_multicolumn_header(grid)
1782
+
1783
  # Fix 6: merge split rows where description wraps to next line
1784
  # (Hardin County Bank and similar monospace statement formats)
1785
  grid = _merge_split_rows(grid)