Прочитайте файл Unicode в Python, который объявляет свою кодировку так же, как исходный код Python - PullRequest
9 голосов
/ 21 мая 2011

Я хочу написать программу на 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 выполняется обработка исходного кода-кодирования?

Ответы [ 5 ]

7 голосов
/ 21 мая 2011

Вы должны иметь возможность свернуть свой собственный декодер в 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']
3 голосов
/ 21 мая 2011

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

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

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

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

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

2 голосов
/ 21 мая 2011

Из указанного PEP (0268) :

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

  1. чтение файла

  2. декодирование его в Unicode при условии фиксированной кодировки для каждого файла

  3. преобразование его в байтовую строку UTF-8

  4. токенизировать содержимое UTF-8

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

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

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

1 голос
/ 11 апреля 2018

Существует поддержка этого в стандартной библиотеке, даже в Python 2. Вот код, который вы можете использовать:

try:

    # Python 3
    from tokenize import open as open_with_encoding_check

except ImportError:

    # Python 2
    from lib2to3.pgen2.tokenize import detect_encoding
    import io


    def open_with_encoding_check(filename):
        """Open a file in read only mode using the encoding detected by
        detect_encoding().
        """
        fp = io.open(filename, 'rb')
        try:
            encoding, lines = detect_encoding(fp.readline)
            fp.seek(0)
            text = io.TextIOWrapper(fp, encoding, line_buffering=True)
            text.mode = 'r'
            return text
        except:
            fp.close()
            raise

Тогда лично мне нужно было разобрать и скомпилировать этот источник. В Python 2 это ошибка при компиляции текста в Юникоде, который включает в себя объявление кодировки, поэтому строки, содержащие объявление, должны быть сначала пустыми (не удаляться, так как это изменяет номера строк). Так я и сделал эту функцию:

def read_source_file(filename):
    from lib2to3.pgen2.tokenize import cookie_re

    with open_with_encoding_check(filename) as f:
        return ''.join([
            '\n' if i < 2 and cookie_re.match(line)
            else line
            for i, line in enumerate(f)
        ])

Я использую их в своем пакете, последний источник (на случай, если мне понадобится их изменить) можно найти здесь , тогда как тесты здесь .

1 голос
/ 17 октября 2015

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...