Юнікод HOWTO

Реліз:

1.12

Цей HOWTO обговорює підтримку Python специфікації Unicode для представлення текстових даних і пояснює різні проблеми, з якими люди зазвичай стикаються, намагаючись працювати з Unicode.

Знайомство з Unicode

визначення

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

Юнікод (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 означає «формат перетворення Юнікоду», а «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 Consortium є таблиці символів, глосарій і PDF-версії специфікації Unicode. Будьте готові до важкого читання. Хронологія походження та розвитку Unicode також доступна на сайті.

На Youtube-каналі Computerphile Том Скотт коротко обговорює історію Unicode та UTF-8 (9 хвилин 36 секунд).

To help understand the standard, Jukka Korpela has written an introductory guide to reading the Unicode character tables.

Ще одну хорошу вступну статтю написав Джоел Спольскі. Якщо цей вступ не прояснив вам щось, спробуйте прочитати цю альтернативну статтю, перш ніж продовжити.

Записи у Вікіпедії часто є корисними; дивіться, наприклад, записи для «кодування символів» і UTF-8.

Підтримка Unicode в Python

Тепер, коли ви вивчили основи Юнікоду, ми можемо розглянути особливості Юнікоду Python.

Тип рядка

Починаючи з Python 3.0, тип str мови містить символи Unicode, тобто будь-який рядок, створений за допомогою "unicode rocks!", 'unicode rocks!' або синтаксис рядка в потрійних лапках зберігається як Unicode.

Кодуванням за замовчуванням для вихідного коду 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 також підтримує використання символів 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' (просто залиште символ поза результатом Юнікоду) або '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 представлення рядка Юнікод, закодованого в потрібному кодуванні.

Параметр 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.

Літерали Unicode у вихідному коді Python

У вихідному коді Python певні кодові точки Unicode можна записати за допомогою escape-послідовності \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

Специфікація 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' означає „Літера, нижній регістр“, 'Ні' означає «Число, інше», 'Mn' це «Позначка, без пробілів» , а 'So' це «Символ, інше». Перегляньте розділ Загальні значення категорій документації бази даних символів Unicode, щоб отримати список кодів категорій.

Порівняння рядків

Юнікод ускладнює порівняння рядків, тому що той самий набір символів може бути представлений різними послідовностями кодових точок. Наприклад, така літера, як «ê», може бути представлена як одна кодова точка 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'

A second tool is the unicodedata module’s normalize() function that converts strings to one of several normal forms, where letters followed by a combining character are replaced with single characters. normalize() can be used to perform string comparisons that won’t falsely report inequality if two strings use combining characters differently:

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))

This will print True. (Why is NFD() invoked twice? Because there are a few characters that make casefold() return a non-normalized string, so the result needs to be normalized again. See section 3.13 of the Unicode Standard for a discussion and an example.)

Регулярні вирази 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 відповідатиме або пробілам Unicode, або [ \t\n\r\f\v].

Список літератури

Деякі хороші альтернативні обговорення підтримки Unicode в Python:

Тип str описано в довідці про бібліотеку Python за адресою Тип текстової послідовності — str.

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

Документація для модуля codecs.

Марк-Андре Лембург провів презентацію під назвою «Python і Юнікод» (PDF-слайди) на EuroPython 2002. Слайди є чудовим оглядом дизайну функцій Юнікоду Python 2 (де тип рядка Юнікод називається unicode і літерали починаються з u).

Читання та запис даних Unicode

Після того, як ви написали код, який працює з даними Unicode, наступною проблемою є введення/виведення. Як отримати рядки Unicode у вашій програмі та як перетворити Unicode у форму, придатну для зберігання чи передачі?

Цілком можливо, що вам може не знадобитися нічого робити залежно від ваших джерел введення та призначення виводу; вам слід перевірити, чи бібліотеки, які використовуються у вашій програмі, підтримують Unicode. Синтаксичні аналізатори XML часто повертають, наприклад, дані Unicode. Багато реляційних баз даних також підтримують стовпці зі значеннями Юнікод і можуть повертати значення Юнікод із запиту SQL.

Дані Unicode зазвичай перетворюються в певне кодування перед записом на диск або надсиланням через сокет. Можна виконати всю роботу самостійно: відкрити файл, прочитати з нього 8-бітний об’єкт bytes і перетворити байти за допомогою 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 на початку файлу; коли використовується таке кодування, специфікація буде автоматично записана як перший символ і буде мовчки відкинута під час читання файлу. Існують варіанти цих кодувань, наприклад «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() повертає кодування для використання у вашій поточній системі, якщо ви хочете зробити кодування вручну, але немає особливих причин турбуватися. Відкриваючи файл для читання або запису, ви зазвичай можете просто вказати рядок Юнікод як ім’я файлу, і він буде автоматично перетворений у правильне для вас кодування:

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, декодуючи вхідні дані якнайшвидше та кодуючи вихід лише в кінці.

Якщо ви спробуєте написати функції обробки, які приймають як Юнікод, так і байтові рядки, ви побачите, що ваша програма вразлива до помилок, коли б ви не поєднували два різних типи рядків. Немає автоматичного кодування або декодування: якщо ви робите, напр. 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 використовується для кодування даних і їх зворотного запису.

Список літератури

One section of Mastering Python 3 Input/Output, a PyCon 2010 talk by David Beazley, discusses text processing and binary data handling.

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

The Guts of Unicode in Python is a PyCon 2013 talk by Benjamin Peterson that discusses the internal Unicode representation in Python 3.3.

Подяки

Початковий проект цього документа був написаний Ендрю Кухлінгом. Відтоді він був додатково переглянут Олександром Бєлопольським, Георгом Брандлом, Ендрю Кухлінгом та Еціо Мелотті.

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