Прочитайте файл unicode в python, который объявляет его кодировку так же, как источник python

Я хочу написать программу python, которая читает файлы, содержащие текст в формате Unicode. Эти файлы обычно кодируются с помощью UTF-8, но могут и не быть; Если это не так, альтернативная кодировка будет явно объявлена ​​в начале файла. Точнее, он будет объявлен с использованием тех же правил, что и сам Python, чтобы позволить исходному коду Python иметь явно объявленную кодировку (как в PEP 0263, см. https://www.python.org/dev/peps/pep-0263/ для получения дополнительной информации). Чтобы быть ясным, обрабатываемые файлы на самом деле не являются источником python, но они объявляют свои кодировки (если не в UTF-8) с использованием тех же правил.

Если кто-то знает кодировку файла перед его открытием, Python предоставляет очень простой способ прочитать файл с автоматическим декодированием: команда codecs.open; например, можно было бы сделать:

import codecs
f = codecs.open('unicode.rst', encoding='utf-8')
for line in f:
    print repr(line)

и каждый line, который мы получаем в цикле, будет строкой unicode. Есть ли библиотека Python, которая делает аналогичную вещь, но выбирая кодировку в соответствии с вышеприведенными правилами (которые, я думаю, являются правилами Python 3.0)? (например, создает ли Python "прочитанный файл с самозамечаемой кодировкой", который он использует для чтения источника на язык?) Если нет, то какой самый простой способ добиться желаемого эффекта?

Одна мысль состоит в том, чтобы открыть файл, используя обычный open, прочитать первые две строки, интерпретировать их как UTF-8, искать декларацию кодирования с использованием регулярного выражения в PEP, а если найти одно начальное декодирование всего последующие строки с использованием объявленной кодировки. Для этого обязательно нужно знать, что для всех кодировок, которые Python разрешает в источнике Python, обычный Python readline будет правильно разделять файл на строки, то есть нам нужно знать, что для всех кодировок Python позволяет в источнике Python, строка байтов '\n' всегда действительно означает новую строку и не является частью некоторого многобайтового кодирования последовательности другого символа. (На самом деле мне также нужно беспокоиться о "\ r\n".) Кто-нибудь знает, верно ли это? Документы были не очень конкретными.

Другая мысль - посмотреть в источниках Python. Кто-нибудь знает, где в источнике Python выполняется обработка кодировки исходного кода?

Ответ 1

Вы должны иметь возможность катить собственный декодер в Python. Если вы поддерживаете только 8-битные кодировки, которые являются надмножествами ASCII, код ниже должен работать как есть.

Если вам нужна поддержка 2-байтных кодировок, таких как UTF-16, вам нужно увеличить шаблон в соответствии с \x00c\x00o.. или наоборот, в зависимости от отметки порядка байтов. Сначала создайте несколько тестовых файлов, которые рекламируют свою кодировку:

import codecs, sys
for encoding in ('utf-8', 'cp1252'):
    out = codecs.open('%s.txt' % encoding, 'w', encoding)
    out.write('# coding = %s\n' % encoding)
    out.write(u'\u201chello se\u00f1nor\u201d')
    out.close()

Затем напишите декодер:

import codecs, re

def open_detect(path):
    fin = open(path, 'rb')
    prefix = fin.read(80)
    encs = re.findall('#\s*coding\s*=\s*([\w\d\-]+)\s+', prefix)
    encoding = encs[0] if encs else 'utf-8'
    fin.seek(0)
    return codecs.EncodedFile(fin, 'utf-8', encoding)

for path in ('utf-8.txt','cp1252.txt'):
    fin = open_detect(path)
    print repr(fin.readlines())

Вывод:

['# coding = utf-8\n', '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']
['# coding = cp1252\n', '\xe2\x80\x9chello se\xc3\xb1nor\xe2\x80\x9d']

Ответ 2

Я исследовал источники tokenizer.c (благодаря @Ninefingers, чтобы предложить это в другом ответе и давая ссылку на исходный браузер). Кажется, что точный алгоритм, используемый Python, эквивалентен следующему. В разных местах я опишу алгоритм как чтение байта байтом --- очевидно, что вы хотите что-то делать на практике, но это проще описать. Начальная часть файла обрабатывается следующим образом:

  • При открытии файла попытайтесь распознать спецификацию UTF-8 в начале файла. Если вы видите это, съешьте его и обратите внимание на то, что вы его видели. Не распознавайте знак порядка байтов UTF-16.
  • Прочитайте "строку" текста из файла. "Строка" определяется следующим образом: вы продолжаете считывать байты до тех пор, пока не увидите одну из строк "\n" , "\ r" или "\ r\n" (пытаясь как можно скорее сопоставить строку --- это означает, что если вы видите '\ r', вы должны продуманно прочитать следующий символ, и если это не "\n" , верните его). Терминатор включен в строку, как обычно, на практике Python.
  • Декодировать эту строку с помощью кодека UTF-8. Если вы не видели спецификацию UTF-8, генерируйте сообщение об ошибке, если вы видите любые символы, отличные от ASCII (т.е. Любые символы выше 127). (Python 3.0, конечно, не генерирует здесь ошибку.) Передайте эту декодированную строку пользователю для обработки.
  • Попытка интерпретировать эту строку как комментарий, содержащий объявление кодирования, используя regexp в PEP 0263. Если вы найдете объявление кодирования, пропустите приведенные ниже инструкции для "Я нашел объявление кодирования".
  • ОК, поэтому вы не нашли объявление кодирования. Прочитайте еще одну строку с входа, используя те же правила, что и на шаге 2 выше.
  • Декодируйте его, используя те же правила, что и шаг 3, и передайте его пользователю для обработки.
  • Попытайтесь снова интерпретировать эту строку как комментарий объявления о кодировании, как на шаге 4. Если вы ее найдете, перейдите к приведенным ниже инструкциям для "Я нашел объявление кодирования".
  • OK. Мы проверили первые две строки. Согласно PEP 0263, если бы было объявление кодирования, это было бы на первых двух строках, поэтому мы теперь знаем, что мы его не увидим. Теперь мы читаем остальную часть файла, используя те же инструкции чтения, что и для чтения первых двух строк: мы читаем строки, используя правила на шаге 2, декодируем с помощью правил на шаге 3 (делая ошибку, ASCII, если мы не увидели спецификацию).

Теперь правила для того, что делать, когда " я нашел объявление кодирования":

  • Если ранее мы увидели спецификацию UTF-8, проверьте, что в заявлении для кодирования указано "utf-8" в той или иной форме. Выбросьте ошибку в противном случае. ('' utf-8 'в некотором виде' означает все, что после преобразования в нижний регистр и преобразования подчеркиваний в дефис является либо литеральной строкой 'utf-8', либо чем-то, начинающейся с 'utf-8-'.)
  • Прочитайте остальную часть файла, используя декодер, связанный с данной кодировкой в ​​модуле Python codecs. В частности, разделение остальных байтов в файле на строки - это задание новой кодировки.
  • Одна окончательная морщина: универсальный материал типа новой строки. Правила здесь заключаются в следующем. Если кодировка - это что-либо, кроме "utf-8" в той или иной форме или "латинский-1" в той или иной форме, вообще не делайте универсальной новой строки; просто передайте строки точно так же, как они поступают из декодера в модуле codecs. С другой стороны, если кодировка является "utf-8" в некоторой форме или "latin-1" в той или иной форме, преобразуйте строки, заканчивающиеся на "\ r" или "\ r\n", в строки, заканчивающиеся на "\n" . ('' utf-8 'в некотором виде' означает то же, что и раньше. '' latin-1 'в какой-то форме' означает все, что после преобразования в нижний регистр и преобразования подчеркиваний в дефис является одной из литеральных строк 'latin-1', 'iso-latin-1' или 'iso-8859-1', или любая строка, начинающаяся с одного из 'latin-1-', 'iso-latin-1-' или 'iso-8859-1-'.

Для того, что я делаю, важна верность поведения Python. Мой план состоит в том, чтобы запустить реализацию алгоритма выше в Python и использовать это. Спасибо всем, кто ответил!

Ответ 3

Из PEP (0268):

Компилятор Python tokenizer/компилятор будет необходимо обновить, чтобы работать следующим образом:

  • прочитать файл

  • декодирует его в Unicode, предполагая фиксированную кодировку для каждого файла

  • преобразовать его в строку байтов UTF-8

  • tokenize содержимое UTF-8

  • скомпилировать его, создавая объекты Unicode из данных Unicode       и создание строковых объектов из данных в формате Unicode       путем первого перекодирования данных UTF-8 в 8-битные строковые данные       используя заданное кодирование файла

В самом деле, если вы проверите Parser/tokenizer.c в источнике Python, вы найдете функции get_coding_spec и check_coding_spec, которые отвечают за поиск этой информации в строке, рассматриваемой в decoding_fgets.

Не похоже, что эта возможность подвергается вам в любом случае как API-интерфейс python (по крайней мере эти конкретные функции не являются Py префиксными), поэтому ваши параметры являются сторонней библиотекой и/или повторно назначают эти как расширение. Я лично не знаю каких-либо сторонних библиотек. Я тоже не вижу эту функциональность в стандартной библиотеке.

Ответ 4

Начиная с Python 3.4 есть функция, которая позволяет вам делать то, что вы просите - importlib.util.decode_source

Согласно документации:

importlib.util.decode_source(source_bytes)
Декодируйте заданные байты, представляющие исходный код, и верните его как строку с универсальными символами новой строки (как требуется importlib.abc.InspectLoader.get_source()).

Бретт Кэннон рассказывает об этой функции в своем разговоре От источника к коду: как работает компилятор CPython.