105 lines
2.8 KiB
Python
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)
|