diff --git a/Lib/csv.py b/Lib/csv.py index 6cae34c705777d..fb4baf83ae3f5d 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -328,18 +328,17 @@ def _guess_quote_and_delimiter(self, data, delimiters): delim = '' skipinitialspace = 0 - # if we see an extra quote between delimiters, we've got a - # double quoted format + # A doubled quote character inside a quoted field means + # a double quoted format. Match whole fields, so that a match + # cannot slide across field boundaries. dq_regexp = re.compile( - r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \ - {'delim':re.escape(delim), 'quote':quotechar}, re.MULTILINE) - - - - if dq_regexp.search(data): - doublequote = True - else: - doublequote = False + r"(?:(?<=%(delim)s)|^) *+%(quote)s" # ," + r"((?:%(quote)s%(quote)s|[^%(quote)s]++)*+)" # the body + r"%(quote)s(?:%(delim)s|$)" # ", + % {'delim': re.escape(delim), 'quote': quotechar}, + re.MULTILINE) + dquotechar = quotechar * 2 + doublequote = any(dquotechar in m[1] for m in dq_regexp.finditer(data)) return (quotechar, doublequote, delim, skipinitialspace) diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 2ab529b51c207d..8bb6bed36b4375 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1564,6 +1564,25 @@ def test_zero_mode_tie_order_colon_first(self): sniffer.sniff(sample) + def test_sniff_regex_backtracking(self): + # gh-109638: this artificial sample used to take minutes. + sniffer = csv.Sniffer() + sample = '"",' * 100 + '"' * 100 + '0' + '"' * 100 + '0' + self.assertEqual(sniffer.sniff(sample).delimiter, ',') + + def test_sniff_doublequote_across_fields(self): + # A quoted field which contains the delimiter, followed by + # an empty quoted field, is not a doubled quote. + sniffer = csv.Sniffer() + sample = '",","",","\n' * 4 + dialect = sniffer.sniff(sample) + self.assertEqual(dialect.delimiter, ',') + self.assertEqual(dialect.quotechar, '"') + self.assertIs(dialect.doublequote, False) + self.assertEqual(next(csv.reader(StringIO(sample), dialect)), + [',', '', ',']) + + class NUL: def write(s, *args): pass diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-25-00.gh-issue-109638.Vt2Rn9.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-25-00.gh-issue-109638.Vt2Rn9.rst new file mode 100644 index 00000000000000..f44163a20c8278 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-11-25-00.gh-issue-109638.Vt2Rn9.rst @@ -0,0 +1,3 @@ +Fix exponential time in :meth:`csv.Sniffer.sniff` for a sample which contains +many quote characters. A doubled quote character is now also detected in +a field which contains the delimiter or a line break.