[3.15] gh-109638: Fix exponential time in csv.Sniffer for doubled quotes - #154868
[3.15] gh-109638: Fix exponential time in csv.Sniffer for doubled quotes#154868serhiy-storchaka wants to merge 3 commits into
Conversation
The regular expression which looks for a doubled quote character let its runs match the quote character too, so every quote could be taken either as a part of a run or as one of the matched quotes. Exclude the quote character from the runs and match them possessively. The runs no longer exclude the delimiter and the line break either, so a doubled quote is now also found in a field which contains them.
|
Note that the widened detection -- a doubled quote is now found in a field which contains the delimiter or a line break -- changes results on 42 of 560 files in the CSVsniffer corpora. Against that corpus's ground truth the change is positive on both sides: There are two alternatives. #154865 changes no results, but leaves the search polynomial (O(n^5) -> O(n^4), still four and a half minutes on a 2 KB sample). Or, since 3.15 is still in beta, the new engine can be backported from main, which fixes this issue along with seven others; it changes results on 122 of 560 files. |
|
Thanks again for identifying the residual polynomial behavior in my original patch. I reworked and reopened #154865 at a45aca2, using the possessive quote-exclusion approach from this PR as the pre-filter, then tested the current #154868 head ( I found two additional cases not covered by the original reproducer. Empty-delimiter scalingWhen Post-rebase Windows x64 Release medians for: sample = '"a"\n' + ' ' * n
The revised patch uses separate non-empty- and empty-delimiter patterns, so the empty case can only start at Cross-field false positivesAn unanchored match can start at a delimiter inside one quoted field and finish in another. For example: sample = '",","",","'This is three quoted fields containing The revised patch keeps the possessive pre-filter, then uses an anchored linear pattern to rule out a complete well-formed sample without doubled quotes. It also handles CRLF/CR boundaries and literal quote characters in unquoted fields. Generated-input resultsI generated two independent batches of 500,000 semantically labelled samples using seeds
I also exhaustively enumerated 506,996 valid combinations: the revised implementation had 0 false negatives and 0 false positives. On the 548 corpus files available in my local checkout, newline-normalized input produced no decision differences from #154868. With raw newlines there were three differences: two agreed with the annotations and one did not; I am reporting the last one conservatively as an annotated regression. The revised implementation remains effectively equivalent to #154868 on the original gh-109638 family:
There is one measured trade-off: when the first real doubled quote is very late in a 128 KB comma-delimited sample, #154868 took 1.870 ms and the revision took 2.497 ms because it performs the structural validation pass. Both remain linear. The complete implementation rationale, rejected alternatives, corpus accounting, and post-rebase validation are documented in the reopening comment on #154865. I would appreciate your thoughts, particularly on the separate empty-delimiter handling and the anchored no-doublequote validation. |
The pattern required a doubled quote character in the middle, so it could match from a delimiter inside one quoted field to a quote in another one, and reported a double quoted format for '",","",","'. Match whole quoted fields instead and test their bodies for a doubled quote separately. Only look behind the delimiter, because the trailing one is consumed, so that consecutive fields are all matched.
|
Thanks to @Punisheroot for finding in #154865 that the pattern reported a doubled quote for Now whole quoted fields are matched and their bodies are tested separately. No file of 560 in the corpora changes, so the numbers above still hold; nothing in the test suite noticed either, hence the added test. |
Thanks for updating the pattern and for the credit. I tested the current head A doubled quote in the last field of a CRLF record is missed: sample = 'x,"a""b"\r\ny,"c"\r\n'
csv.Sniffer().sniff(sample).doublequote # False, expected TrueConversely, this valid CR-separated CSV contains no doubled quote but reports sample = '"",","\r"",","'
list(csv.reader(io.StringIO(sample, newline="")))
# [['', ','], ['', ',']]This appears to be because The empty-delimiter case also remains approximately quadratic: sample = '"a"\n' + ' ' * nOn Windows x64 Release, the current head grows from 2.30 ms at 4 KB to #154865 handles CR/CRLF and the empty delimiter separately. Would you consider |
The regular expression which detects a doubled quote character lets its runs match the quote character too, so every quote can be taken either as part of a run or as one of the three matched quotes. With three unbounded quantifiers over a region of quotes and delimiters the search is O(n^5) -- 254 s at n=100 in the report.
The runs now exclude the quote character and are matched possessively, which leaves exactly one way to read any input.
The runs also no longer exclude the delimiter and the line break, which the original did for no reason a reader shares -- so a doubled quote is now found in a field containing them as well, e.g.
,"All-Weather Dining Table, Round 48""",. That is the bulk of the behaviour change: 42 of 560 corpus files, and against ground truthdoublequote=Trueimproves from 25.8% to 36.7% anddoublequote=Falsefrom 77.0% to 78.7%, with delimiter and quotechar unchanged.Note #109639 proposed an atomic group on the first run only. It removes the time but also the detection:
doublequotebecomes unconditionallyFalse(0 of 126 annotated files, vs 32 for 3.15).main is not affected: the sniffer was rewritten there in gh-83273.