![Нельзя предполагать, что все используют UTF-8 - 1 Нельзя предполагать, что все используют UTF-8 - 1](https://www.pvsm.ru/images/2024/05/03/nelzya-predpolagat-chto-vse-ispolzuyut-UTF-8.jpg)
Как вычислять кодировку при помощи статистики
Люди говорят на бесчисленном количестве разных языков. Эти языки не только несовместимы между собой, но и представляют огромную трудность при транспиляции в среде исполнения. К сожалению, все попытки стандартизации провалились.
По крайней мере, в таком положении вещей есть, кого винить: Бога. Ведь именно он вынудил человечество говорить на разных языках из-за древнего спора о строительстве объекта недвижимости.
Однако человечество может винить себя за то, что сложности в общении испытывают компьютеры.
И одна из самых больших проблем одновременно является самой простой: компьютеры не договорились о том, как записывать буквы двоичным кодом.
Как записываются буквы двоичным кодом
Возьмём для примера символ латиницы «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.
Что будет тогда?
Решением будет статистика.
Определение кодировки при помощи статистики
Существует две базовые стратегии для определения кодировки неразмеченной строки текста.
- На уровне байтов
- На уровне символов
В большинстве реализаций сначала используется уровень байтов, а при необходимости уровень символов.
▍ Эвристики уровня байтов
На уровне байтов всё довольно просто. Достаточно взглянуть на байты и понять, выглядят ли они похожими на конкретную кодировку символов.
Например, как я говорил выше, в 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 стоит сделать частью пользовательского интерфейса, а не заранее выбранным пунктом выпадающего списка.
Автор: ru_vds