From dde954e44bce3ff62b557542c601eb8f8e36701b Mon Sep 17 00:00:00 2001 From: pikammmmm <124257101+pikammmmm@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:36:55 +0200 Subject: [PATCH] gh-109638: Fix excessive backtracking in csv.Sniffer doublequote detection The doublequote-detection pattern used two greedy character classes that both admitted the quote character, so the engine had to try every way of splitting a run of quotes between them. Excluding the quote from the first class forces the quote that follows it to match at the first one available, which removes the ambiguity while leaving detection intact. The second class must keep admitting the quote, since that is what lets `"a""b"` match -- narrowing both classes, or making the first one atomic while it still admits the quote, silently disables the detection. --- Lib/csv.py | 8 +++++++- Lib/test/test_csv.py | 6 ++++++ .../2026-07-29-12-33-49.gh-issue-109638.Rq7bXm.rst | 4 ++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-29-12-33-49.gh-issue-109638.Rq7bXm.rst 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.