[Из песочницы] Автоопределение кодировки текста

image

Введение

Я очень люблю программировать, я любитель и первый и последний раз заработал на программировании в далёком 1996 году. Но для автоматизации повседневных задач иногда что-то пишу. Примерно год назад открыл для себя golang. В качестве инструмента создания утилит golang оказался очень удобным. Итак.

Возникла потребность обработать большое количество (больше тысячи, так и вижу улыбки профи) архивных файлов со специальной геофизической информацией. Формат файлов текстовый, простой. Если вдруг интересно то это LAS формат.
LAS файл содержит заголовок и данные.

Данные практически CSV, только разделитель табуляция или пробелы.

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

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

В итоге изобрёл велосипед на golang и соответственно родилась маленькая библиотечка с возможностью детектировать кодовую страницу.

Про кодировки. Не так давно на хабре была хорошая статья про кодировки Как работают кодировки текста. Откуда появляются «кракозябры». Принципы кодирования. Обобщение и детальный разбор Если хочется понять что такое «кракозябры» или «кости» то стоит прочитать.

В начале я накидал своё решение. Потом пытался найти готовое работающее решение на golang, но не вышло. Нашлось два решения, но оба не работают.


  • Первое «из коробки»— golang.org/x/net/html/charset функция DetermineEncoding ()
  • Второе библиотека — saintfish/chardet на github

Обе уверенно ошибаются на некоторых кодировках. Стандартная та вообще почти ничего определить не может по текстовым файлам, оно и понятно, её для html страниц делали.

При поиске часто натыкался на готовые утилиты из мира linux — enca. Нашёл её версию скомпилированную для WIN32, версия 1.12. Её я тоже рассмотрю, там есть забавности. Я прошу сразу прощения за своё полное незнание linux, а значит возможно есть ещё решения которые тоже можно попытаться прикрутить к golang коду, я больше искать не стал.


Сравнение найденных решений на автоопределение кодировки

Подготовил каталог softlandia\cpd тестовые данные с файлами в разных кодировках. Содержимое файлов очень короткое и одинаковое. Одна строка «Русский в кодировке CodePageName». Дополнил файлами со смешением кодировок и некоторыми сложными случаями и попробовал определить.

Мне кажется получилось забавно.


Наблюдение 1

enca не определила кодировку у файла UTF-16LE без BOM — это странно, ну ладно. Я попробовал добавить больше текста, но результата не получил.


Наблюдение 2. Проблемы с кодировками CP1251 и KOI8-R

Строка 15 и 16. У команды enca есть проблемы.
Здесь сделаю объяснение, дело в том, что кодировки CP1251 (она же Windows 1251) и KOI8-R очень близки если рассматривать только алфавитные символы.


Таблица CP 1251

image


Таблица KOI8-r

image

В обеих кодировках алфавит расположен от 0xC0 до 0xFF, но там где у одной кодировки заглавные буквы у другой строчные. Судя по всему enca работает по строчным буквам. Вот и получается, если подать на вход программе enca строку «СТП» в кодировке CP1251, то она решит, что это строка «яро» в кодировке KOI8-r, о чём и сообщит. В обратную сторону также работает.


Наблюдение 3

Стандартной библиотеке html/charset можно доверить только определение UTF-8, но осторожно! Пользоваться следует именно charset.DetermineEncoding (), поскольку метод utf8.Valid (b []byte) на файлах в кодировке utf-16be возвращает true.


Собственный велосипед

image

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

Для меня такая цель не стояла. Мне достаточно определять кодировки в предположении что там есть русский язык. И второе, определять нужно по небольшому количеству символов — на 10 символах должно быть достаточно уверенное определение, а желательно вообще на 5–6 символах.


Алгоритм

Когда я обнаружил совпадение кодировок KOI8-r и CP1251 по местоположению алфавита, то на пару дней загрустил… стало понятно, что чуть-чуть придётся подумать. Получилось так.

Основные решения:


  1. Работу будем вести со слайсом байтов, для совместимости с charset.DetermineEncoding ()
  2. Кодировку UTF-8 и случаи с BOM проверяем отдельно
  3. Входные данные передаём по очереди каждой кодировке. Каждая сама вычисляет два целочисленных критерия. У кого сумма двух критериев больше, тот и выиграл.


Критерии соответствия


Первый критерий

Первым критерием является количество самых популярных букв русского алфавита.

Наиболее часто встречаются буквы: о, е, а, и, н, т, с, р, в, л, к, м, д, п, у. Данные буквы дают 82% покрытия. Для всех кодировок кроме KOI8-r и CP1251 я использовал только первые 9 букв: о, е, а, и, н, т, с, р, в. Этого вполне хватает для уверенного определения.

А вот для KOI8-r и CP1251 пришлось доработать напильником. Коды некоторых из этих букв совпадают, например буква о имеет в CP1251 код 0xEE при этом в KOI8-r этот код у буквы н. Для этих кодировок были взяты следующие популярные буквы.


Второй критерий

К сожалению для очень коротких случаев (общая длина русского текста 5–6 символов) встречаемость популярных букв на уровне 1–3 шт и происходит нахлёст кодировок KOI8-r и CP1251. Пришлось вводить второй критерий. Подсчёт количества пар согласная+гласная.
Такие комбинации ожидаемо наиболее часто встречаются в русском языке и соответственно в той кодировке в которой число таких пар больше, та кодировка имеет больший критерий.

Вычисляются оба критерия, складываются и полученная сумма является итоговым критерием.
Результат отражен в таблице выше.


Особенности с которыми я столкнулся

Чуть коснусь прелестей и проблем связанных с golang. Раздел может быть интересен только начинающим писать на golang.


Проблемы

Лично походил по некоторым подводным камушкам из 50 оттенков Go: ловушки, подводные камни и распространённые ошибки новичков.
Что делать если интерфейс является входным параметром нашей функции? Например если мы принимаем io.Reader, проверить его на nil ведь надо. Проверить на существование переменной типа io.Reader мне удалось только с помощью рефлексии.

//CodePageDetect - detect code page of ascii data from reader 'r'
func CodePageDetect(r io.Reader, stopStr ...string) (IDCodePage, error) {
    if !reflect.ValueOf(r).IsValid() {
        return ASCII, fmt.Errorf("input reader is nil")
    }
...

Разок наступил на грабли с передачей массивов по значению. Немного тупанул на попытке изменять элементы хранящиеся в map пробегая по ним в range…


Прелести

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

Конечно очень приятно жить со сборщиком мусора. Полагаю мне ещё предстоит освоить грабли автоматизации выделения/освобождения памяти, но пока дебильная улыбка не покидает лица.
Строгая типизация — тоже кусочек счастья.

Переменные имеющие тип функции — соответственно лёгкая реализация различного поведения у однотипных объектов.

Странно мало пришлось сидеть в отладчике, перечитывание кода обычно быстро давало результат.

Щенячий восторг от наличия массы инструментов из коробки, это чудное ощущение когда компилятор, язык, библиотека и IDE Visual Studio Code работают на тебя вместе, слаженно.

© Habrahabr.ru