diff --git a/Lib/csv.py b/Lib/csv.py index 6cae34c705777d..96845831f17e5e 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -280,12 +280,16 @@ def _guess_quote_and_delimiter(self, data, delimiters): """ import re + # The body of a quoted field ends at the first quote which is + # not doubled, as it does for a reader. A lazy ".*?" scans to + # the end of the sample instead, from every start: quadratically. + body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+' matches = [] - for restr in (r'(?P[^\w\n"\'])(?P ?)(?P["\']).*?(?P=quote)(?P=delim)', # ,".*?", - r'(?:^|\n)(?P["\']).*?(?P=quote)(?P[^\w\n"\'])(?P ?)', # ".*?", - r'(?P[^\w\n"\'])(?P ?)(?P["\']).*?(?P=quote)(?:$|\r|\n)', # ,".*?" - r'(?:^|\n)(?P["\']).*?(?P=quote)(?:$|\r|\n)'): # ".*?" (no delim, no space) - regexp = re.compile(restr, re.DOTALL | re.MULTILINE) + for restr in (r'(?P[^\w\n"\'])(?P ?)(?P["\'])%s(?P=quote)(?P=delim)', # ,"...", + r'(?:^|\n)(?P["\'])%s(?P=quote)(?P[^\w\n"\'])(?P ?)', # "...", + r'(?P[^\w\n"\'])(?P ?)(?P["\'])%s(?P=quote)(?:$|\r|\n)', # ,"..." + r'(?:^|\n)(?P["\'])%s(?P=quote)(?:$|\r|\n)'): # "..." (no delim, no space) + regexp = re.compile(restr % body, re.DOTALL | re.MULTILINE) matches = regexp.findall(data) if matches: break diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 2ab529b51c207d..03bcf9c9e0ea15 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1564,6 +1564,14 @@ def test_zero_mode_tie_order_colon_first(self): sniffer.sniff(sample) + def test_sniff_quoted_single_column(self): + # gh-98820: this sample used to take minutes. + sniffer = csv.Sniffer() + sample = '"abcdefghijklmnopqrstuvwxyz"\n' * 10000 + with self.assertRaisesRegex(csv.Error, "Could not determine delimiter"): + sniffer.sniff(sample, delimiters=',:|\t') + + class NUL: def write(s, *args): pass diff --git a/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst new file mode 100644 index 00000000000000..aa9ae8d937004f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst @@ -0,0 +1,2 @@ +Fix quadratic time in :meth:`csv.Sniffer.sniff` for a sample which contains +quoted fields, in particular for a single column of quoted fields.