@@ -243,8 +243,14 @@ def __init__(self):
243243 def sniff (self , sample , delimiters = None ):
244244 """
245245 Returns a dialect (or None) corresponding to the sample
246+
247+ If several delimiters fit the sample equally well, the
248+ delimiters listed in the preferred attribute are preferred, in
249+ that order, no matter how many times each of them occurs.
246250 """
247251
252+ sample = sample .replace ('\r \n ' , '\n ' ).replace ('\r ' , '\n ' )
253+
248254 quotechar , doublequote , delimiter , skipinitialspace = \
249255 self ._guess_quote_and_delimiter (sample , delimiters )
250256 if not delimiter :
@@ -282,12 +288,16 @@ def _guess_quote_and_delimiter(self, data, delimiters):
282288 """
283289 import re
284290
291+ # The body of a quoted field ends at the first quote which is
292+ # not doubled, as it does for a reader. A lazy ".*?" scans to
293+ # the end of the sample instead, from every start: quadratically.
294+ body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+'
285295 matches = []
286- for restr in (r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*? (?P=quote)(?P=delim)' , # ,".*? ",
287- r'(?:^|\n)(?P<quote>["\']).*? (?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)' , # ".*? ",
288- r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*? (?P=quote)(?:$|\n)' , # ,".*? "
289- r'(?:^|\n)(?P<quote>["\']).*? (?P=quote)(?:$|\n)' ): # ".*? " (no delim, no space)
290- regexp = re .compile (restr , re .DOTALL | re .MULTILINE )
296+ for restr in (r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\'])%s (?P=quote)(?P=delim)' , # ,"... ",
297+ r'(?:^|\n)(?P<quote>["\'])%s (?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)' , # "... ",
298+ r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\'])%s (?P=quote)(?:$|\n)' , # ,"... "
299+ r'(?:^|\n)(?P<quote>["\'])%s (?P=quote)(?:$|\n)' ): # "... " (no delim, no space)
300+ regexp = re .compile (restr % body , re .DOTALL | re .MULTILINE )
291301 matches = regexp .findall (data )
292302 if matches :
293303 break
@@ -330,18 +340,22 @@ def _guess_quote_and_delimiter(self, data, delimiters):
330340 delim = ''
331341 skipinitialspace = 0
332342
333- # if we see an extra quote between delimiters, we've got a
334- # double quoted format
335- dq_regexp = re .compile (
336- r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \
337- {'delim' :re .escape (delim ), 'quote' :quotechar }, re .MULTILINE )
338-
339-
340-
341- if dq_regexp .search (data ):
342- doublequote = True
343- else :
344- doublequote = False
343+ # A doubled quote character inside a quoted field means
344+ # a double quoted format. Match whole fields, so that a match
345+ # cannot slide across field boundaries.
346+ doublequote = False
347+ if delim :
348+ dq_regexp = re .compile (
349+ r"(?:(?<=%(delim)s)|^)%(space)s%(quote)s" # ,"
350+ r"((?:%(quote)s%(quote)s|[^%(quote)s]++)*+)" # the body
351+ r"%(quote)s(?:%(delim)s|$)" # ",
352+ % {'delim' : re .escape (delim ), 'quote' : quotechar ,
353+ # Skipping spaces after a space rescans them.
354+ 'space' : ' *+' if delim != ' ' else '' },
355+ re .MULTILINE )
356+ dquotechar = quotechar * 2
357+ doublequote = any (dquotechar in m [1 ]
358+ for m in dq_regexp .finditer (data ))
345359
346360 return (quotechar , doublequote , delim , skipinitialspace )
347361
0 commit comments