Чтение CSV-файла с некоторыми строками, имеющими неправильную кодировку, и возвращение неправильных строк пользователю - PullRequest
2 голосов
/ 17 октября 2019

У меня есть пользователи, загружающие CSV-файлы для загрузки. В Python 2 я смог открыть файл в двоичном формате, передать его в unicodecsv.DictReader, и если в некоторых строках возникла проблема с кодировкой, например, недопустимый символ Unicode, потому что клиент использовал CP1251 или что-то еще, я мог записать эти строки ивернуть точно, какие строки имели проблему.

С py3.7 не похоже, что я могу это сделать - модуль csv требует, чтобы файл был декодирован, и если я вместо этого передам егогенератор, как (line.decode('utf8') for line in my_binary_file), я не могу заставить его генерировать исключение только для плохих строк и продолжать идти после. Я попытался использовать unicodecsv, хотя он не видел коммит более четырех лет и технически не поддерживает py> 3.5, и, похоже, он тоже не работает - итератор просто останавливается после неверной строки.

Я вижу два пути решения этой проблемы, ни один из которых не является привлекательным:
1) предварительно декодируйте файл построчно и находите плохие строки, что расточительно, или
2) пишите мой собственныйCSV-парсер, который позволяет пропускать плохие строки, что похоже на возникновение проблемы.

Могу ли я сделать это по-другому?

Для справки, вот пример кода, который работал в py2:

def unicode_safe_iterator(reader):
    while True:
        try:
            yield True, next(reader)
        except UnicodeDecodeError as exc:
            yield False, 'UnicodeDecodeError: %s' % str(exc)
        # uncomment for py3:
        # except StopIteration:
        #     return

def get_data_iter_from_csv(csv_file, ...):
    reader = unicodecsv.DictReader(csv_file)
    error_messages = []
    line_num = 1
    for valid, row in unicode_safe_iterator(reader):
        line_num += 1
        if not valid:
            error_messages.append(dict(line_number=line_num, error=row))
        else:
            row_data = validate_row_data(row)  # check for errors other than encoding, etc.
        if not error_messages:
            # stop yielding in case of errors, but keep iterating to find all errors.
            yield row_data
    if error_messages:
        raise ValidationError(Errors.CSV_FILE_ERRORS, error_items=error_messages)


data_iter = get_data_iter_from_csv(open(path_to_csv, 'rb'), ...)

1 Ответ

2 голосов
/ 18 октября 2019

Вот обходной путь. Мы читаем файл как поток байтов, разбиваем его на новые строки и пытаемся преобразовать строки в строки utf8. Если это не удалось, попробуйте преобразовать неподходящие детали в строку cp1251. Затем вы можете использовать io.StringIO для имитации открытия файла.

import csv, io

def convert(bl):

    rslt=[]
    done=False
    pos=0
    while not done:
        try:
            s=bl[pos:].decode("utf8")
            rslt.append(s)
            done=True
        except UnicodeDecodeError as ev:
            abs_start, abs_end= pos+ev.start, pos+ev.end
            rslt.append(bl[pos:abs_start].decode("utf8"))
            rslt.append(bl[abs_start:abs_end].decode("cp1251",errors="replace"))
            pos= abs_end
            if pos>= len(bl):
                done=True

    return "".join(rslt)


with open(path_to_csv,"rb") as ff:

    data= ff.read().split(b'\x0a')
    text= [ convert(line)  for line in data ]

text="\n".join(text)
print(text)

rdr= csv.DictReader(io.StringIO(text))

Это можно сделать сразу, а не построчно:

with open(path_to_csv,"rb") as ff:  
    text= convert( ff.read() )

rdr= csv.DictReader(io.StringIO(text))
...