Files

105 lines
2.8 KiB
Python

"""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)