Юникод HOWTO

Релиз

1.12

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

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

Определения

Современные программы должны уметь работать с большим количеством символов. Приложения часто интернационализированы для отображения сообщений и вывода на различных языках, выбираемых пользователем; одна и та же программа может выводить сообщение об ошибке на английском, французском, японском, иврите или русском языках. Веб-контент может быть написан на любом из этих языков, а также может включать различные символы emoji. Строковый тип 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“, „♞“. В неформальных контекстах это различие между кодовыми точками и символами иногда забывается.

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

Кодировки

Подведем итоги предыдущего раздела: строка 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. Он не совместим с существующими функциями языка Си, такими как 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 о происхождении и развитии Юникода.

На канале Computerphile Youtube Том Скотт кратко 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, то есть любая строка, созданная с помощью "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, вы также можете использовать экранирующие последовательности в строковых литералах. (В зависимости от вашей системы, вы можете увидеть реальный глиф заглавной буквы-дельты вместо 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“ являются синонимами одной и той же кодировки.

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

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

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

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

Регулярные выражения, поддерживаемые модулем 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 соответствует широкому разнообразию символов Unicode, но только [a-zA-Z0-9_] в байтах или при наличии re.ASCII, а \s будет соответствовать либо пробельным символам Unicode, либо [ \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 в вашей программе, и как преобразовать 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().

Поэтому чтение Unicode из файла очень просто:

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

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

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

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

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

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

Если вы попытаетесь написать функции обработки, которые принимают как строки 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, в котором обсуждается внутреннее представление Unicode в 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.

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