Unicode HOWTO

Выпуск:

1.12

В этом HOWTO рассказывается о поддержке 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 - это кодовая точка, которая представляет определенный символ; в данном случае она представляет символ „BLACK CHESS KNIGHT“, „♞“. В неформальной обстановке это различие между кодовыми точками и символами иногда забывается.

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

Кодировки

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

Первое кодирование, о котором вы можете подумать, - это использование 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. Она не совместима с существующими функциями языка C, такими как strlen(), поэтому необходимо использовать новое семейство функций широких строк.

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

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

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

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

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

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

  2. Строка Unicode превращается в последовательность байтов, содержащую встроенные нулевые байты только там, где они представляют нулевой символ (U+0000). Это означает, что строки UTF-8 могут обрабатываться функциями языка Си, такими как 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.

На Youtube-канале Computerphile Том Скотт кратко discusses the history of Unicode and UTF-8 (9 минут 36 секунд).

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

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

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

Поддержка Юникода в Python

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

Тип строки

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

По умолчанию кодировка исходного кода 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. Этот метод принимает аргумент кодировка, например UTF-8, и, по желанию, аргумент ошибки.

Аргумент errors определяет реакцию, когда входная строка не может быть преобразована в соответствии с правилами кодировки. Допустимыми значениями этого аргумента являются 'strict' (вызывает исключение UnicodeDecodeError), 'replace' (использует U+FFFD, REPLACEMENT CHARACTER), 'ignore' (просто оставляет символ вне результата Unicode) или 'backslashreplace' (вставляет управляющую последовательность \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. Однако функции кодирования и декодирования, возвращаемые этим модулем, обычно более низкоуровневые, чем это удобно, а написание новых кодировок является специализированной задачей, поэтому в данном HOWTO этот модуль рассматриваться не будет.

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

В исходном коде Python определенные кодовые точки Unicode могут быть записаны с помощью управляющей последовательности \u, за которой следуют четыре шестнадцатеричные цифры, указывающие на кодовую точку. Эскейп-последовательность \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]

Использование экранирующих последовательностей для кодовых точек, превышающих 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, которая является кодовой точкой для «e», за которой следует кодовая точка для «COMBINING CIRCUMFLEX ACCENT». При печати они выдадут одинаковый результат, но одна из них - строка длины 1, а другая - длины 2.

Одним из инструментов для сравнения без учета регистра является строковый метод casefold(), который преобразует строку в форму без учета регистра, следуя алгоритму, описанному в стандарте 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))

При запуске выводится сообщение:

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

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

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

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 Стандарта Юникода).

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

Регулярные выражения, поддерживаемые модулем 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+ будет соответствовать тайским цифрам и выводить их на печать. Если к compile() добавить флаг re.ASCII, то вместо него \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 в вашу программу и как преобразовать Unicode в форму, пригодную для хранения или передачи?

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

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

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

Решением было бы использование низкоуровневого интерфейса декодирования для отлова случаев частичного кодирования последовательностей. Работа по реализации этого уже проделана: встроенная функция open() может возвращать файлоподобный объект, который предполагает, что содержимое файла находится в указанной кодировке, и принимает параметры Unicode для таких методов, как read() и write(). Это работает через параметры encoding и errors функции open(), которые интерпретируются так же, как и параметры в 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, ожидают, что BOM будет присутствовать в начале файла; при использовании такой кодировки BOM будет автоматически записываться в качестве первого символа и будет беззвучно отбрасываться при чтении файла. Существуют варианты этих кодировок, такие как „utf-16-le“ и „utf-16-be“ для little-endian и big-endian кодировок, которые определяют один конкретный порядок байтов и не пропускают BOM.

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

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

Большинство современных операционных систем поддерживают имена файлов, содержащие произвольные символы Юникода. Обычно это реализуется путем преобразования строки Unicode в некоторую кодировку, которая зависит от системы. Сегодня Python сходится на использовании UTF-8: Python на MacOS использует 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(), также будут принимать имена файлов в формате Unicode.

Функция os.listdir() возвращает имена файлов, в связи с чем возникает вопрос: должна ли она возвращать версию имен файлов в кодировке Unicode или байты, содержащие закодированные версии? os.listdir() может делать и то, и другое, в зависимости от того, в каком виде вы передали путь к каталогу - в виде байтов или в виде строки Unicode. Если вы передадите в качестве пути строку 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 можно просто использовать Unicode. API байтов следует использовать только в системах, где могут присутствовать недекодируемые имена файлов; сейчас это практически только Unix-системы.

Советы по написанию программ с поддержкой Юникода

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

Самый важный совет:

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

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

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

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

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

Например, если у вас есть входной файл f в формате Latin-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 будет использован для кодирования данных и записи их обратно.

Ссылки

Один из разделов Mastering Python 3 Input/Output, доклада Дэвида Бизли на PyCon 2010, посвящен обработке текста и двоичных данных.

В 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, в котором обсуждается внутреннее представление Юникода в Python 3.3.

Благодарности

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

Спасибо следующим людям, которые отметили ошибки или внесли предложения по этой статье: Éric Araujo, Nicholas Bastin, Nick Coghlan, Marius Gedminas, Kent Johnson, Ken Krugler, Marc-André Lemburg, Martin von Löwis, Terry J. Reedy, Serhiy Storchaka, Eryk Sun, Chad Whitacre, Graham Wideman.