вторник, 28 ноября 2006 г.

Важное дополнение к статье о трансляции кодировок — HTML character entities.

В предыдущей статье я показал, как можно написать middleware для Django, которая транслирует данные запроса из "внешней" кодировки во "внутреннюю" и обратно. Обсуждение статьи в LiveJournal вскрыло серьезный недостаток первого подхода к решению данной проблемы.

Этот недостаток заключался в том, что приведенный код не учитывал той особенности, что пространство кодировки UTF-8 гораздо шире, чем пространство однобайтовой кодировки, такой как Windows-1251. В запросе могут прийти символы, которые, не являясь символами других, нецелевых национальных кодировок (о поддержке которых я речи тут не веду), тем не менее вполне легально могут присутствовать в тексте запроса - они могут попасть туда при копировании текста из приложений, свободно работающих с данными в Unicode, таких как MS Word. При попытке транслировать такие символы в однобайтовую кодировку возникнет исключение UnicodeDecodeError, и дальнейшая работа с запросом станет невозможна.

Для решения этой проблемы в стандарте HTML был определен список таких символов и порядок работы с ними. В том случае, когда браузер отправляет данные в однобайтовой кодировке, и должны быть переданы символы из списка, эти символы кодируются в числовые сущности (numeric character entities). Так, например, символ "»" (соответствующий позиции 187 в Unicode - U+00BB) должен быть закодирован как ». В случае, если браузер считает, что сервер в состоянии обработать многобайтную кодировку, такую как UTF-8 (обычно это значит, что страница, с которой отправляются данные формы, была отдана браузеру в этой кодировке), то данная процедура не применяется.

Соответственно, если наша внутренняя кодировка — однобайтная, нам нужно преобразовывать данные на сервере, чтобы мы смогли принять эти символы, хотя и в виде character entities.
Для начала, определим массив целых чисел, соответствующих Unicode-позициям символов.

[python]
def makeCodes():
eCodes = range(160, 256) # Latin-1 chars
eCodes += range(913, 938) # greek capital
eCodes += range(945, 983) # greek small
eCodes += [402, 8226, 8230, 8242, 8243, 8254, 8260, 8472, # symbols, math
8465, 8476, 8482, 8501, 8592, 8593, 8594, 8595,
8596, 8629, 8656, 8657, 8658, 8659, 8660, 8704,
8706, 8707, 8709, 8711, 8712, 8713, 8715, 8719,
8721, 8722, 8727, 8730, 8733, 8734, 8736, 8743,
8744, 8745, 8746, 8747, 8756, 8764, 8773, 8776,
8800, 8801, 8804, 8805, 8834, 8835, 8836, 8838,
8839, 8853, 8855, 8869, 8901, 8968, 8969, 8970,
8971, 9001, 9002, 9674, 9824, 9827, 9829, 9830]
eCodes += [338, 339, 352, 353, 376, 710, 732, #markup, international; minus amp, lt, gt, quot
8194, 8195, 8201, 8204, 8205, 8206, 8207,
8211, 8212, 8216, 8217, 8218, 8220, 8221, 8222,
8224, 8225, 8240, 8249, 8250, 8364]
return eCodes
[/python]

Данная функция вернет нам такой массив в соответствие co спецификацией. Из массива я исключил позиции 34, 38, 60 и 62, соответствующие символам ", &, < и >, которые есть в любой кодировке и должны передаваться как есть (в спецификации они указаны, т.к. в XML или HTML-документе они имеют специальное назначение, и entities нужны чтобы показать их как есть).

Далее определяем регулярные выражения и функции для трансляции Unicode-символов в entities и обратно.

[python]
import re
eCodes = makeCodes()
ESCAPE_RE = re.compile( u'([%s])' % u''.join( [unichr(x) for x in eCodes] ) )
UNESCAPE_RE = re.compile( '(%s)' % '|'.join( [('&#%d;' % x) for x in eCodes] ) )

def entEscape(s):
return ESCAPE_RE.sub(lambda x: u'&#%d;' % ord( x.groups()[0] ), s)
def entUnescape(s):
return UNESCAPE_RE.sub(lambda x: unichr( int( x.groups()[0][2:-1] ) ), s)
[/python]

Функция entEscape превратит "сырые" символы в тексте в character entities. entUnescape производит обратное превращение.

Метод tr класса TranslationMiddleware будет выглядеть так:

[python]
ESCAPE_ENTITIES = True
UNESCAPE_ENTITIES = True

class TranslationMiddleware(object):
..................
def tr(self, data, _from, _to):
if _to == _from:
return data

# make unicode data
data = data.decode(_from)

if ESCAPE_ENTITIES and _from.lower().startswith('utf'):
data = entEscape(data)

if UNESCAPE_ENTITIES and _to.lower().startswith('utf'):
data = entUnescape(data)

data = data.encode(_to)

return data
[/python]

В нем мы сначала переводим данные в Unicode, затем, если мы транслируем из многобайтной кодировки, то меняем символы на entities, а если — в многобайтную, производим обратное преобразование.

Для включения трансляции служат переменные ESCAPE_ENTITIES и UNESCAPE_ENTITIES. ESCAPE_ENTITIES обычно отключать не следует (если вы не хотите, чтобы запрос завершался ошибкой в том случае, если приедут данные не из пространства вашей однобайтной кодировки). UNESCAPE_ENTITIES можно отключить, тогда entities не будут преобразованы обратно в символы. Браузер в любом случае отрисует нужные символы на экране. Однако, если клиентом вашего приложения является что-то, что не имеет представления о тонкостях стандарта HTML, и ждет конкретных символов UTF-8, то обратную перекодировку следует включить.

Замечу, что обратная перекодировка касается только сущностей, объявленных как numeric entities. Так, если у вас в тексте будет сохранено значение », то оно перекодировано в символ не будет, тогда как его эквивалент » будет перекодирован.

2 комментария:

  1. Хм. Отэкранировать забыл. Там где вывод после .encode, стоит
    '«broken»'.

    ОтветитьУдалить
  2. Да что такое. Экранировал, а он опять. В общем, там стоит &_#171; и &_#187;

    ОтветитьУдалить

Постоянные читатели