@@ -361,6 +361,7 @@ def sniff(self, sample, delimiters=None):
361361
362362 best = None
363363 best_score = None
364+ best_field_start_count = None
364365 for combo , rows in sorted (parsed ):
365366 if rows is None :
366367 continue
@@ -379,6 +380,18 @@ def sniff(self, sample, delimiters=None):
379380 if best_score is None or score > best_score :
380381 best_score = score
381382 best = combo [1 :]
383+ best_field_start_count = None
384+ elif (score == best_score and delimiter == best [0 ]
385+ and escapechar == best [2 ] and quotechar != best [1 ]):
386+ # Do not let quote frequency affect delimiter or escape choice.
387+ if best_field_start_count is None :
388+ best_field_start_count = self ._field_start_count (
389+ lines , best [1 ], best [0 ], best [2 ], cut )
390+ field_start_count = self ._field_start_count (
391+ lines , quotechar , delimiter , escapechar , cut )
392+ if field_start_count > best_field_start_count :
393+ best = combo [1 :]
394+ best_field_start_count = field_start_count
382395
383396 if best is None :
384397 raise Error ("Could not determine delimiter" )
@@ -401,6 +414,109 @@ class dialect(Dialect):
401414
402415 return dialect
403416
417+ def _field_start_count (self , lines , quotechar , delimiter , escapechar ,
418+ cut ):
419+ """
420+ The number of fields in the sample starting with *quotechar*:
421+ at the start of a field. Used for breaking ties between quote
422+ characters.
423+ """
424+ if not quotechar :
425+ return 0
426+
427+ class LineIterator :
428+ def __init__ (self , lines ):
429+ self .lines = iter (lines )
430+ self .position = 0
431+
432+ def __iter__ (self ):
433+ return self
434+
435+ def __next__ (self ):
436+ line = next (self .lines )
437+ self .position += 1
438+ return line
439+
440+ source = LineIterator (lines )
441+ record_counts = []
442+ start = 0
443+ # Keep the scan boundaries identical to the candidate reader's.
444+ reader = self ._make_reader (source , delimiter , quotechar , escapechar )
445+ parse_error = False
446+ try :
447+ while True :
448+ next (reader )
449+ end = source .position
450+ record = '' .join (lines [start :end ])
451+ record_counts .append (
452+ self ._record_field_start_count (
453+ record , quotechar , delimiter , escapechar ))
454+ start = end
455+ except StopIteration :
456+ pass
457+ except Error :
458+ # Match _try_dialect(): a failing row is not counted.
459+ parse_error = True
460+ if not parse_error and cut and len (record_counts ) > 1 :
461+ record_counts .pop ()
462+ return sum (record_counts )
463+
464+ def _record_field_start_count (self , record , quotechar , delimiter ,
465+ escapechar ):
466+ if not record :
467+ return 0
468+ skipinitialspace = delimiter != ' '
469+ count = 0
470+ field_start = True
471+ skip_spaces = skipinitialspace
472+ active_quote = None
473+ pos = 0
474+ # The candidate reader fixes record boundaries; recognize either
475+ # quote mark within a record so embedded delimiters do not vote.
476+ while pos < len (record ):
477+ char = record [pos ]
478+ if active_quote is not None :
479+ if escapechar and char == escapechar :
480+ pos += 2
481+ continue
482+ if char == active_quote :
483+ if pos + 1 < len (record ) and record [pos + 1 ] == char :
484+ pos += 2
485+ continue
486+ active_quote = None
487+ pos += 1
488+ continue
489+
490+ if escapechar and char == escapechar :
491+ field_start = False
492+ pos += 2
493+ continue
494+ if field_start :
495+ if skip_spaces and char == ' ' :
496+ pos += 1
497+ continue
498+ if char in "\" '" :
499+ if char == quotechar :
500+ count += 1
501+ active_quote = char
502+ field_start = False
503+ pos += 1
504+ continue
505+ if char in '\r \n ' :
506+ break
507+ if char == delimiter :
508+ skip_spaces = skipinitialspace
509+ pos += 1
510+ continue
511+ field_start = False
512+ elif char == delimiter :
513+ field_start = True
514+ skip_spaces = skipinitialspace
515+ elif char in '\r \n ' :
516+ break
517+ pos += 1
518+ return count
519+
404520 def _parses_as_single_column (self , sample ):
405521 """
406522 True if the whole sample parses as a single column of quoted
0 commit comments