[3.15] gh-109638: Avoid pathological backtracking in csv.Sniffer (GH-153694) - #154865
[3.15] gh-109638: Avoid pathological backtracking in csv.Sniffer (GH-153694)#154865Punisheroot wants to merge 1 commit into
Conversation
|
Excluding the quote character from the first run lowers the degree, but the second run can still take each quote either as its own content or as one of the matched quotes, so the search stays polynomial. Measured on the reproducer from the issue:
O(n^5) before, O(n^4) after: a 2 KB sample still takes four and a half minutes, and the test added here takes 1.05 s. #154869 proposed the same change and was closed in favour of #154868, reporting the same limitation. Excluding the quote character from both runs and matching them possessively makes it linear, at the cost of changing results on 42 of 560 files in the CSVsniffer corpora, where this PR changes none. 3.15 is still in beta, so there is also a third option: backporting the new engine from main, which fixes this issue along with seven others. |
|
Thanks for the analysis. I implemented the quote-exclusion approach suggested in the July 21 issue comment, but I have now reproduced the residual scaling locally on the current 3.15 branch: n=50 took 0.122 s, n=100 1.598 s, and n=150 7.856 s. |
7ba94ca to
a45aca2
Compare
|
Reopening with a substantially revised implementation at a45aca2. The PR body describes the superseded first implementation; this comment documents the current patch and its validation. Thanks to @serhiy-storchaka for showing that the original change only reduced the pathological search from O(n^5) to O(n^4). The previous implementation and its performance claims have been replaced. Revised designDoublequote detection now has two linear stages. 1. Possessive pre-filterThe first pattern adopts the core approach from #154868:
There are separate patterns for non-empty and empty delimiters. This is necessary because interpolating an empty delimiter into The empty-delimiter pattern instead starts only at 2. Anchored no-doublequote validationAn unanchored local match can begin at a delimiter inside one quoted field and finish in another field. A minimal example is: sample = '",","",","'This is three quoted fields whose values are After a pre-filter match, the revised code therefore checks whether the complete sample is a well-formed sequence of fields containing no doubled quote. If it is, the result is The validator is anchored at Generated-input validationI generated two independent batches of 500,000 semantically labelled samples, using seeds The generator covered comma, semicolon, tab, pipe, space, and the Sniffer's internal empty delimiter; both quote characters; LF, CRLF, and CR; quoted and unquoted fields; embedded delimiters and line breaks; empty fields; leading spaces; doubled quotes; and literal quote characters in unquoted fields. Ground truth came from the semantic values used to construct and escape each field, not from another detector.
The anchored validation is what removes the remaining 742 pre-filter false positives. Additional validation:
The arbitrary-input run is only a robustness check: malformed CSV has no unique semantic ground truth, so it is not included in the accuracy totals. Corpus comparisonI tested the 548 files available in my local CSVsniffer corpus checkout. With newline normalization there were no decision differences from #154868. With raw newlines, three decisions differed: two agreed with the annotations and one did not. I am reporting the latter conservatively as an annotated regression, even though the raw input contains doubled quote characters. PerformanceThe following are post-rebase measurements from a Windows x64 Release build. Each value is a median after warm-up. Original gh-109638 familysample = '"",' * n + '"' * n + '0' + '"' * n + '0'
Both implementations are linear and effectively equivalent on the original reproducer. Empty-delimiter familysample = '"a"\n' + ' ' * n
#154868 is approximately quadratic on this family; the revised implementation is linear. Repeated cross-field false positivesRepeating the valid
The full-sample validation is also linear. Known trade-offWhen the first real doubled quote occurs very late in a 128 KB comma-delimited sample, #154868 measured 1.870 ms and this revision measured 2.497 ms. This is the cost of the additional structural validation; both remain linear. Rejected alternatives
The final two-regexp design avoids those problems while keeping the maintenance-branch scope limited to doubled-quote detection. Post-rebase validation
The new tests cover the gh-109638 reproducer, true and false single-column detection, LF/CRLF/CR boundaries, both quote characters, comma/space/empty delimiters, cross-field false positives, and literal quotes in unquoted fields. This remains a focused 3.15 maintenance-branch fix; it does not backport the complete Sniffer rewrite from |
Summary
Avoid pathological backtracking in the regular expression used by
csv.Snifferto detect doubled quote characters.This is a narrow maintenance-branch replacement for the gh-109638 portion of
GH-153694. The full rewrite cannot be backported because it changes behavior
on valid inputs, while GH-153694 explicitly identifies a targeted regex fix as
the appropriate approach for maintenance branches.
Root cause
The first negated character class in the doublequote pattern excluded the
delimiter and newline, but not the detected quote character. On inputs with
long runs of quotes, the regex engine could repeatedly repartition those
quotes between the character class and the following quote tokens.
The change excludes the quote character from that first character class. This
has the same intent as the lookahead originally proposed in gh-109638, without
introducing another capture group. It deliberately does not use the atomic
group from GH-109639: that version consumed the quote characters needed by the
rest of the pattern and could silently disable doublequote detection.
The regression test is the pathological sample already used by the rewritten
3.16 implementation. Existing positive and negative
doublequotetests remainunchanged and pass.
Performance
Measured with a Windows x64 release build of the current 3.15 branch and the
60-iteration reproducer from gh-109638:
This is approximately a 40.6x speedup. Both versions detected
,as thedelimiter and left
doublequotefalse for this sample.Validation
TestSniffer: 14 tests passedtest_csv, Debug: 135 tests passedtest_csv, Release: 135 tests passed, 4 skippedTools/patchcheck/patchcheck.py: passed and recognized the NEWS entrygit diff --check: passed