saulsolis09 commited on
Commit
94c25c0
·
1 Parent(s): e8072a2

Added 2 more delimiters to delimiters sniffing code, and logic to skip, count and add to a csv bad rows with more than allowed columns

Browse files
Files changed (1) hide show
  1. app.py +71 -8
app.py CHANGED
@@ -122,11 +122,13 @@ def get_safety_list_path() -> str:
122
  _SAFETY_CACHE: dict | None = None
123
 
124
 
125
- def detect_delimiter(path, candidates=(",", "|", "^", "\t", ";")):
126
  """Sniff a text file's field delimiter from its first lines. SQL exports here
127
- are sometimes '^' or '|' rather than ','. Returns the delimiter char (default
128
- ','). Detects once from a small header sample so the actual (possibly chunked)
129
- read can still use pandas' fast C parser with an explicit sep."""
 
 
130
  try:
131
  with open(path, "r", encoding="utf-8", errors="replace", newline="") as fh:
132
  lines = []
@@ -155,12 +157,62 @@ def detect_delimiter(path, candidates=(",", "|", "^", "\t", ";")):
155
 
156
 
157
  def _read_csv_auto(path, **kwargs):
158
- """pd.read_csv with the delimiter auto-detected (unless caller passed sep)."""
 
159
  if "sep" not in kwargs:
160
  kwargs["sep"] = detect_delimiter(path)
 
161
  return pd.read_csv(path, **kwargs)
162
 
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  def build_whitelist_from_path(path):
165
  """Parse a safety-list file (xlsx or csv) into {pairs, cities, rows, fips_name}."""
166
  if str(path).lower().endswith(".csv"):
@@ -700,6 +752,10 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
700
  except Exception as e:
701
  return "\n".join(log) + f"\n\nCould not parse reference list: {e}", []
702
 
 
 
 
 
703
  # ---- Load & merge uploads (Excel or CSV) -------------------------
704
  dfs = []
705
  for f in uploaded_files:
@@ -707,7 +763,8 @@ def run_pipeline(uploaded_files, reference_file, blank_cities_not_in_list,
707
  try:
708
  if str(name).lower().endswith(".csv"):
709
  sep = detect_delimiter(name)
710
- dfs.append(pd.read_csv(name, low_memory=False, sep=sep))
 
711
  _step(log, f"Loaded {Path(name).name} ({len(dfs[-1]):,} rows, "
712
  f"delimiter {sep!r})")
713
  else:
@@ -1293,7 +1350,8 @@ def _split_by_county(uploaded_files, out_dir, log, chunksize=STREAM_CHUNK_ROWS):
1293
  # detect the FIPS-county column from a small sample of the first file
1294
  first = uploaded_files[0].name
1295
  if str(first).lower().endswith(".csv"):
1296
- sample = pd.read_csv(first, nrows=50_000, low_memory=False, sep=detect_delimiter(first))
 
1297
  else:
1298
  sample = pd.read_excel(first)
1299
  fips_col, reason = detect_fips_county_column(sample)
@@ -1327,7 +1385,8 @@ def _split_by_county(uploaded_files, out_dir, log, chunksize=STREAM_CHUNK_ROWS):
1327
  if str(nm).lower().endswith(".csv"):
1328
  sep = detect_delimiter(nm)
1329
  _step(log, f"Reading {Path(nm).name} (delimiter {sep!r})")
1330
- for chunk in pd.read_csv(nm, chunksize=chunksize, low_memory=False, sep=sep):
 
1331
  route(chunk)
1332
  total += len(chunk)
1333
  _step(log, f"Split: routed {total:,} rows into {len(county_paths)} counties")
@@ -1360,6 +1419,9 @@ def run_streaming(uploaded_files, reference_file, blank_cities_not_in_list,
1360
  except Exception as e:
1361
  return "\n".join(log) + f"\n\nCould not parse reference list: {e}", []
1362
 
 
 
 
1363
  _step(log, "Splitting input by county (streaming)...")
1364
  fips_col, reason, counties, sample_cols = _split_by_county(uploaded_files, out_dir, log)
1365
  if fips_col is None:
@@ -1418,6 +1480,7 @@ def run_streaming(uploaded_files, reference_file, blank_cities_not_in_list,
1418
  outputs.append(str(p))
1419
  output_parts = out_roll.finalize()
1420
  outputs.extend(output_parts)
 
1421
 
1422
  summary_cols = ["County", "FIPS", "Total Records in County", "Total Subdivisions",
1423
  "Total Clusters Made", "Total Blank Cities", "Total Cities Assignment",
 
122
  _SAFETY_CACHE: dict | None = None
123
 
124
 
125
+ def detect_delimiter(path, candidates=(",", "|", "^", "\t", ";", "\x1f", "\x1e")):
126
  """Sniff a text file's field delimiter from its first lines. SQL exports here
127
+ are sometimes '^' or '|' rather than ','. Also recognizes the ASCII control
128
+ separators US ('\\x1f') and RS ('\\x1e') -- ideal for messy unquoted data since
129
+ free-text fields never contain them. Returns the delimiter char (default ',').
130
+ Detects once from a small header sample so the actual (possibly chunked) read
131
+ can still use pandas' fast C parser with an explicit sep."""
132
  try:
133
  with open(path, "r", encoding="utf-8", errors="replace", newline="") as fh:
134
  lines = []
 
157
 
158
 
159
  def _read_csv_auto(path, **kwargs):
160
+ """pd.read_csv with the delimiter auto-detected (unless caller passed sep),
161
+ tolerating malformed rows (a stray delimiter in one row won't abort the read)."""
162
  if "sep" not in kwargs:
163
  kwargs["sep"] = detect_delimiter(path)
164
+ kwargs.setdefault("on_bad_lines", "skip")
165
  return pd.read_csv(path, **kwargs)
166
 
167
 
168
+ def scan_bad_rows(path, sep, out_dir):
169
+ """Find rows with MORE fields than the header -- a stray delimiter inside a
170
+ field, which is exactly what pandas skips. Save them verbatim (with their line
171
+ number) to BadRows_<name>.csv so the offending fields can be reviewed and fixed
172
+ at the source. Returns (count, bad_path_or_None)."""
173
+ bad_path = Path(out_dir) / f"BadRows_{Path(path).stem}.csv"
174
+ count = 0
175
+ bw = None
176
+ try:
177
+ with open(path, "r", encoding="utf-8", errors="replace", newline="") as fin:
178
+ header = fin.readline()
179
+ if not header:
180
+ return 0, None
181
+ base = header.rstrip("\r\n").count(sep) # header delimiter count
182
+ for lineno, line in enumerate(fin, start=2):
183
+ s = line.rstrip("\r\n")
184
+ if not s:
185
+ continue
186
+ if s.count(sep) > base: # too many fields -> skipped by pandas
187
+ if bw is None:
188
+ bw = open(bad_path, "w", encoding="utf-8", newline="")
189
+ bw.write("line_number" + sep + header.rstrip("\r\n") + "\n")
190
+ bw.write(str(lineno) + sep + s + "\n")
191
+ count += 1
192
+ finally:
193
+ if bw is not None:
194
+ bw.close()
195
+ return count, (str(bad_path) if count else None)
196
+
197
+
198
+ def scan_inputs_for_bad_rows(uploaded_files, out_dir, log):
199
+ """Scan every CSV input for malformed rows; log the count and save them for
200
+ review. Returns the list of BadRows_*.csv paths produced."""
201
+ bad_files = []
202
+ for f in uploaded_files:
203
+ nm = f.name
204
+ if not str(nm).lower().endswith(".csv"):
205
+ continue
206
+ count, bad_path = scan_bad_rows(nm, detect_delimiter(nm), out_dir)
207
+ if count:
208
+ bad_files.append(bad_path)
209
+ _step(log, f"{Path(nm).name}: {count:,} malformed row(s) skipped "
210
+ f"(saved to {Path(bad_path).name} for review)")
211
+ else:
212
+ _step(log, f"{Path(nm).name}: no malformed rows detected")
213
+ return bad_files
214
+
215
+
216
  def build_whitelist_from_path(path):
217
  """Parse a safety-list file (xlsx or csv) into {pairs, cities, rows, fips_name}."""
218
  if str(path).lower().endswith(".csv"):
 
752
  except Exception as e:
753
  return "\n".join(log) + f"\n\nCould not parse reference list: {e}", []
754
 
755
+ # ---- Scan inputs for malformed rows (save them for review) -------
756
+ bad_files = scan_inputs_for_bad_rows(uploaded_files, out_dir, log)
757
+ outputs.extend(bad_files)
758
+
759
  # ---- Load & merge uploads (Excel or CSV) -------------------------
760
  dfs = []
761
  for f in uploaded_files:
 
763
  try:
764
  if str(name).lower().endswith(".csv"):
765
  sep = detect_delimiter(name)
766
+ dfs.append(pd.read_csv(name, low_memory=False, sep=sep,
767
+ on_bad_lines="skip"))
768
  _step(log, f"Loaded {Path(name).name} ({len(dfs[-1]):,} rows, "
769
  f"delimiter {sep!r})")
770
  else:
 
1350
  # detect the FIPS-county column from a small sample of the first file
1351
  first = uploaded_files[0].name
1352
  if str(first).lower().endswith(".csv"):
1353
+ sample = pd.read_csv(first, nrows=50_000, low_memory=False,
1354
+ sep=detect_delimiter(first), on_bad_lines="skip")
1355
  else:
1356
  sample = pd.read_excel(first)
1357
  fips_col, reason = detect_fips_county_column(sample)
 
1385
  if str(nm).lower().endswith(".csv"):
1386
  sep = detect_delimiter(nm)
1387
  _step(log, f"Reading {Path(nm).name} (delimiter {sep!r})")
1388
+ for chunk in pd.read_csv(nm, chunksize=chunksize, low_memory=False,
1389
+ sep=sep, on_bad_lines="skip"):
1390
  route(chunk)
1391
  total += len(chunk)
1392
  _step(log, f"Split: routed {total:,} rows into {len(county_paths)} counties")
 
1419
  except Exception as e:
1420
  return "\n".join(log) + f"\n\nCould not parse reference list: {e}", []
1421
 
1422
+ _step(log, "Scanning inputs for malformed rows...")
1423
+ bad_files = scan_inputs_for_bad_rows(uploaded_files, out_dir, log)
1424
+
1425
  _step(log, "Splitting input by county (streaming)...")
1426
  fips_col, reason, counties, sample_cols = _split_by_county(uploaded_files, out_dir, log)
1427
  if fips_col is None:
 
1480
  outputs.append(str(p))
1481
  output_parts = out_roll.finalize()
1482
  outputs.extend(output_parts)
1483
+ outputs.extend(bad_files) # malformed-row files for review
1484
 
1485
  summary_cols = ["County", "FIPS", "Total Records in County", "Total Subdivisions",
1486
  "Total Clusters Made", "Total Blank Cities", "Total Cities Assignment",