Spec-Zone.ru › Python 3.9

Руководство по Unicode

Версия

1.12

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

Введение в Unicode

Определения

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

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

Символ — это наименьшая возможная составляющая текста. ‘A’, ‘B’, ‘C’ и т. д. — это все разные символы. Так же как и ‘È’ и ‘Í’. Символы различаются в зависимости от языка или контекста, о котором идёт речь. Например, есть символ для «римской цифры один», ‘Ⅰ’, который отличается от заглавной буквы ‘I’. Они обычно выглядят одинаково, но это два разных символа, имеющих разное значение.

Стандарт Unicode описывает, как символы представлены кодовыми точками. Значение кодовой точки — это целое число в диапазоне от 0 до 0x10FFFF (около 1,1 миллиона значений, фактическое присвоенное количество меньше этого). В стандарте и в данном документе кодовая точка записывается с помощью обозначения U+265E для обозначения символа со значением 0x265e (9822 в десятичной системе).

Стандарт 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 не нужно беспокоиться о глифах; определение правильного глифа для отображения обычно является задачей инструментария графического интерфейса или рендерера шрифта терминала.

Кодировки

Для подведения итогов предыдущего раздела: строка 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», а «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 могут обрабатываться функциями C, такими как strcpy(), и передаваться по протоколам, которые не могут обрабатывать нулевые байты ни для чего, кроме маркеров конца строки.
  3. Строка ASCII текста также является допустимым текстом UTF-8.
  4. UTF-8 довольно компактен; большинство часто используемых символов можно представить одним или двумя байтами.
  5. Если байты повреждены или потеряны, можно определить начало следующей кодовой точки, закодированной в UTF-8, и синхронизироваться. Вероятность того, что случайные 8-битные данные будут выглядеть как допустимый UTF-8, также мала.
  6. UTF-8 — это кодировка, ориентированная на байты. Кодировка определяет, что каждый символ представлен определённой последовательностью одного или нескольких байтов. Это позволяет избежать проблем с порядком байтов, которые могут возникнуть при использовании кодировок, ориентированных на целые числа и слова, таких как UTF-16 и UTF-32, где последовательность байтов меняется в зависимости от оборудования, на котором была закодирована строка.

Ссылки

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

На канале YouTube Computerphile Том Скотт кратко рассказывает об истории Unicode и UTF-8 (9 минут 36 секунд).

Для лучшего понимания стандарта Юкка Корпела написал вводное руководство по чтению таблиц символов Unicode.

Ещё одна хорошая вводная статья была написана Джоэлом Спольски. Если это введение не прояснило ситуацию, вам следует попробовать прочитать эту альтернативную статью, прежде чем продолжить.

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

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

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

Тип строки

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

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

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

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

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

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

>>> "\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' (просто исключить символ из результата Юникода) или '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 в запрошенной кодировке.

Параметр errors такой же, как и параметр метода decode(), но поддерживает несколько дополнительных обработчиков. Помимо 'strict', 'ignore', и 'replace' (в этом случае вставляется вопросительный знак вместо некодируемого символа), также есть 'xmlcharrefreplace' (вставляет XML-ссылку символа), backslashreplace (вставляет escape-последовательность \uNNNN) и namereplace (вставляет escape-последовательность \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'ꀀabcd޴'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

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

Литералы Юникода в исходном коде 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 поддерживает только ‘coding’. Символы -*- указывают Emacs, что комментарий является специальным; они не имеют никакого значения для Python, но являются соглашением. Python ищет coding: name или coding=name в комментарии.

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

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

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

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

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' — «Символ, другой». Обратитесь к разделу «Значения категорий» документации базы данных символов Юникода для списка кодов категорий.

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

Юникод добавляет некоторую сложность к сравнениям строк, потому что один и тот же набор символов может быть представлен различными последовательностями кодов символов. Например, буква «ê» может быть представлена одним кодом U+00EA или как U+0065 U+0302, что представляет собой код «e» и код «COMBINING CIRCUMFLEX ACCENT». Эти представления дадут одинаковый результат при печати, но одно — строка длиной 1, а другое — длиной 2.

Одним из инструментов для сравнения строк без учета регистра является метод строки casefold(), который преобразует строку в форму без учета регистра, следуя алгоритму, описанному в стандарте Юникода. Этот алгоритм имеет специальную обработку для символов, таких как немецкая буква «ß» (код 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».

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

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 соответствует широкому диапазону символов Unicode, но только [a-zA-Z0-9_] в байтах или если задан re.ASCII, и \s будет соответствовать либо символам Unicode-пробела, либо [ \t\n\r\f\v].

Ссылки

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

  • Обработка текстовых файлов в Python 3, автором Nick Coghlan.
  • Практический Unicode, презентация Ned Batchelder на PyCon 2012.

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

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

Marc-André Lemburg представил презентацию под названием «Python и Unicode» (PDF слайды) на EuroPython 2002. Слайды являются отличным обзором дизайна функций Unicode Python 2 (где тип строки Unicode называется unicode , а литералы начинаются с u).

Чтение и запись данных Юникод

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

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

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

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

Решением было бы использовать низкоуровневый интерфейс декодирования, чтобы поймать случай частичных кодируемых последовательностей. Эта работа уже выполнена за вас: встроенная функция open() может вернуть похожий на файл объект, предполагающий, что содержимое файла находится в указанной кодировке, и принимает параметры Юникод для методов, таких как 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», чтобы автоматически пропустить метку, если она есть.

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

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

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

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

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

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

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

Рекомендации по написанию программ, учитывающих Юникод

Этот раздел содержит несколько рекомендаций по написанию программного обеспечения, обрабатывающего Юникод.

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

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

Если вы попытаетесь написать функции обработки, которые принимают как строки Юникод, так и байтовые строки, вы обнаружите, что ваша программа уязвима для ошибок там, где вы объединяете два разных типа строк. Автоматического кодирования или декодирования не происходит: если вы, например, 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 будет использован для кодирования данных и записи их обратно.

Ссылки

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

В слайдах PDF-презентации Марка-Андре Лембурга «Написание приложений, учитывающих Юникод, на Python» обсуждаются вопросы кодировок символов, а также способы локализации и интернационализации приложения. Эти слайды охватывают только Python 2.x.

Внутренности Юникода в Python — доклад Бенджамина Питерсона на PyCon 2013, в котором обсуждается внутреннее представление Юникода в Python 3.3.

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

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

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

© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.9/howto/unicode.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API