"""RFC 4180-ish CSV parser + writer with correct handling of quoted fields.""" from __future__ import annotations def parse_csv(text, delimiter=",", quotechar='"'): if text.startswith("\ufeff"): text = text[1:] rows = [] field = [] row = [] i = 0 n = len(text) in_quotes = False at_field_start = True def flush_field(): nonlocal field, row, at_field_start row.append("".join(field)) field = [] at_field_start = True def flush_row(): nonlocal row, at_field_start rows.append(row) row = [] at_field_start = True if text == "": return rows while i < n: c = text[i] if in_quotes: if c == quotechar: if i + 1 < n and text[i + 1] == quotechar: field.append(quotechar) i += 2 else: in_quotes = False i += 1 else: field.append(c) i += 1 else: if c == quotechar and at_field_start: in_quotes = True at_field_start = False i += 1 elif c == delimiter: flush_field() i += 1 elif c == "\r": flush_field() flush_row() if i + 1 < n and text[i + 1] == "\n": i += 2 else: i += 1 at_field_start = True elif c == "\n": flush_field() flush_row() i += 1 at_field_start = True else: field.append(c) at_field_start = False i += 1 if field or row or not at_field_start: if not at_field_start or field: flush_field() if row: flush_row() return rows def _needs_quoting(field, delimiter, quotechar, lineterminator): if quotechar in field or delimiter in field: return True if "\n" in field or "\r" in field: return True if field != field.strip(" "): return True return False def write(rows, delimiter=",", quotechar='"', lineterminator="\r\n"): out = [] for row in rows: fields = [] for f in row: f = "" if f is None else str(f) if _needs_quoting(f, delimiter, quotechar, lineterminator): f = quotechar + f.replace(quotechar, quotechar + quotechar) + quotechar fields.append(f) out.append(delimiter.join(fields)) return lineterminator.join(out) + (lineterminator if rows else "") def dumps(rows, **kw): return write(rows, **kw) def loads(text, **kw): return parse_csv(text, **kw)