Spaces:
Sleeping
Sleeping
Updated app.py with EOF Error Handling logic for ghost characters
Browse files
app.py
CHANGED
|
@@ -243,13 +243,56 @@ def detect_delimiter(path, candidates=(",", "|", "^", "\t", ";", "\x1f", "\x1e")
|
|
| 243 |
return best
|
| 244 |
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
def _read_csv_auto(path, **kwargs):
|
| 247 |
"""pd.read_csv with the delimiter auto-detected (unless caller passed sep),
|
| 248 |
tolerating malformed rows (a stray delimiter in one row won't abort the read)."""
|
| 249 |
if "sep" not in kwargs:
|
| 250 |
kwargs["sep"] = detect_delimiter(path)
|
| 251 |
kwargs.setdefault("on_bad_lines", "skip")
|
| 252 |
-
|
|
|
|
| 253 |
|
| 254 |
|
| 255 |
def scan_bad_rows(path, sep, out_dir):
|
|
@@ -262,12 +305,12 @@ def scan_bad_rows(path, sep, out_dir):
|
|
| 262 |
bw = None
|
| 263 |
try:
|
| 264 |
with open(path, "r", encoding="utf-8", errors="replace", newline="") as fin:
|
| 265 |
-
header = fin.readline()
|
| 266 |
if not header:
|
| 267 |
return 0, None
|
| 268 |
base = header.rstrip("\r\n").count(sep) # header delimiter count
|
| 269 |
for lineno, line in enumerate(fin, start=2):
|
| 270 |
-
s = line.rstrip("\r\n")
|
| 271 |
if not s:
|
| 272 |
continue
|
| 273 |
if s.count(sep) > base: # too many fields -> skipped by pandas
|
|
@@ -850,8 +893,9 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
|
|
| 850 |
try:
|
| 851 |
if str(name).lower().endswith(".csv"):
|
| 852 |
sep = detect_delimiter(name)
|
| 853 |
-
|
| 854 |
-
|
|
|
|
| 855 |
_step(log, f"Loaded {Path(name).name} ({len(dfs[-1]):,} rows, "
|
| 856 |
f"delimiter {sep!r})")
|
| 857 |
else:
|
|
@@ -1501,8 +1545,9 @@ def _split_by_county(uploaded_files, out_dir, log, chunksize=STREAM_CHUNK_ROWS):
|
|
| 1501 |
# detect the FIPS-county column from a small sample of the first file
|
| 1502 |
first = uploaded_files[0].name
|
| 1503 |
if str(first).lower().endswith(".csv"):
|
| 1504 |
-
|
| 1505 |
-
|
|
|
|
| 1506 |
else:
|
| 1507 |
sample = pd.read_excel(first)
|
| 1508 |
fips_col, reason = detect_fips_county_column(sample)
|
|
@@ -1536,11 +1581,12 @@ def _split_by_county(uploaded_files, out_dir, log, chunksize=STREAM_CHUNK_ROWS):
|
|
| 1536 |
if str(nm).lower().endswith(".csv"):
|
| 1537 |
sep = detect_delimiter(nm)
|
| 1538 |
_step(log, f"Reading {Path(nm).name} (delimiter {sep!r})")
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
|
| 1542 |
-
|
| 1543 |
-
|
|
|
|
| 1544 |
else:
|
| 1545 |
chunk = pd.read_excel(nm)
|
| 1546 |
route(chunk)
|
|
|
|
| 243 |
return best
|
| 244 |
|
| 245 |
|
| 246 |
+
_CTRL_DELETE = {c: None for c in range(32)}
|
| 247 |
+
for _keep in (9, 10, 13): # keep tab, newline, carriage return
|
| 248 |
+
_CTRL_DELETE.pop(_keep, None)
|
| 249 |
+
_CTRL_DELETE[127] = None # DEL
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def _strip_ctrl(s):
|
| 253 |
+
"""Remove C0 control bytes (NUL, SUB/'EOF', and the rest) except tab/LF/CR.
|
| 254 |
+
A stray control byte embedded in a text field (e.g. an address or legal
|
| 255 |
+
description from the source system) otherwise either aborts pandas' C parser
|
| 256 |
+
or silently truncates the field at a fake end-of-file."""
|
| 257 |
+
return s.translate(_CTRL_DELETE)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class _CleanReader:
|
| 261 |
+
"""Text file-like wrapper that strips control bytes as the file is read, so a
|
| 262 |
+
stray control character in one field can't break the parse. Works with pandas'
|
| 263 |
+
chunked and whole-file reads alike; opened with errors='replace' so stray
|
| 264 |
+
non-UTF-8 bytes don't abort the read either."""
|
| 265 |
+
def __init__(self, path, encoding="utf-8", errors="replace"):
|
| 266 |
+
self._f = open(path, "r", encoding=encoding, errors=errors, newline="")
|
| 267 |
+
|
| 268 |
+
def read(self, size=-1):
|
| 269 |
+
return _strip_ctrl(self._f.read(size))
|
| 270 |
+
|
| 271 |
+
def readline(self, size=-1):
|
| 272 |
+
return _strip_ctrl(self._f.readline(size))
|
| 273 |
+
|
| 274 |
+
def __iter__(self):
|
| 275 |
+
for line in self._f:
|
| 276 |
+
yield _strip_ctrl(line)
|
| 277 |
+
|
| 278 |
+
def close(self):
|
| 279 |
+
self._f.close()
|
| 280 |
+
|
| 281 |
+
def __enter__(self):
|
| 282 |
+
return self
|
| 283 |
+
|
| 284 |
+
def __exit__(self, *exc):
|
| 285 |
+
self.close()
|
| 286 |
+
|
| 287 |
+
|
| 288 |
def _read_csv_auto(path, **kwargs):
|
| 289 |
"""pd.read_csv with the delimiter auto-detected (unless caller passed sep),
|
| 290 |
tolerating malformed rows (a stray delimiter in one row won't abort the read)."""
|
| 291 |
if "sep" not in kwargs:
|
| 292 |
kwargs["sep"] = detect_delimiter(path)
|
| 293 |
kwargs.setdefault("on_bad_lines", "skip")
|
| 294 |
+
with _CleanReader(path) as fh:
|
| 295 |
+
return pd.read_csv(fh, **kwargs)
|
| 296 |
|
| 297 |
|
| 298 |
def scan_bad_rows(path, sep, out_dir):
|
|
|
|
| 305 |
bw = None
|
| 306 |
try:
|
| 307 |
with open(path, "r", encoding="utf-8", errors="replace", newline="") as fin:
|
| 308 |
+
header = _strip_ctrl(fin.readline())
|
| 309 |
if not header:
|
| 310 |
return 0, None
|
| 311 |
base = header.rstrip("\r\n").count(sep) # header delimiter count
|
| 312 |
for lineno, line in enumerate(fin, start=2):
|
| 313 |
+
s = _strip_ctrl(line).rstrip("\r\n")
|
| 314 |
if not s:
|
| 315 |
continue
|
| 316 |
if s.count(sep) > base: # too many fields -> skipped by pandas
|
|
|
|
| 893 |
try:
|
| 894 |
if str(name).lower().endswith(".csv"):
|
| 895 |
sep = detect_delimiter(name)
|
| 896 |
+
with _CleanReader(name) as _fh:
|
| 897 |
+
dfs.append(pd.read_csv(_fh, low_memory=False, sep=sep,
|
| 898 |
+
on_bad_lines="skip"))
|
| 899 |
_step(log, f"Loaded {Path(name).name} ({len(dfs[-1]):,} rows, "
|
| 900 |
f"delimiter {sep!r})")
|
| 901 |
else:
|
|
|
|
| 1545 |
# detect the FIPS-county column from a small sample of the first file
|
| 1546 |
first = uploaded_files[0].name
|
| 1547 |
if str(first).lower().endswith(".csv"):
|
| 1548 |
+
with _CleanReader(first) as _fh:
|
| 1549 |
+
sample = pd.read_csv(_fh, nrows=50_000, low_memory=False,
|
| 1550 |
+
sep=detect_delimiter(first), on_bad_lines="skip")
|
| 1551 |
else:
|
| 1552 |
sample = pd.read_excel(first)
|
| 1553 |
fips_col, reason = detect_fips_county_column(sample)
|
|
|
|
| 1581 |
if str(nm).lower().endswith(".csv"):
|
| 1582 |
sep = detect_delimiter(nm)
|
| 1583 |
_step(log, f"Reading {Path(nm).name} (delimiter {sep!r})")
|
| 1584 |
+
with _CleanReader(nm) as _fh:
|
| 1585 |
+
for chunk in pd.read_csv(_fh, chunksize=chunksize, low_memory=False,
|
| 1586 |
+
sep=sep, on_bad_lines="skip"):
|
| 1587 |
+
route(chunk)
|
| 1588 |
+
total += len(chunk)
|
| 1589 |
+
_step(log, f"Split: routed {total:,} rows into {len(county_paths)} counties")
|
| 1590 |
else:
|
| 1591 |
chunk = pd.read_excel(nm)
|
| 1592 |
route(chunk)
|