Как стать автором
Обновить
3037.86
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Нельзя предполагать, что все используют UTF-8

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров17K
Автор оригинала: Cal Paterson
Как вычислять кодировку при помощи статистики

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

По крайней мере, в таком положении вещей есть, кого винить: Бога. Ведь именно он вынудил человечество говорить на разных языках из-за древнего спора о строительстве объекта недвижимости.

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

И одна из самых больших проблем одновременно является самой простой: компьютеры не договорились о том, как записывать буквы двоичным кодом.

Как записываются буквы двоичным кодом


Возьмём для примера символ латиницы «A». В American Standard Code for Information Interchange, или ASCII, ему назначено число 65. Такая нумерация была унаследована Unicode, только в Unicode число 65 записывается в шестнадцатеричном виде (U+0041). Такую запись называют «элементом кодового пространства» (codepoint).

Здесь всё довольно просто; по крайней мере, в вопросе числа, обозначающего «A», в целом есть консенсус. Но компьютеры не могут просто хранить десятичные числа, они хранят только двоичные.

В самой популярной кодировке символов UTF-8 номер символа 65 («A») записывается так:

01000001

Равны единице, или «включены» только второй и последний биты. Второй бит обозначает 64, а последний — 1. В сумме они дают 65. Всё очень просто.

Ещё одна популярная кодировка — это UTF-16, в основном применяемая в мире Windows, Java и
JavaScript. В UTF-16 число 65 записывается следующим образом:

01000001 00000000

Практически то же самое, только UTF-16 использует под каждый символ два полных байта (как минимум), но не требует дополнительных битов для описания 65, так что второй байт остаётся пустым.

А что там с другими кодировками? Вот лишь некоторые из наиболее популярных:

  • Win-1252 — не относящаяся к Unicode кодировка, применяется там, где говорят на европейских языках
  • KOI8 — не относящаяся к Unicode кодировка, используется там, где применяется кириллица
  • GB18030 — Unicode, но в основном применяется в континентальном Китае
  • Big5 — не относится к Unicode, широко используется там, где применяются традиционные китайские иероглифы
  • Shift_JIS — не относится к Unicode, используется в Японии

Все эти кодировки наследуют от букв ASCII, поэтому во всех них A записывается так:

01000001

Точно так же, как в UTF-8.

Очень удобно. Именно поэтому базовый западноевропейский алфавит читаем, даже когда остальная часть документа превращается в исковерканный хаос. Многие популярные кодировки (за исключением UTF-16) соответствуют ASCII, по крайней мере, для латиницы.

Пока всё неплохо. Но давайте теперь рассмотрим более сложный символ: знак евро, €. Консорциум Unicode обозначил его числом 8364 (U+20AC).

В UTF-8 число 8364 представляется в следующем виде:

11100010 10000010 10101100

Обратите внимание, что в UTF-8 оно занимает три байта. UTF-8 — это кодировка символов с «переменной длиной»: чем больше число Unicode, тем больше байтов требуется. (На самом деле это справедливо и для UTF-16, но встречается реже.)

Однако в UTF-16 число 8364 кодируется совершенно иначе:

10101100 00100000

Win-1252 не следует стандарту Unicode. В этой кодировке знак евро имеет номер 128. И кодировка записывает 128 вот так:

10000000

То есть лишь один включённый бит равен 128.

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

невозможно никак представить в KOI8.

В GB18030 символ € кодируется так:

10100010 11100011

В Big5 символ € выглядит так:

10100011 11100001

В Shift JIS это

10000000 00111111

Абсолютно разные и совершенно несовместимые. Если автоматически предполагать, что используется UTF-8, то мы получим полную чушь.

Как определить, какая кодировка используется?


Некоторые форматы сами задают кодировку, например, JSON обязует применять UTF-8. Это сильно упрощает жизнь — если ты знаешь, что данные записаны в JSON, то они должны быть закодированы в UTF-8.

В других случаях можно передать кодировку отдельно. HTTP позволяет помещать кодировку в заголовок Content-Type:

Content-type: text/html; charset=ISO-8859-1

А у некоторых форматов есть внутренние способы указания кодировки. Например, у некоторых текстовых файлов есть заголовок:

# -*- encoding: utf-16be -*-

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

Ну а если у данных нет никаких меток?

Или если метка ошибочна? Как будет показано ниже, встречается такое довольно часто.

В частности, у файлов CSV нет внутреннего способа информирования о том, какая кодировка в них использована. В них нельзя поместить комментарий, потому что для этого нет места, и программы чтения csv чаще всего не смогут распарсить ваш csv. А многие популярные инструменты, работающие с файлами CSV (MS Excel), не используют UTF-8.

Что будет тогда?

Решением будет статистика.

Определение кодировки при помощи статистики


Существует две базовые стратегии для определения кодировки неразмеченной строки текста.

  1. На уровне байтов
  2. На уровне символов

В большинстве реализаций сначала используется уровень байтов, а при необходимости уровень символов.

▍ Эвристики уровня байтов


На уровне байтов всё довольно просто. Достаточно взглянуть на байты и понять, выглядят ли они похожими на конкретную кодировку символов.

Например, как я говорил выше, в UTF-16 используется по два байта на символ (чаще всего). В случае текста на латинице (например, английского) обычно появляется множество «пустых» вторых байтов. К счастью, во многих языках разметки активно используется латиница (например, <, >, [, ] и так далее), даже если сам документ составлен не на латинице. Если строка текста содержит множество пустых вторых байтов, то есть вероятность, что это UTF-16.

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

00111100 00100001

Если бы это был UTF-16, то эти два байта оказались бы символом ℼ, имеющим название double struck small pi и номер 8508 (U+213C) в Unicode. Часто ли этот символ первым встречается в файле HTML?

Или более вероятно, что это двухсимвольная последовательность <! в кодировке UTF-8? Возможно, следующие байты — это doctype> или стандартный бойлерплейт любого документа HTML?

Ещё одна подсказка — это конкретные байты. Как ни ужасно, UTF-16 имеет две версии: в одной биты записываются обычным образом, в другой — в обратную сторону. Чтобы люди различали эти две версии, в стандарте UTF-16 есть маркер последовательности байтов (byte order mark), который можно поместить перед текстовым потоком, чтобы обозначить используемую версию. Эта пара байтов редко встречается в других кодировках, и практически никогда не бывает в начале, так что они становятся хорошей подсказкой о том, что идёт за ними.

Итак, байты могут дать нам довольно много информации о кодировке. Если вы можете с их помощью однозначно определить UTF-8 или UTF-16, то наша задача выполнена.

▍ Эвристики уровня символов


Сложности возникают с однобайтовыми кодировками, не относящимися к Unicode. Например, сложно отличить Win-1252 от KOI8, ведь для кодирования разных вещей и та, и другая используют обычно пустой первый бит ASCII.

Как же их отличить? Благодаря частотному анализу. Мы смотрим на буквы, которые могли бы присутствовать в документе, например, если это KOI8, и задаёмся вопросом: «Действительно ли это типичное распределение букв для документа на кириллице?».

Вот базовый алгоритм:

  • Исключаем все кодировки, отсечённые предыдущими эвристиками уровня байтов
  • Для каждой оставшейся возможной кодировки X:
    • Парсим входные данные, как будто они были закодированы X
    • Сравниваем частотность символов в строке со значениями в известной таблице частотности
    • Опционально также сравниваем пары букв (например, qu) с известной таблицей частотности
    • Если они достаточно хорошо совпадают, то возвращаем X

  • В противном случае возвращаем ошибку

Часто таким образом можно также определить, на каком языке написан этот документ — именно благодаря этому веб-браузеры открывают диалоговое окно «Перевести эту страницу?».

Действительно ли это работает?


Обычно люди не очень любят эвристики, но ответом является «да». Это работает, и на удивление хорошо. И намного лучше, чем просто предположения о том, что текст закодирован UTF-8 (в конечном итоге это и является бенчмарком).

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

Эвристики тоже важны, потому что люди понимают кодировки неправильно.

Может показаться логичным, что если вы экспортируете лист Excel в файл csv в последней версии MS Excel, то получите UTF-8. Ну, или, возможно, UTF-16. Но вы ошибётесь. По умолчанию, в большинстве конфигураций Excel сохраняет CSV в кодировке Win-1252.

Win-1252 — это однобайтная кодировка, не относящаяся к Unicode. Это расширение ASCII, засовывающее в неиспользованный восьмой бит достаточно большое количество символов для почти каждого европейского языка. Обычный пользователь Excel никогда о ней не слышал, если вообще слышал о кодировках символов. Во многой мудрости много печали.

Дополнительные источники


Вероятно, основная часть кода определения кодировок работает на принципах, заложенных Netscape в начале 2000-х. Статья с описанием этого подхода есть в архиве Mozilla.

У меня есть чёткое впечатление, что автоматическое определение кодировки текста — это частный случай закона Постела: «будь консервативным в том, что делаешь, будь либерален в том, что принимаешь от других». Я всегда воспринимал закон Постела как что-то истинное, но сейчас у меня возникает всё больше сомнений. Возможно, механизм автоматического определения кодировки в моей базе данных csvbase стоит сделать частью пользовательского интерфейса, а не заранее выбранным пунктом выпадающего списка.

Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
Теги:
Хабы:
+69
Комментарии92

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds