Spec-Zone.ru › Python 3.11

Руководство по 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 есть таблицы символов, глоссарий и PDF-версии спецификации Unicode. Будьте готовы к некоторому сложному чтению. Хронология происхождения и развития 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, вы также можете использовать последовательности 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 с использованием запрошенной encoding.

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

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

Юникод добавляет некоторые сложности к сравнениям строк, поскольку один и тот же набор символов может быть представлен различными последовательностями кодов символов. Например, буква «ê» может быть представлена одним кодом 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, Ник Коглан.
  • Практический Unicode, презентация Неда Батчелдера на PyCon 2012.

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

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

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

Символ 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 для кодирования данных и записи их обратно.

Ссылки

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

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

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

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

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

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

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

Spec-Zone.ru

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