РУКОВОДСТВО по работе с Юникодом

Выпускать:

1.12

В этом руководстве рассматривается поддержка Python спецификации Unicode для представления текстовых данных и объясняются различные проблемы, с которыми люди обычно сталкиваются при попытке работать с Unicode.

Введение в Юникод

Определения

Современные программы должны уметь работать с широким спектром символов. Приложения часто интернационализированы для отображения сообщений и вывода данных на различных языках, выбираемых пользователем; одной и той же программе может потребоваться вывод сообщения об ошибке на английском, французском, японском, иврите или русском. Веб-контент может быть написан на любом из этих языков и может также содержать различные символы-эмодзи. Строковый тип Python использует стандарт Unicode для представления символов, что позволяет программам на Python работать со всеми этими различными возможными символами.

Unicode (https://www.unicode.org/) - это спецификация, цель которой - перечислить все символы, используемые в человеческих языках, и присвоить каждому символу свой уникальный код. Спецификации Unicode постоянно пересматриваются и обновляются с целью добавления новых языков и символов.

Символ * - это наименьший из возможных компонентов текста. «A», «B», «C» и т.д. - это разные символы. Например, «È» и «И». Символы различаются в зависимости от языка или контекста, о котором вы говорите. Например, для обозначения римской цифры «Единица» есть символ «Ⅰ», который отделен от заглавной буквы «I». Обычно они выглядят одинаково, но это два разных символа, которые имеют разное значение.

Стандарт Unicode описывает, как символы представляются с помощью ** кодовых точек**. Значением кодовой точки является целое число в диапазоне от 0 до 0x10FFFF (около 1,1 миллиона значений, actual number assigned меньше). В стандарте и в этом документе кодовая точка записывается с использованием обозначения U+265E для обозначения символа со значением 0x265e (9 822 в десятичной системе счисления).

Стандарт Unicode содержит множество таблиц, в которых перечислены символы и соответствующие им кодовые точки:

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET
...
2167    'Ⅷ'; ROMAN NUMERAL EIGHT
2168    'Ⅸ'; ROMAN NUMERAL NINE
...
265E    '♞'; BLACK CHESS KNIGHT
265F    '♟'; BLACK CHESS PAWN
...
1F600   '😀'; GRINNING FACE
1F609   '😉'; WINKING FACE
...

Строго говоря, эти определения подразумевают, что бессмысленно говорить «это символ» U+265E“. U+265E - это кодовая точка, которая представляет некоторый конкретный символ; в данном случае это символ «ЧЕРНЫЙ ШАХМАТНЫЙ КОНЬ», «♞». В неформальном контексте это различие между кодовыми точками и символами иногда забывается.

Символ представлен на экране или на бумаге набором графических элементов, который называется глифом. Например, символ для заглавной буквы A состоит из двух диагональных штрихов и горизонтального штриха, хотя точные детали будут зависеть от используемого шрифта. В большинстве случаев в коде на Python не нужно беспокоиться о глифах; определение правильного глифа для отображения - это, как правило, работа инструментария GUI или средства визуализации шрифтов в терминале.

Кодировки

Подводя итог предыдущему разделу: строка в Юникоде - это последовательность кодовых точек, которые представляют собой числа от 0 до 0x10FFFF (1 114 111 десятичных знаков). Эта последовательность кодовых точек должна быть представлена в памяти в виде набора ** кодовых единиц**, и кодовые единицы затем преобразуются в 8-битные байты. Правила преобразования строки в Юникоде в последовательность байтов называются кодировкой символов, или просто кодировкой.

Первая кодировка, которая может прийти вам в голову, - это использование 32-разрядных целых чисел в качестве единицы кода, а затем использование представления 32-разрядных целых чисел процессором. В этом представлении строка «Python» может выглядеть следующим образом:

   P           y           t           h           o           n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00
   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

Это простое представление, но его использование сопряжено с рядом проблем.

  1. Это не переносимо; разные процессоры упорядочивают байты по-разному.

  2. Это очень экономно расходует пространство. В большинстве текстов большинство кодовых точек меньше 127 или меньше 255, поэтому 0x00 байт занимает много места. Приведенная выше строка занимает 24 байта по сравнению с 6 байтами, необходимыми для представления в формате ASCII. Увеличение использования оперативной памяти не имеет большого значения (настольные компьютеры имеют гигабайты оперативной памяти, а строки обычно не такие большие), но увеличение использования дисковой и сетевой пропускной способности в 4 раза недопустимо.

  3. Он несовместим с существующими функциями языка Си, такими как strlen(), поэтому необходимо использовать новое семейство расширенных строковых функций.

Поэтому эта кодировка используется нечасто, и люди вместо нее выбирают другие, более эффективные и удобные, такие как UTF-8.

UTF-8 - одна из наиболее часто используемых кодировок, и Python часто использует ее по умолчанию. UTF расшифровывается как «Формат преобразования Unicode», а «8» означает, что в кодировке используются 8-разрядные значения. (Существуют также кодировки UTF-16 и UTF-32, но они используются реже, чем UTF-8.) В UTF-8 используются следующие правила:

  1. Если кодовая точка равна < 128, то она представлена соответствующим байтовым значением.

  2. Если кодовая точка >= 128, она преобразуется в последовательность из двух, трех или четырех байт, где каждый байт последовательности находится в диапазоне от 128 до 255.

UTF-8 обладает несколькими удобными свойствами:

  1. Он может обрабатывать любую кодовую точку в Юникоде.

  2. Строка в Юникоде преобразуется в последовательность байтов, которая содержит встроенные нулевые байты только там, где они представляют нулевой символ (U+0000). Это означает, что строки UTF-8 могут обрабатываться функциями C, такими как strcpy(), и передаваться по протоколам, которые не могут обрабатывать нулевые байты ни для чего, кроме маркеров конца строки.

  3. Строка текста в формате ASCII также является допустимым текстом в формате UTF-8.

  4. UTF-8 довольно компактен; большинство часто используемых символов могут быть представлены одним или двумя байтами.

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

  6. UTF-8 - это кодировка, ориентированная на байты. Кодировка определяет, что каждый символ представлен определенной последовательностью из одного или нескольких байтов. Это позволяет избежать проблем с упорядочением байтов, которые могут возникнуть при использовании целочисленных кодировок и кодировок, ориентированных на слова, таких как UTF-16 и UTF-32, где последовательность байтов варьируется в зависимости от аппаратного обеспечения, на котором была закодирована строка.

Рекомендации

В Unicode Consortium site содержатся таблицы символов, глоссарий и PDF-версии спецификации Unicode. Будьте готовы к тому, что вам будет нелегко читать. A chronology информация о происхождении и развитии Unicode также доступна на сайте.

`discusses the history of Unicode and UTF-8 <https://www.youtube.com/watch?v=MijmeoH9LT4>`_На Youtube-канале Computerphile Том Скотт кратко (9 минут 36 секунд) рассказал о своей игре.

Чтобы помочь разобраться в стандарте, Юкка Корпела написал an introductory guide для чтения таблиц символов Unicode.

Еще одна статья good introductory article написана Джоэлом Спольски. Если из этого вступления вам что-то не ясно, попробуйте прочитать эту альтернативную статью, прежде чем продолжить.

Записи в Википедии часто полезны; например, смотрите записи для «character encoding» и UTF-8.

Поддержка Unicode в Python

Теперь, когда вы освоили основы Unicode, мы можем рассмотреть возможности Unicode в Python.

Строковый тип

Начиная с версии Python 3.0, тип языка str содержит символы Unicode, что означает, что любая строка, созданная с использованием синтаксиса "unicode rocks!", 'unicode rocks!', или строки, заключенной в тройные кавычки, хранится как Unicode.

Кодировка исходного кода Python по умолчанию - UTF-8, поэтому вы можете просто включить символ Unicode в строковый литерал:

try:
    with open('/tmp/input.txt', 'r') as f:
        ...
except OSError:
    # 'File not found' error message.
    print("Fichier non trouvé")

Примечание: Python 3 также поддерживает использование символов Unicode в идентификаторах:

répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
    f.write("test\n")

Если вы не можете ввести определенный символ в своем редакторе или по какой-либо причине хотите сохранить исходный код только в формате ASCII, вы также можете использовать escape-последовательности в строковых литералах. (В зависимости от вашей системы, вы можете увидеть фактический символ заглавной буквы-дельта вместо отступа u.)

>>> "\N{GREEK CAPITAL LETTER DELTA}"  # Using the character name
'\u0394'
>>> "\u0394"                          # Using a 16-bit hex value
'\u0394'
>>> "\U00000394"                      # Using a 32-bit hex value
'\u0394'

Кроме того, можно создать строку, используя decode() метод bytes. Этот метод принимает аргумент encoding, такой как UTF-8, и, возможно, аргумент errors.

Аргумент errors определяет ответ, когда входная строка не может быть преобразована в соответствии с правилами кодирования. Допустимыми значениями для этого аргумента являются 'strict' (создать исключение UnicodeDecodeError), 'replace' ( использовать U+FFFD, REPLACEMENT CHARACTER), 'ignore' (просто исключить символ из результата Unicode) или 'backslashreplace' (вставить escape-последовательность \xNN). Следующие примеры демонстрируют различия:

>>> b'\x80abc'.decode("utf-8", "strict")  
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
  invalid start byte
>>> b'\x80abc'.decode("utf-8", "replace")
'\ufffdabc'
>>> b'\x80abc'.decode("utf-8", "backslashreplace")
'\\x80abc'
>>> b'\x80abc'.decode("utf-8", "ignore")
'abc'

Кодировки задаются в виде строк, содержащих название кодировки. В Python имеется около 100 различных кодировок; список приведен в ссылке на библиотеку Python по адресу Стандартные кодировки. Некоторые кодировки имеют несколько названий; например,, 'latin-1', 'iso_8859_1' и '8859“ являются синонимами для одной и той же кодировки.

Односимвольные строки в Юникоде также могут быть созданы с помощью встроенной функции chr(), которая принимает целые числа и возвращает строку в Юникоде длиной 1, содержащую соответствующую кодовую точку. Обратная операция выполняется встроенной функцией ord(), которая принимает односимвольную строку в Юникоде и возвращает значение кодовой точки:

>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344

Преобразование в байты

Противоположным методу bytes.decode() является str.encode(), который возвращает bytes представление строки Unicode, закодированной в запрошенной кодировке.

Параметр errors совпадает с параметром метода decode(), но поддерживает еще несколько возможных обработчиков. Помимо 'strict', 'ignore', и 'replace' (которые в данном случае вставляют вопросительный знак вместо неэнкодируемого символа), есть также 'xmlcharrefreplace' (вставляет ссылку на XML-символ)., backslashreplace (вставляет управляющую последовательность \uNNNN) и namereplace (вставляет управляющую последовательность \N{...}).

В следующем примере показаны различные результаты:

>>> u = chr(40960) + 'abcd' + chr(1972)
>>> u.encode('utf-8')
b'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')  
Traceback (most recent call last):
    ...
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in
  position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
b'abcd'
>>> u.encode('ascii', 'replace')
b'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
b'&#40960;abcd&#1972;'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

Процедуры низкого уровня для регистрации доступных кодировок и доступа к ним находятся в модуле codecs. Внедрение новых кодировок также требует понимания модуля codecs. Однако функции кодирования и декодирования, предоставляемые этим модулем, обычно являются более низкоуровневыми, чем это удобно, а написание новых кодировок является специализированной задачей, поэтому этот модуль не будет рассмотрен в данном руководстве.

Литералы Unicode в исходном коде Python

В исходном коде Python конкретные кодовые точки в Юникоде могут быть записаны с использованием escape-последовательности \u, за которой следуют четыре шестнадцатеричные цифры, дающие кодовую точку. Escape-последовательность \U аналогична, но содержит восемь шестнадцатеричных цифр, а не четыре:

>>> s = "a\xac\u1234\u20ac\U00008000"
... #     ^^^^ two-digit hex escape
... #         ^^^^^^ four-digit Unicode escape
... #                     ^^^^^^^^^^ eight-digit Unicode escape
>>> [ord(c) for c in s]
[97, 172, 4660, 8364, 32768]

Использование escape-последовательностей для кодовых точек, превышающих 127, допустимо в небольших дозах, но становится раздражающим, если вы используете много символов с ударениями, как это было бы в программе с сообщениями на французском или каком-либо другом языке, использующем ударения. Вы также можете собирать строки, используя встроенную функцию chr(), но это еще более утомительно.

В идеале вы хотели бы иметь возможность записывать литералы в естественной кодировке вашего языка. Затем вы могли бы отредактировать исходный код Python с помощью вашего любимого редактора, который бы естественным образом отображал символы с ударениями и использовал правильные символы во время выполнения.

По умолчанию Python поддерживает запись исходного кода в UTF-8, но вы можете использовать практически любую кодировку, если вы укажете используемую кодировку. Это делается путем включения специального комментария в качестве первой или второй строки исходного файла:

#!/usr/bin/env python
# -*- coding: latin-1 -*-

u = 'abcdé'
print(ord(u[-1]))

Синтаксис основан на нотации Emacs для указания переменных, локальных для файла. Emacs поддерживает множество различных переменных, но Python поддерживает только «кодирование». Символы -*- указывают Emacs на то, что комментарий является специальным; для Python они не имеют значения, но являются условным обозначением. Python ищет coding: name или coding=name в комментарии.

Если вы не добавите такой комментарий, по умолчанию будет использоваться кодировка UTF-8, как уже упоминалось. Смотрите также PEP 263 для получения дополнительной информации.

Свойства Юникода

Спецификация Unicode включает в себя базу данных с информацией о кодовых точках. Для каждой определенной кодовой точки информация включает название символа, его категорию, числовое значение, если применимо (для символов, представляющих числовые понятия, такие как римские цифры, дроби, такие как одна треть и четыре пятых, и т.д.). Также существуют свойства, связанные с отображением, например, как чтобы использовать кодовую точку в двунаправленном тексте.

Следующая программа отображает некоторую информацию о нескольких символах и печатает числовое значение одного конкретного символа:

import unicodedata

u = chr(233) + chr(0x0bf2) + chr(3972) + chr(6000) + chr(13231)

for i, c in enumerate(u):
    print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
    print(unicodedata.name(c))

# Get numeric value of second character
print(unicodedata.numeric(u[1]))

При запуске эта программа выводит:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0

Коды категорий - это сокращения, описывающие природу символа. Они сгруппированы в такие категории, как «Буква», «Цифра», «Знак препинания» или «Символ», которые, в свою очередь, разбиты на подкатегории. Чтобы получить коды из приведенного выше вывода, 'Ll' означает «Буква в нижнем регистре», 'No' означает «Число, другое», 'Mn' - «Знак, без пробелов», а 'So' - это «Символ, другой». Список кодов категорий приведен в разделе the General Category Values section of the Unicode Character Database documentation.

Сравнение строк

Юникод несколько усложняет сравнение строк, поскольку один и тот же набор символов может быть представлен разными последовательностями кодовых точек. Например, буква типа «к» может быть представлена в виде одной кодовой точки U+00EA или в виде U+0065 U+0302, которая является кодовой точкой для «е», за которой следует кодовая точка для «СОЧЕТАНИЯ ОГИБАЮЩЕГО УДАРЕНИЯ». При печати они будут выдавать один и тот же результат, но один из них представляет собой строку длиной 1, а другой - длиной 2.

Одним из инструментов для сравнения без учета регистра является метод casefold() string, который преобразует строку в форму без учета регистра в соответствии с алгоритмом, описанным стандартом Unicode. Этот алгоритм имеет специальную обработку для таких символов, как немецкая буква «β» (кодовая точка U+00DF), которая преобразуется в пару строчных букв «ss».

>>> street = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'

Вторым инструментом является функция модуля unicodedata, которая normalize() преобразует строки в одну из нескольких обычных форм, где буквы, за которыми следует комбинирующий символ, заменяются одиночными символами. normalize() может использоваться для выполнения сравнения строк, которое не приведет к ложному выводу о неравенстве, если в двух строках используются различные комбинации символов.:

import unicodedata

def compare_strs(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(s1) == NFD(s2)

single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('length of first string=', len(single_char))
print('length of second string=', len(multiple_chars))
print(compare_strs(single_char, multiple_chars))

При запуске это приводит к:

$ python3 compare-strs.py
length of first string= 1
length of second string= 2
True

Первым аргументом функции normalize() является строка, задающая желаемую форму нормализации, которая может быть одной из „NFC“, „NFKC“, „NFD“ и „NFKD“.

Стандарт Unicode также определяет, как выполнять сравнения без учета регистра:

import unicodedata

def compare_caseless(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())

# Example usage
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'

print(compare_caseless(single_char, multiple_chars))

Это выведет True. (Почему NFD() вызывается дважды? Потому что есть несколько символов, из-за которых casefold() возвращает ненормализованную строку, поэтому результат необходимо нормализовать еще раз. Обсуждение и пример приведены в разделе 3.13 стандарта Unicode.)

Регулярные выражения Юникода

Регулярные выражения, поддерживаемые модулем re, могут быть представлены либо в виде байтов, либо в виде строк. Некоторые последовательности специальных символов, такие как \d и \w, имеют разные значения в зависимости от того, представлен ли шаблон в виде байтов или строки. Например, \d будет соответствовать символам [0-9] в байтах, но в строках будет соответствовать любому символу, который находится в категории 'Nd'.

Строка в этом примере содержит число 57, написанное как тайскими, так и арабскими цифрами:

import re
p = re.compile(r'\d+')

s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))

При выполнении \d+ будет соответствовать тайским цифрам и они будут выведены на печать. Если вы установите флажок re.ASCII, то compile(), \d+ вместо этого будет соответствовать подстроке «57».

Аналогично, \w соответствует широкому спектру символов Юникода, но только [a-zA-Z0-9_] в байтах или если указано re.ASCII, а \s будет соответствовать либо пробельным символам Юникода, либо [ \t\n\r\f\v].

Рекомендации

Вот несколько хороших альтернативных обсуждений поддержки Unicode в Python:

Тип str описан в ссылке на библиотеку Python по адресу Тип текстовой последовательности — str.

Документация к модулю unicodedata.

Документация к модулю codecs.

Марк-Андре Лембург выступил с докладом a presentation titled «Python and Unicode» (PDF slides) на EuroPython 2002. Слайды представляют собой отличный обзор дизайна функций Unicode в Python 2 (где строковый тип Unicode называется unicode, а литералы начинаются с u).

Чтение и запись данных в формате Unicode

Как только вы написали некоторый код, который работает с данными в Юникоде, возникает следующая проблема - ввод/вывод данных. Как вы вводите строки в Юникоде в свою программу и как вы преобразуете Юникод в форму, подходящую для хранения или передачи?

Возможно, вам не потребуется ничего делать, в зависимости от ваших источников ввода и назначений вывода; вам следует проверить, поддерживают ли библиотеки, используемые в вашем приложении, изначально Unicode. Например, синтаксические анализаторы XML часто возвращают данные в формате Unicode. Многие реляционные базы данных также поддерживают столбцы, содержащие значения в Юникоде, и могут возвращать значения в юникоде из SQL-запроса.

Данные в Юникоде обычно преобразуются в определенную кодировку перед записью на диск или отправкой по сокету. Можно выполнить всю работу самостоятельно: открыть файл, прочитать из него 8-битный объект bytes и преобразовать байты в bytes.decode(encoding). Однако использовать ручной подход не рекомендуется.

Одной из проблем является многобайтовый характер кодировок; один символ Юникода может быть представлен несколькими байтами. Если вы хотите прочитать файл в виде фрагментов произвольного размера (скажем, 1024 или 4096 байт), вам нужно написать код обработки ошибок, чтобы отследить случай, когда в конце фрагмента считывается только часть байт, кодирующих один символ Unicode. Одним из решений было бы считывание всего файла в память и последующее декодирование, но это не позволяет работать с файлами большого размера; если вам нужно прочитать файл размером 2 ГБ, вам потребуется 2 Гб оперативной памяти. (На самом деле, даже больше, поскольку, по крайней мере, на мгновение вам нужно будет сохранить в памяти как закодированную строку, так и ее версию в Юникоде.)

Решением было бы использовать низкоуровневый интерфейс декодирования для отслеживания случаев частичного кодирования последовательностей. Работа по реализации этого уже была проделана за вас: встроенная функция open() может возвращать объект, подобный файлу, который предполагает, что содержимое файла находится в указанной кодировке, и принимает параметры Unicode для таких методов, как read() и write(). Это работает с помощью параметров open() encoding и errors, которые интерпретируются точно так же, как в str.encode() и bytes.decode().

Таким образом, считывание Юникода из файла является простым:

with open('unicode.txt', encoding='utf-8') as f:
    for line in f:
        print(repr(line))

Также можно открывать файлы в режиме обновления, позволяя как чтение, так и запись:

with open('test', encoding='utf-8', mode='w+') as f:
    f.write('\u4500 blah blah blah\n')
    f.seek(0)
    print(repr(f.readline()[:1]))

Символ Юникода U+FEFF используется в качестве обозначения порядка байтов (BOM) и часто записывается как первый символ файла, чтобы облегчить автоматическое определение порядка байтов в файле. Некоторые кодировки, такие как UTF-16, предполагают наличие спецификации в начале файла; при использовании такой кодировки спецификация автоматически записывается как первый символ и автоматически удаляется при чтении файла. Существуют варианты этих кодировок, такие как «utf-16-le» и «utf-16-be» для кодировок с малым и большим порядком байтов, которые определяют один конкретный порядок байтов и не пропускают спецификацию.

В некоторых областях также принято использовать «спецификацию» в начале файлов, закодированных в UTF-8; это название вводит в заблуждение, поскольку UTF-8 не зависит от порядка байтов. Пометка просто сообщает о том, что файл закодирован в UTF-8. Для чтения таких файлов используйте кодек „utf-8-sig“, чтобы автоматически пропустить метку, если она присутствует.

Имена файлов в Юникоде

Большинство широко используемых сегодня операционных систем поддерживают имена файлов, содержащие произвольные символы Unicode. Обычно это реализуется путем преобразования строки Unicode в некоторую кодировку, которая варьируется в зависимости от системы. Сегодня Python переходит на использование UTF-8: Python в Mac OS использовал UTF-8 в нескольких версиях, а Python 3.6 также перешел на использование UTF-8 в Windows. В системах Unix будет только filesystem encoding. если вы установили переменные окружения LANG или LC_CTYPE; если вы этого не сделали, кодировка по умолчанию снова будет UTF-8.

Функция sys.getfilesystemencoding() возвращает кодировку, которая будет использоваться в вашей текущей системе, на случай, если вы захотите выполнить кодировку вручную, но нет особых причин беспокоиться. При открытии файла для чтения или записи вы обычно можете просто указать строку Unicode в качестве имени файла, и она будет автоматически преобразована в нужную вам кодировку:

filename = 'filename\u4500abc'
with open(filename, 'w') as f:
    f.write('blah\n')

Функции в модуле os, такие как os.stat(), также будут принимать имена файлов в Юникоде.

Функция os.listdir() возвращает имена файлов, в связи с чем возникает проблема: должна ли она возвращать версию имен файлов в Юникоде или она должна возвращать байты, содержащие закодированные версии? os.listdir() можно использовать и то, и другое, в зависимости от того, был ли указан путь к каталогу в байтах или в виде строки в Юникоде. Если вы передадите строку Unicode в качестве пути, имена файлов будут декодированы с использованием кодировки файловой системы и будет возвращен список строк Unicode, в то время как передача байтового пути вернет имена файлов в виде байтов. Например, предполагая, что значение по умолчанию filesystem encoding равно UTF-8, запустите следующую программу:

fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()

import os
print(os.listdir(b'.'))
print(os.listdir('.'))

приведет к следующему результату:

$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]

Первый список содержит имена файлов в кодировке UTF-8, а второй список содержит версии в Юникоде.

Обратите внимание, что в большинстве случаев вы можете просто использовать Unicode с этими API. API-интерфейсы bytes следует использовать только в системах, где могут присутствовать не поддающиеся расшифровке имена файлов; сейчас это в значительной степени относится только к системам Unix.

Советы по написанию программ, поддерживающих Unicode

В этом разделе приведены некоторые рекомендации по написанию программного обеспечения, работающего с Unicode.

Самый важный совет заключается в следующем:

Программное обеспечение должно работать только со строками Unicode внутри системы, декодируя входные данные как можно быстрее и кодируя выходные данные только в конце.

Если вы попытаетесь написать функции обработки, которые принимают как Юникод, так и байтовые строки, вы обнаружите, что ваша программа уязвима для ошибок везде, где вы комбинируете два разных типа строк. Автоматического кодирования или декодирования нет: если вы сделаете это, например, str + bytes, будет поднят TypeError.

При использовании данных, поступающих из веб-браузера или какого-либо другого ненадежного источника, распространенным методом является проверка на наличие недопустимых символов в строке, прежде чем использовать строку в сгенерированной командной строке или сохранять ее в базе данных. Если вы делаете это, будьте внимательны и проверяйте декодированную строку, а не закодированные байтовые данные; некоторые кодировки могут обладать интересными свойствами, такими как отсутствие биективности или неполная совместимость с ASCII. Это особенно верно, если во входных данных также указана кодировка, поскольку злоумышленник может выбрать хитроумный способ скрыть вредоносный текст в закодированном байтовом потоке.

Преобразование между Кодировками Файлов

Класс StreamRecoder может прозрачно выполнять преобразование между кодировками, принимая поток, который возвращает данные в кодировке #1, и ведя себя как поток, возвращающий данные в кодировке #2.

Например, если у вас есть входной файл f, который написан на латинице 1, вы можете обернуть его символом StreamRecoder, чтобы вернуть байты, закодированные в UTF-8:

new_f = codecs.StreamRecoder(f,
    # en/decoder: used by read() to encode its results and
    # by write() to decode its input.
    codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),

    # reader/writer: used to read and write to the stream.
    codecs.getreader('latin-1'), codecs.getwriter('latin-1') )

Файлы в неизвестной кодировке

Что вы можете сделать, если вам нужно внести изменения в файл, но вы не знаете кодировку файла? Если вы знаете, что кодировка совместима с ASCII, и хотите проверить или изменить только части ASCII, вы можете открыть файл с помощью обработчика ошибок surrogateescape:

with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:
    data = f.read()

# make changes to the string 'data'

with open(fname + '.new', 'w',
          encoding="ascii", errors="surrogateescape") as f:
    f.write(data)

Обработчик ошибок surrogateescape декодирует любые байты, отличные от ASCII, в виде кодовых точек в специальном диапазоне от U+DC80 до U+DCFF. Затем эти кодовые точки снова преобразуются в те же байты, когда для кодирования данных и их обратной записи используется обработчик ошибок surrogateescape.

Рекомендации

В одном из разделов доклада Дэвида Бизли на PyCon 2010 Mastering Python 3 Input/Output обсуждается обработка текста и двоичных данных.

В PDF slides for Marc-André Lemburg’s presentation «Writing Unicode-aware Applications in Python» обсуждаются вопросы кодировки символов, а также способы интернационализации и локализации приложения. Эти слайды посвящены только Python 2.x.

The Guts of Unicode in Python - это доклад Бенджамина Питерсона на PyCon 2013, в котором обсуждается внутреннее представление Unicode в Python 3.3.

Признание

Первоначальный проект этого документа был написан Эндрю Кухлингом. С тех пор он был доработан Александром Белопольским, Георгом Брандлом, Эндрю Кухлингом и Эцио Мелотти.

Благодарим следующих пользователей, которые заметили ошибки или высказали свои предложения по этой статье: Эрик Араужо, Николас Бастин, Ник Коглан, Мариус Гедминас, Кент Джонсон, Кен Круглер, Марк-Андре Лембург, Мартин фон Левис, Терри Дж. Риди, Сергей Сторчака, Эрик Сан, Чад Уайтекер, Грэм Вайдман.

Вернуться на верх