Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Parser hardening + brand aliases: multi-issuer layouts, NUL/trim/control-char safety
Browse files- app/statement_parser.py +4 -3
app/statement_parser.py
CHANGED
|
@@ -102,7 +102,7 @@ def _split_glued_city(s: str) -> str:
|
|
| 102 |
|
| 103 |
|
| 104 |
def _clean_merchant(raw: str) -> str:
|
| 105 |
-
original = (raw or "").strip()
|
| 106 |
strip = lambda x: re.sub(r"^[\s*.,\-]+|[\s*.,\-]+$", "", x).strip()
|
| 107 |
if re.search(r"paypal", original, re.IGNORECASE):
|
| 108 |
m = re.search(r"paypal\s*\*?\s*([a-z][a-z0-9 &._-]{1,24})", original, re.IGNORECASE)
|
|
@@ -119,8 +119,8 @@ def _clean_merchant(raw: str) -> str:
|
|
| 119 |
s = re.sub(r"\*+", " ", s)
|
| 120 |
s = strip(re.sub(r"\s{2,}", " ", s))
|
| 121 |
if len(s) < 2:
|
| 122 |
-
return original[:40] or "Transaction"
|
| 123 |
-
return _title(_split_glued_city(s))[:40]
|
| 124 |
|
| 125 |
|
| 126 |
def _parse_date(s: str) -> Optional[str]:
|
|
@@ -213,6 +213,7 @@ def parse_csv(content: bytes) -> List[Dict]:
|
|
| 213 |
skipped so only real spends feed the optimal-card analysis.
|
| 214 |
"""
|
| 215 |
text = content.decode("utf-8-sig", errors="ignore")
|
|
|
|
| 216 |
delim = _detect_delimiter(text)
|
| 217 |
lines = [ln for ln in text.splitlines() if ln.strip()]
|
| 218 |
rows = [[c.strip() for c in _split_line(ln, delim)] for ln in lines]
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
def _clean_merchant(raw: str) -> str:
|
| 105 |
+
original = re.sub(r"[\x00-\x1f\x7f]+", " ", (raw or "")).strip() # never show control chars in a name
|
| 106 |
strip = lambda x: re.sub(r"^[\s*.,\-]+|[\s*.,\-]+$", "", x).strip()
|
| 107 |
if re.search(r"paypal", original, re.IGNORECASE):
|
| 108 |
m = re.search(r"paypal\s*\*?\s*([a-z][a-z0-9 &._-]{1,24})", original, re.IGNORECASE)
|
|
|
|
| 119 |
s = re.sub(r"\*+", " ", s)
|
| 120 |
s = strip(re.sub(r"\s{2,}", " ", s))
|
| 121 |
if len(s) < 2:
|
| 122 |
+
return original[:40].strip() or "Transaction"
|
| 123 |
+
return _title(_split_glued_city(s))[:40].strip() # trim AFTER truncation so a cut long name has no trailing space
|
| 124 |
|
| 125 |
|
| 126 |
def _parse_date(s: str) -> Optional[str]:
|
|
|
|
| 213 |
skipped so only real spends feed the optimal-card analysis.
|
| 214 |
"""
|
| 215 |
text = content.decode("utf-8-sig", errors="ignore")
|
| 216 |
+
text = text.replace("\x00", "") # NUL bytes (corrupt/binary upload) make csv.reader raise
|
| 217 |
delim = _detect_delimiter(text)
|
| 218 |
lines = [ln for ln in text.splitlines() if ln.strip()]
|
| 219 |
rows = [[c.strip() for c in _split_line(ln, delim)] for ln in lines]
|