Skip to content

Commit ddb0488

Browse files
Detect a doubled quote at a record boundary
Normalize the record separators in the sample, so that ^ and $ match at every record boundary: a doubled quote in the last field of a CRLF record was not detected, and a match could cross a CR separated record. Skip the detection if there is no delimiter, because an empty one matches at every position and makes the search quadratic.
1 parent e78bb80 commit ddb0488

2 files changed

Lines changed: 30 additions & 8 deletions

File tree

Lib/csv.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ def sniff(self, sample, delimiters=None):
243243
Returns a dialect (or None) corresponding to the sample
244244
"""
245245

246+
sample = sample.replace('\r\n', '\n').replace('\r', '\n')
247+
246248
quotechar, doublequote, delimiter, skipinitialspace = \
247249
self._guess_quote_and_delimiter(sample, delimiters)
248250
if not delimiter:
@@ -331,14 +333,17 @@ def _guess_quote_and_delimiter(self, data, delimiters):
331333
# A doubled quote character inside a quoted field means
332334
# a double quoted format. Match whole fields, so that a match
333335
# cannot slide across field boundaries.
334-
dq_regexp = re.compile(
335-
r"(?:(?<=%(delim)s)|^) *+%(quote)s" # ,"
336-
r"((?:%(quote)s%(quote)s|[^%(quote)s]++)*+)" # the body
337-
r"%(quote)s(?:%(delim)s|$)" # ",
338-
% {'delim': re.escape(delim), 'quote': quotechar},
339-
re.MULTILINE)
340-
dquotechar = quotechar * 2
341-
doublequote = any(dquotechar in m[1] for m in dq_regexp.finditer(data))
336+
doublequote = False
337+
if delim:
338+
dq_regexp = re.compile(
339+
r"(?:(?<=%(delim)s)|^) *+%(quote)s" # ,"
340+
r"((?:%(quote)s%(quote)s|[^%(quote)s]++)*+)" # the body
341+
r"%(quote)s(?:%(delim)s|$)" # ",
342+
% {'delim': re.escape(delim), 'quote': quotechar},
343+
re.MULTILINE)
344+
dquotechar = quotechar * 2
345+
doublequote = any(dquotechar in m[1]
346+
for m in dq_regexp.finditer(data))
342347

343348
return (quotechar, doublequote, delim, skipinitialspace)
344349

Lib/test/test_csv.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,23 @@ def test_sniff_doublequote_across_fields(self):
15821582
self.assertEqual(next(csv.reader(StringIO(sample), dialect)),
15831583
[',', '', ','])
15841584

1585+
def test_sniff_doublequote_record_separators(self):
1586+
# The record separator ends a field as a delimiter does.
1587+
sniffer = csv.Sniffer()
1588+
for sep in '\n', '\r\n', '\r':
1589+
with self.subTest(sep=sep):
1590+
sample = ('x,"a""b"' + sep + 'y,"c"' + sep) * 2
1591+
self.assertIs(sniffer.sniff(sample).doublequote, True)
1592+
sample = ('"",","' + sep) * 4
1593+
self.assertIs(sniffer.sniff(sample).doublequote, False)
1594+
1595+
def test_sniff_single_column(self):
1596+
# This sample used to be quadratic.
1597+
sniffer = csv.Sniffer()
1598+
sample = '"a"\n' + ' ' * 100000
1599+
with self.assertRaisesRegex(csv.Error, "Could not determine delimiter"):
1600+
sniffer.sniff(sample, delimiters=',;')
1601+
15851602

15861603
class NUL:
15871604
def write(s, *args):

0 commit comments

Comments
 (0)