Эта статья про нюансы распознавания кириллицы в коде, и про волшебные файлы, которые не понимают, в какой они кодировке, и ломают работу в коде.

Чаще всего кодировки распознаются правильно. Но когда что-то пошло не так, в моей речи закрепилось слово “крокозябры” примерно на месяц.

В этой статье разберём реальный кейс:

  • как файл «притворялся» что он Macintosh (или Macintoch),

  • примеры, как библиотека Ude ошибалась с кодировкой (и не только она!). Честно - мы так до конца и не поняли, почему :)

  • какую проверку пришлось дописать поверх Encoding.GetEncoding(cdet.Charset);

Немного контекста

В ходе задачи наткнулись на странное поведение текстовых файлов.

По умолчанию файл в Notepad++ открывается в Macintosh, причём это выглядит, как будто это не его родная кодировка.
По умолчанию файл в Notepad++ открывается в Macintosh, причём это выглядит, как будто это не его родная кодировка.
Если сменить кодировку на Windows-1251, то текст начинает выглядеть нормально. Но почему он изначально открылся как Macintosh?
Если сменить кодировку на Windows-1251, то текст начинает выглядеть нормально. Но почему он изначально открылся как Macintosh?

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

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 отличаются набором потенциально используемых байтов. В каждой кодировке я обозначила зеленым, какие байты могут встречаться в русском тексте и их появление будет нормальным. Другие байты, в теории, тоже могут появляться, но мы не будем на них ориентироваться.

"Разрешённые символы" для MacCyrillic.
"Разрешённые символы" для MacCyrillic.
"Разрешённые символы" для Windows-1251
"Разрешённые символы" для Windows-1251

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

В Macintosh будет странно, если используют эти байты, но нормально для Windows-1251
В Macintosh будет странно, если используют эти байты, но нормально для Windows-1251
В Windows-1251 будет странно, если используют эти байты, но нормально для Macintosh
В Windows-1251 будет странно, если используют эти байты, но нормально для Macintosh

Так родились следующие массивы байтов:

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-ов на каждый случай.

А с пользователем пытались поговорить?

Пытались... "Я все сделал правильно, я не знаю, почему у меня создаётся такой файл". И живи с этим как хочешь.

Дополнительно мы смогли выработать алгоритм, по которому он должен проверять свои данные, которые он собирается загрузить. Логика ведь какая: если пользователь хочет, чтобы загрузились красивые данные, пусть предоставит красивые данные. А как сделать красивые данные из битых?

  1. Открыть файл в Notepad++

  2. Если видишь крокозябры и странную кодировку (например, Macintosh)- меняешь прямо в редакторе отображение файла (кодировку) на правильную. Например, на Windows-1251, или на UTF-8, или, по сути, на любую, в которой файл откроется в читабельном виде и не будет содержать крокозябр.

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

  4. Создаем новый файл через Notepad++ с правильной кодировкой.

  5. Вставляем в этот правильный файл честные скопированные данные и нажимаем сохранить. The end.

Самое обидное, что это не гарант успеха, потому что мы сталкивались (повторюсь) с файлом, который в Notepad++ открывается как Windows-1251 (и все нормально отображается), но библиотека Ude почему-то считает, что там Macintoch. То есть даже честная перепроверка пользователем своих же данных не дает 100% защиты от ошибки.

Итог?

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