Spec-Zone.ru › Python 3.8

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

Ссылки

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

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

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

Параметр 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'ꀀabcd޴'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

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

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

В исходном коде Python конкретные коды символов Юникода можно записать с использованием последовательности экранирования \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 для получения дополнительной информации.

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

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

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

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» и кодом «ДЕЙСТВИТЕЛЬНЫЙ ГРИФ ЦИРКУМФЛЕКС». Они дадут один и тот же результат при выводе, но один — строка длиной 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 соответствует широкому спектру символов Юникода, но только [a-zA-Z0-9_] в байтах или если указан re.ASCII, а \s будет соответствовать либо символам пробелов Юникода, либо [ \t\n\r\f\v].

Ссылки

Некоторые хорошие альтернативные обсуждения поддержки Юникода в Python:

  • Обработка текстовых файлов в Python 3, автор Ник Коглан.
  • Практический Юникод, презентация Неда Бэтчелдера на PyCon 2012.

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

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

Марк-Андре Лембюрг представил презентацию «Python и Юникод» (PDF слайды) на EuroPython 2002. Слайды являются отличным обзором дизайна функций Юникода в Python 2 (где тип строки Юникода называется 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]))

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

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

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

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

Ссылки

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

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

Внутренности Unicode в Python — доклад Бенджамина Петерсона на PyCon 2013, посвящённый внутренней представленности Unicode в Python 3.3.

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

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

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

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

Spec-Zone.ru

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