Эта статья про нюансы распознавания кириллицы в коде, и про волшебные файлы, которые не понимают, в какой они кодировке, и ломают работу в коде.
Чаще всего кодировки распознаются правильно. Но когда что-то пошло не так, в моей речи закрепилось слово “крокозябры” примерно на месяц.
В этой статье разберём реальный кейс:
как файл «притворялся» что он Macintosh (или Macintoch),
примеры, как библиотека Ude ошибалась с кодировкой (и не только она!). Честно - мы так до конца и не поняли, почему :)
какую проверку пришлось дописать поверх
Encoding.GetEncoding(cdet.Charset);
Немного контекста
В ходе задачи наткнулись на странное поведение текстовых файлов.


В коде кодировка тоже автоматически распознавалась неправильно.
public static Encoding GetFileEncoding(string filename) { Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); using (FileStream fs = File.OpenRead(filename)) { Ude.CharsetDetector cdet = new Ude.CharsetDetector(); cdet.Feed(fs); cdet.DataEnd(); if (cdet.Charset != null) { // Здесь возвращалась Macintoch return Encoding.GetEncoding(cdet.Charset); } } return Encoding.GetEncoding(CodePages.Windows1251); }
И потом мы радостно шли менять файл с неподходящей для нашей работы кодировкой на Windows-1251. Но это работало неправильно.
// на вход приходит midFileEncoding = Macintoch private static void ChangeEncodingForMidFile(string midFile, Encoding midFileEncoding, string newFullMidFileName) { // Мы собираемся читать файл, думая, что он в кодировке Macintoch using StreamReader reader = new StreamReader(midFile, midFileEncoding); // Мы хотим записать в новый файл с правильной кодировкой, а старый файл просто удалим using StreamWriter writer = new StreamWriter( newFullMidFileName, false, Encoding.GetEncoding(CodePages.Windows1251) ); // но когда мы его читаем - мы читаем его вместе со всеми крокозябрами, // которые мы видели в Notepad++ // и вместе с этими же крокозябрами записываем в новый файл writer.Write(reader.ReadToEnd()); File.Delete(midFile); // теперь у нас новый файл в кодировке Windows-1251, в котором лежат крокозябры. Ура! File.Move(newFullMidFileName, midFile); }
То есть из-за неправильный вычитки кодировки никакой нормальной конвертации не произошло. Прочитали неправильно и неправильно записали. И что делать? Как заставить библиотеку работать правильно?
Неизвестно, как такой файл вообще был создан, в какой операционной системе и каком редакторе, и почему Ude считает, что кодировка Macintoch. В другом файле вообще столкнулись с тем, что в Notepad++ открывается как Windows-1251 (и все нормально отображается), но библиотека Ude почему-то считает, что там Macintoch. Тут у нас вообще крыша поехала.
Решение проблемы
Был написан немного костыльный метод. Поскольку проблема была только с отображением русского текста, без всяких сложных символов, мы смогли себе позволить это решение. Если бы встречались очень сложные символы - было бы сложнее.
Итак, кодировки MacCyrillic и Windows-1251 отличаются набором потенциально используемых байтов. В каждой кодировке я обозначила зеленым, какие байты могут встречаться в русском тексте и их появление будет нормальным. Другие байты, в теории, тоже могут появляться, но мы не будем на них ориентироваться.


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


Так родились следующие массивы байтов:
private static readonly ArrayList<byte> StrangeForMacintochBytes = new ArrayList<byte> { 0xAB, 0xB8, 0xB9, 0xBB, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC9, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xFF }; private static readonly ArrayList<byte> StrangeForWindows1251Bytes = new ArrayList<byte> { 0x80, 0x81, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F };
И был написан код, который "активируется" при распознавании кодировки Macintoch библиотекой Ude.
// дополнили метод получения кодировки if (cdet.Charset != null) { // допустим, здесь возвращается Macintoch var encoding = Encoding.GetEncoding(cdet.Charset); // а мы пойдём и перепроверим для этих странных кодировок if (encoding.CodePage == CodePages.Macintoch || encoding.CodePage == CodePages.MacCyrillic) return CustomGetEncoding(encoding, filename); }
CustomGetEncoding живёт теперь примерно с вот такой логикой:
Если в потоке байтов встречается байт из множества странных для Macintosh,
то это не кодировка Macintosh.
Вернуть кодировку Windows-1251.
Иначе
Если в потоке байтов встречается байт из множества странных для Windows-1251
то это не кодировка Windows-1251
Вернуть кодировку Macintosh.
Если нет никаких странных символов, вернуть Windows-1251, ибо разницы в чтении текста в разных кодировках тогда быть не должно.
В чем недостатки?
Во-первых, ну мягко говоря неприятно писать код, который должна уметь делать библиотека. Код слишком специфичный, узконаправленный, нацелен на русский текст, какой-то он весь из себя костыльный...
Во-вторых - по сути такое могло произойти и в обратную сторону. Что если на самом деле MacCyrillic, а распозналась как Windows-1251? А как там поживает UTF-8? А остальные 100500 кодировок? Ну бред же, писать пятьдесят if-ов на каждый случай.
А с пользователем пытались поговорить?
Пытались... "Я все сделал правильно, я не знаю, почему у меня создаётся такой файл". И живи с этим как хочешь.
Дополнительно мы смогли выработать алгоритм, по которому он должен проверять свои данные, которые он собирается загрузить. Логика ведь какая: если пользователь хочет, чтобы загрузились красивые данные, пусть предоставит красивые данные. А как сделать красивые данные из битых?
Открыть файл в Notepad++
Если видишь крокозябры и странную кодировку (например, Macintosh)- меняешь прямо в редакторе отображение файла (кодировку) на правильную. Например, на Windows-1251, или на UTF-8, или, по сути, на любую, в которой файл откроется в читабельном виде и не будет содержать крокозябр.
Это действие не меняет кодировку файла - оно меняет только отображение. Когда получили нормальный читабельный текст, мы его копируем.
Создаем новый файл через Notepad++ с правильной кодировкой.
Вставляем в этот правильный файл честные скопированные данные и нажимаем сохранить. The end.
Самое обидное, что это не гарант успеха, потому что мы сталкивались (повторюсь) с файлом, который в Notepad++ открывается как Windows-1251 (и все нормально отображается), но библиотека Ude почему-то считает, что там Macintoch. То есть даже честная перепроверка пользователем своих же данных не дает 100% защиты от ошибки.
Итог?
Костыльный эвристический подход спас пользователей и техническую поддержку, но режет глаза в коде и вызывает вопросы. Во время разработки даже успели попробовать какие-то другие библиотеки и они работали не лучше. Автоматическое определение кодировки, по всей видимости, это вероятности, основанные на том, какие байты попадаются в тексте, и поэтому шанс ошибки всегда присутствует.
