diff --git a/Lib/csv.py b/Lib/csv.py index 6cae34c705777d..044f785ba07798 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -330,8 +330,14 @@ def _guess_quote_and_delimiter(self, data, delimiters): # if we see an extra quote between delimiters, we've got a # double quoted format + # + # The first character class must exclude the quote character. If it + # can cross a quote, the engine has to try every way of splitting a + # run of quotes between the two classes, which is pathological on + # quote-heavy input (gh-109638). The second class must keep admitting + # the quote, since that is what lets `"a""b"` match. dq_regexp = re.compile( - r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \ + r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s%(quote)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \ {'delim':re.escape(delim), 'quote':quotechar}, re.MULTILINE) diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 2ab529b51c207d..d98853d968d0e6 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1501,6 +1501,12 @@ def test_delimiters(self): self.assertEqual(dialect.delimiter, ',') self.assertEqual(dialect.quotechar, '"') + def test_sniff_regex_backtracking(self): + # gh-109638: this artificial sample used to take about ten seconds. + sniffer = csv.Sniffer() + sample = '"",' * 60 + '"' * 60 + '0' + '"' * 60 + '0' + self.assertEqual(sniffer.sniff(sample).delimiter, ',') + def test_doublequote(self): sniffer = csv.Sniffer() dialect = sniffer.sniff(self.header1) diff --git a/Misc/NEWS.d/next/Library/2026-07-29-12-33-49.gh-issue-109638.Rq7bXm.rst b/Misc/NEWS.d/next/Library/2026-07-29-12-33-49.gh-issue-109638.Rq7bXm.rst new file mode 100644 index 00000000000000..a03e1180586367 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-12-33-49.gh-issue-109638.Rq7bXm.rst @@ -0,0 +1,4 @@ +Fix excessive regular expression backtracking in :class:`csv.Sniffer` on +quote-heavy samples. Detecting a doubled-quote dialect could try every way of +splitting a run of quotes between two character classes, making +:meth:`~csv.Sniffer.sniff` take seconds on samples of a few hundred characters.