Перекодировка текста UTF-8 и WINDOWS-1251
Проблема кодировок часто возникает при написании парсеров, чтении данных из xml и CSV файлов. Ниже представлены способы эту проблему решить.
windows-1251 в UTF-8
UTF-8 в windows-1251
Когда ни что не помогает
Иногда доходит до бреда, но работает:
File_get_contents / CURL
Ещё бывают случаи, когда file_get_contents() возвращает текст в виде:
Это сжатый текст в GZIP, т.к. функция не отправляет правильные заголовки. Решение проблемы через CURL:
Изменение кодировки в Microsoft Excel
С потребностью менять кодировку текста часто сталкиваются пользователи, работающие браузерах, текстовых редакторах и процессорах. Тем не менее, и при работе в табличном процессоре Excel такая необходимость тоже может возникнуть, ведь эта программа обрабатывает не только цифры, но и текст. Давайте разберемся, как изменить кодировку в Экселе.
Работа с кодировкой текста
Кодировка текста – эта набор электронных цифровых выражений, которые преобразуются в понятные для пользователя символы. Существует много видов кодировки, у каждого из которых имеются свои правила и язык. Умение программы распознавать конкретный язык и переводить его на понятные для обычного человека знаки (буквы, цифры, другие символы) определяет, сможет ли приложение работать с конкретным текстом или нет. Среди популярных текстовых кодировок следует выделить такие:
- Windows-1251;
- KOI-8;
- ASCII;
- ANSI;
- UKS-2;
- UTF-8 (Юникод).
Последнее наименование является самым распространенным среди кодировок в мире, так как считается своего рода универсальным стандартом.
Чаще всего, программа сама распознаёт кодировку и автоматически переключается на неё, но в отдельных случаях пользователю нужно указать приложению её вид. Только тогда оно сможет корректно работать с кодированными символами.

Наибольшее количество проблем с расшифровкой кодировки у программы Excel встречается при попытке открытия файлов CSV или экспорте файлов txt. Часто, вместо обычных букв при открытии этих файлов через Эксель, мы можем наблюдать непонятные символы, так называемые «кракозябры». В этих случаях пользователю нужно совершить определенные манипуляции для того, чтобы программа начала корректно отображать данные. Существует несколько способов решения данной проблемы.
Способ 1: изменение кодировки с помощью Notepad++
К сожалению, полноценного инструмента, который позволял бы быстро изменять кодировку в любом типе текстов у Эксель нет. Поэтому приходится в этих целях использовать многошаговые решения или прибегать к помощи сторонних приложений. Одним из самых надежных способов является использование текстового редактора Notepad++.
- Запускаем приложение Notepad++. Кликаем по пункту «Файл». Из открывшегося списка выбираем пункт «Открыть». Как альтернативный вариант, можно набрать на клавиатуре сочетание клавиш Ctrl+O.

- Запускается окно открытия файла. Переходим в директорию, где расположен документ, который некорректно отобразился в Экселе. Выделяем его и жмем на кнопку «Открыть» в нижней части окна.
Несмотря на то, что данный способ основан на использовании стороннего программного обеспечения, он является одним из самых простых вариантов для перекодировки содержимого файлов под Эксель.

Способ 2: применение Мастера текстов
Кроме того, совершить преобразование можно и с помощью встроенных инструментов программы, а именно Мастера текстов. Как ни странно, использование данного инструмента несколько сложнее, чем применение сторонней программы, описанной в предыдущем методе.

- Запускаем программу Excel. Нужно активировать именно само приложение, а не открыть с его помощью документ. То есть, перед вами должен предстать чистый лист. Переходим во вкладку «Данные». Кликаем на кнопку на ленте «Из текста», размещенную в блоке инструментов «Получение внешних данных».
- Открывается окно импорта текстового файла. В нем поддерживается открытие следующих форматов:
- TXT;
- CSV;
- PRN.
- Общий;
- Текстовый;
- Дата;
- Пропустить столбец.
Способ 3: сохранение файла в определенной кодировке
Бывает и обратная ситуация, когда файл нужно не открыть с корректным отображением данных, а сохранить в установленной кодировке. В Экселе можно выполнить и эту задачу.
- Переходим во вкладку «Файл». Кликаем по пункту «Сохранить как».

- Открывается окно сохранения документа. С помощью интерфейса Проводника определяем директорию, где файл будет храниться. Затем выставляем тип файла, если хотим сохранить книгу в формате отличном от стандартного формата Excel (xlsx). Потом кликаем по параметру «Сервис» и в открывшемся списке выбираем пункт «Параметры веб-документа».

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

- Возвращаемся в окно «Сохранения документа» и тут жмем на кнопку «Сохранить».
Документ сохранится на жестком диске или съемном носителе в той кодировке, которую вы определили сами. Но нужно учесть, что теперь всегда документы, сохраненные в Excel, будут сохраняться в данной кодировке. Для того, чтобы изменить это, придется опять заходить в окно «Параметры веб-документа» и менять настройки.
Существует и другой путь к изменению настроек кодировки сохраненного текста.
- Находясь во вкладке «Файл», кликаем по пункту «Параметры».

- Открывается окно параметров Эксель. Выбираем подпункт «Дополнительно» из перечня расположенного в левой части окна. Центральную часть окна прокручиваем вниз до блока настроек «Общие». Тут кликаем по кнопке «Параметры веб-страницы».

- Открывается уже знакомое нам окно «Параметры веб-документа», где мы проделываем все те же действия, о которых говорили ранее.
Теперь любой документ, сохраненный в Excel, будет иметь именно ту кодировку, которая была вами установлена.
Как видим, у Эксель нет инструмента, который позволил бы быстро и удобно конвертировать текст из одной кодировки в другую. Мастер текста имеет слишком громоздкий функционал и обладает множеством не нужных для подобной процедуры возможностей. Используя его, вам придется проходить несколько шагов, которые непосредственно на данный процесс не влияют, а служат для других целей. Даже конвертация через сторонний текстовый редактор Notepad++ в этом случае выглядит несколько проще. Сохранение файлов в заданной кодировке в приложении Excel тоже усложнено тем фактом, что каждый раз при желании сменить данный параметр, вам придется изменять глобальные настройки программы.
Как перекодировать кракозябры при ошибке конвертации utf 8 сз1251
utf8_1251.exe test_utf8.xml test.xml
Программа изменяет объявление кодировки в первой строке XML.
Перекодирование из Windows-1251 в UTF-8
Перекодирует XML-файл из кодировки Windows-1251 в UTF8.
Если файл уже в кодировке UTF8, то он остается как есть.
Если файл не содержит маркера кодировки UTF8, то добавляет его.
Вызов win1251_utf8.exe Файл1.xml Файл2.xml
Добавление маркера в UTF-8
Используйте ту же самую программу win1251_utf8.exe для добавления маркера UTF-8.
Если кодировка уже была UTF-8, и был маркер, то файл не изменится.
Практическое руководство по Unicode’изации

Мы, наконец, это сделали! Долгое время позорное наследие CP1251 раздражало разработчиков, наводило на мысли о том, что, как же так? Эпоха Unicode уже давно наступила, а мы все еще используем однобайтовую кодировку и расставляем в разных местах костыли для совместимости с внешними системами. Но причина тому была достаточно рациональная: перевести на Unicode большой проект, в который развился Мой Мир, очень трудоемко. Мы оценивали это в полгода и не были готовы тратить столько ресурсов на фичу, которая не принесет русскоязычной аудитории существенной пользы.
Но история вносит свои коррективы, зачастую весьма неожиданные. Не секрет, что в Казахстане весьма популярен проект Мой Мир, который является самой популярной социальной сетью в этой стране. И нам всегда хотелось, чтобы у наших казахских пользователей появилась возможность использовать символы казахского алфавита из расширенного кириллического набора, которым, к сожалению, не нашлось места в CP1251. И дополнительным стимулом для нас, позволившим, наконец, оправдать длительную разработку, стал дальнейший рост популярности проекта за пределами нашей страны. Мы поняли, что пора делать шаг навстречу зарубежным пользователям.
Разумеется, первое, что было необходимо для интернационализации проекта, это начать принимать, передавать, обрабатывать и хранить данные в UTF-8. Процедура эта для большого проекта непростая и длительная, по пути нам пришлось решить несколько достаточно интересных задач, про которые мы постараемся рассказать.
Перекодирование баз данных
Первый выбор, с которым мы столкнулись, был достаточно стандартным — с какого конца начинать: от отображения страницы или от хранилищ данных. Решили начать с хранилищ по причине того, что это самый затяжной и трудоемкий процесс, требующий слаженных действий разработчиков и администраторов.
Ситуацию осложняло еще и то, что в нашей социальной сети, из соображений быстродействия, используется большое количество разнообразных специализированных хранилищ. Разумеется, не все они содержали текстовые поля, подлежащие интернационализации, но все же их оказалось немало. И первое, что пришлось сделать — провести полную инвентаризацию всех наших хранилищ на предмет содержания в них текстовых строк. Стоит признать, что мы узнали много нового.
MySQL
Конвертацию хранилищ в UTF-8 мы начали с MySQL. Причиной этому послужило то, что, в общем-то, изменение кодировки этой базой поддерживается нативно. Но на практике все оказалось не так просто.
Во-первых, необходимо было осуществить конвертацию базы без даунтайма на время конвертации.
Во-вторых, выяснилось, что выполнить для всех таблиц alter table `my_table` convert to character set utf8; не рационально и, более того, невозможно. Не рационально потому, что индекс для UTF-8-поля всегда занимает 3 * length_in_characters байт, даже если поле содержит только ASCII-символы. А таких полей у нас оказалось немало, в том числе индексных, особенно таких, которые содержали hex-строки. Невозможно по причине того, что максимальная длина ключа индекса в MySQL 767 байт, и индексы (особенно многоколоночные) перестают помещаться. Помимо этого обнаружилось, что в текстовых полях кое-где по недосмотру хранятся бинарные данные и наоборот, и надо внимательно проверять каждое поле.
После того, как мы собрали с наших баз данных информацию об имеющихся там таблицах, появилось понимание, что львиная доля их, скорее всего, не используется. Так оно в результате и оказалось, мы удалили из баз примерно половину всех имевшихся в них таблиц. Для того чтобы найти неиспользуемые таблицы мы применили следующую технику: при помощи tcpdump собрали все запросы к нашим базам за сутки, затем пересекли список таблиц из этого дампа с текущей схемой баз и на всякий случай поискали неиспользуемые таблицы по коду (заодно подчистили код). Tcpdump применили потому, что он, в отличие от записи всех запросов в лог средствами MySQL, не требует перезапуска базы и не оказывает влияния на скорость обработки запросов. Разумеется, сразу удалять таблицы было страшно, поэтому сначала просто переименовали таблицы со специальным суффиксом, выждали несколько недель и потом удалили (кстати, не зря перестраховались, парочку лишних малоиспользуемых зацепили по недосмотру, пришлось вернуть).
- если в таблице не было текстовых полей, то (на всякий случай, вдруг когда-нибудь добавим) просто выполняли запрос: alter table `my_table` default character set utf8;
- если в таблице были только varchar текстовые поля требующие интернационализации, то: alter table `my_table` convert to character set utf8;
- поля, содержащие только ASCII-символы, конвертили в ASCII: alter table `my_table` modify `my_column` varchar(n) character set ascii …;
- поля, требующие интернационализации, стандартно: alter table `my_table` modify `my_column` varchar(n) character set utf8 …;
- но, для некоторых полей с уникальным индексом, из-за равенства в collation utf8_general_ci (в отличии от cp1251_general_ci) букв е и ё, пришлось костылить: alter table `my_table` modify `my_column` varchar(n) charater set utf8 collate utf8_bin …;
- для индексных полей, которые после конвертации перестали влезать в индекс, тоже пришлось костылить: alter table `my_table` drop index `my_index`, modify `my_column` varchar(n) character set utf8 …, add index `my_index`(`my_column`(m)); (где m < n, а индекс, как правило, по нескольким полям);
- текстовые поля, содержащие двоичные данные, переводили в binary и varbinary;
- двоичные поля, содержащие текстовые строки в CP1251, конвертировали в два приема: alter table `my_table` modify `my_column` varchar(n) character set cp1251; alter table `my_table` modify `my_column` varchar(n) character set utf8; Это необходимо, чтобы первым запросом MySQL понял, что данные в кодировке cp1251, а вторым сконвертировал в utf8.
- текстовые блобы пришлось обрабатывать отдельно, так как при convert to character set utf8 MySQL расширяет блоб до минимально необходимого, чтобы уместить текст максимальной длины, все символы которого трехбайтные. То есть text автоматически расширяется до mediumtext. Это не совсем то, чего мы хотели в ряде случаев, поэтому приводили явно: alter table `my_table` alter `my_column` text character set utf8;
- и, разумеется, на будущее финальный аккорд: alter database `my_database` default character set utf8;
- Переключаем мастер в режим statement-репликации.
- Поднимаем временную копию базы для конвертации, запускаем на ней конвертацию.
- По окончанию конвертации переводим копию в режим реплики от основной базы, данные догоняются, строки на лету также конвертируются.
- Для каждой реплики базы:
— переводим нагрузку с реплики на временную реплику в UTF-8;
— переливаем все реплики с нуля из временной базы, включаем репликацию с нее;
— возвращаем нагрузку обратно на реплику. - Переводим временную базу в режим мастера, перебрасываем запросы со старого мастера на временный при помощи NAT.
- Старый мастер переливаем из временной базы, догоняем репликацией.
- Переключаем мастер обратно, убираем NAT, возвращаем репликацию обратно в mixed.
- Отключаем временную базу.
Помимо конвертации собственно баз, понадобилось также впилить поддержку UTF-8 в коде, а также обеспечить плавный и незаметный переход. С MySQL тут все, правда, просто. Дело в том, что у него есть отдельно кодировка, в которой он хранит данные, и отдельно кодировка, в которой он данные отдает клиенту. Исторически на серверах у нас было прописано, что character_set_* = cp1251. Для параметров character_set_client, character_set_connection, character_set_results мы ничего менять не стали, чтобы не ломать старые клиенты, и оставили cp1251. Остальные заменили на utf8. В итоге старые клиенты, работающие в cp1251, по-прежнему получают данные в cp1251, независимо от того, отконвертирована база или нет, а новые, работающие в UTF-8, после установления соединения сразу же выполняют команду set names utf8; и начинают пользоваться всеми благами этой кодировки.
Tarantool
Что такое тарантул, думаю, уже можно не рассказывать. Это детище Моего Мира уже обрело достаточную известность и выросло в хороший open source проект.
Мы за годы его использования ухитрились накопить в нем огромное количество информации, и когда обнаружилось, что инстансов тарантула у нас под 400 штук, честно говоря, стало страшно, что конвертация затянется надолго. Но, к счастью для нас, выяснилось, что текстовые поля есть только в 60 из них (в основном это пользовательские профили).
Стоит признать, что перекодировать тарантулы оказалось действительно интересной задачей. И решение получилось достаточно элегантное. Но, разумеется, не совсем из коробки. Сразу же оговорюсь, что исторически так получилось, что после того, как tarantool стал развиваться как open source проект, выяснилось, что потребности сообщества и наши немного не совпадают. Сообществу нужен понятный продукт, key-value хранилище, которое работает из коробки, нам же нужен продукт с модульной архитектурой (фрейморк для написания хранилищ), дополнительными узкоспециализированными возможностями, оптимизациями производительности. Поэтому где-то мы продолжали использовать tarantool, а где-то стали использовать его форк octopus, который развивается силами автора самого первого тарантула. И это нам сильно упростило процесс конвертации. Дело в том, что в октопусе есть возможность писать репликационные фильтры на lua, то есть передавать не оригинальные команды из снапшота и xlog’а мастера, а прошедшие модификацию при помощи lua-функции. Эту возможность когда-то давно добавляли для того, чтобы была возможность поднимать частичные реплики, содержащие не все данные из мастера, а только определенные поля tuple’а. И у нас возникла мысль, что аналогичным образом мы можем в процессе репликации на лету перекодировать тексты.
И все же октопус для этой задачи пришлось чуть допилить: хотя feeder (процесс мастера, скармливающий xlog’и реплике) уже долгое время был реализован в виде отдельного модуля октопуса mod_feeder, он все же не мог запускаться отдельно без хранилища (в данном случае key-value, реализуемого модулем mod_box), а это было необходимо, чтобы изменения в механизме репликации не требовали перезапуска мастера. Ну и, разумеется, пришлось написать репликационные фильтры на lua, которые для каждого namespace’а конвертировали нужные поля из CP1251 в UTF-8.
Помимо, собственно, конвертации данных в тарантулах и октопусах, необходимо было обеспечить прозрачную работу кода с их шардами, которые уже отконвертированы и еще нет, а также обеспечить атомарное переключение с работы в CP1251 на работу в UTF-8. Поэтому было решено поставить перед хранилищами специальную перекодирующую прокси, которая, в зависимости от флажка в запросе клиента, конвертировала данные из кодировки базы в кодировку клиента. Тут нам на помощь опять пришел октопус, а вернее его модуль mod_colander, который позволяет писать быстрые прокси-серверы, в том числе на lua (так как octopus использует luajit и ffi, то получается действительно производительно).
- Настраиваем utf8proxy на мастере и репликах. Поднимаем его на том порту, который до этого слушал тарантул, сам тарантул перевешиваем на другой порт. Начиная с этого момента, клиенты могут выполнять запросы как в CP1251, так и в UTF-8.
- На сервере с мастером запускаем переконвертирующий utf8feeder, настраиваем его читать snapshot’ы и xlog’и из тех же директорий, куда их пишет мастер.
- На другом сервере в сторонке поднимаем временную реплику мастера, настраиваем ее реплицироваться с переконвертирующего фидера. Во временную реплику данные уже будут приезжать в кодировке UTF-8.
- utf8proxy реплики настраиваем реплицироваться из временной реплики, старую реплику переливаем из временной, затем возвращаем нагрузку обратно.
- Файерволим порт на utf8proxy мастера (чтобы не было коллизий на апдейтах), utf8proxy перенастраиваем на временную реплику, временную реплику делаем временным мастером, старый мастер тушим, расфайерволиваем порт на utf8proxy.
- Переливаем новый мастер из временного, переключаем реплики на репликацию из него.
- Делаем новый мастер мастером при помощи utf8proxy, временный мастер выключаем. На этом шаге все инстансы содержат данные в UTF-8, можно начинать писать с клиентов некириллические тексты.
- После перехода всех клиентов на UTF-8 вынимаем utf8proxy.
Весь процесс перекодирования тарантулов/октопусов занял примерно месяц. К сожалению, не обошлось без накладок: так как конвертировали по несколько шардов параллельно, ухитрились при переключении мастеров обратно перепутать два шарда местами. К тому времени, как проблема обнаружилось, уже произошло существенное количество изменений данных. Пришлось анализировать xlog’и с обоих шардов и восстанавливать справедливость.
Memcached
На первый взгляд, кажется (нам, во всяком случае, так сначала казалось), что с конвертацией кэшей будет проще всего: либо пишем UTF-8 в ключи с другим именем, либо в другие инстансы. Но на практике так не получается. Причины этому две: во-первых, потребуется в два раза больше кэшей, во-вторых, при переключении кодировки кэши будут непрогретые. Если со второй проблемой можно бороться плавным переключением по несколько серверов, то с первой, учитывая большое количество кэшей, — гораздо сложнее.
Поэтому мы пошли по пути помечания каждого ключа флагом о том, в какой кодировке он хранится. Тем более что перловый клиент к мемкешу Cache::Memcached::Fast уже имеет эту возможность: при сохранении строки в мемкеше в одном из флагов ключа (F_UTF8 = 0x4) он записывает внутренний перловый флаг строки SVf_UTF8, который установлен, если строка содержит мультибайтовые символы. Таким образом, если флаг стоит, то строка однозначно в UTF-8, если не стоит, то все чуть сложнее: это строка либо текстовая в CP1251, либо бинарная. Текстовые строки, понятное дело, мы переконвертируем при необходимости, а вот с бинарными возникла сложность: чтобы не ломать их ненужной конвертацией, пришлось разделить методы set/get (и пр.) для текстовых строк и бинарных, найти все сохранения бинарных строк в мемкэшед и их получения, и заменить соответствующими методами без автоматического перекодирования. В сишном коде поступили аналогично и добавили поддержку флага F_UTF8.
Прочие самописные хранилища
- Хранилище сложно переконвертировать без даунтайма, либо скоро планируем перелить данные в новое хранилище, либо данные с коротким сроком жизни. В таких случаях данные не конвертировали, а помечали новые записи признаком кодировки одним из двух способов: либо при помощи флага, указывающего в какой кодировке находится вся запись, либо при помощи BOM-маркера в начале каждого строкового поля, если оно в UTF-8.
- В хранилище хранятся не сами строки, а хэш-сумма от них. Используется у нас для поиска. Тут просто прошли по всему хранилищу скриптом, который перегенирировал хэш-суммы от оригинальных строк, переконвертированных в UTF-8. На момент переконвертации пришлось на каждый поисковый запрос выполнять два запроса к базе: один в CP1251, другой в UTF-8.
- Перед хранилищем уже установлена прокси, и все запросы в хранилище идут через нее. В этом случае реализовывали переконвертацию в прокси, аналогично тому, как это было сделано для тарантула, с той лишь разницей, что если для тарантула это временный функционал, то в данном случае он останется до тех пор, пока будут актуальны данные, хранящиеся в базе.
Поддержка UTF-8 в коде
Параллельно с тем, как наши администраторы конвертировали базы данных, разработчики адаптировали код к работе с кодировкой UTF-8. Вся наша кодовая база условно делится на три части: Perl, C и шаблоны.
При проектировании процедуры переключения проекта на кодировку UTF-8 одним из ключевых требований для нас была возможность переключения по одному серверу. Нужно было это в первую очередь для того, чтобы обеспечить возможность тестирования проекта в UTF-8 с использованием боевых баз сначала силами наших тестировщиков, а затем несколькими процентами наших пользователей.
Perl и UTF-8
- переконвертировать кириллические строки, разбросанные по коду;
- учитывать кодировку сервера при установке соединения ко всем хранилищам и сервисам;
- учитывать, что параметры HTTP-запросов могут приходить не в той кодировке, в которой работает сервер;
- необходимо отдавать контент в кодировке сервера и использовать правильные шаблоны;
- необходимо однозначно логически разделять байтовые строки и символьные, декодировать UTF-8 (из байтов в символы) на входе и кодировать в него на выходе.
Самым рутинным был процесс добавления во все необходимые места автоматической конвертации строк для хранилищ, которые мы не стали конвертировать в UTF-8 целиком. Но даже и в тех случаях, когда конвертация строк в перле была не нужна, все же приходилось учитывать то, что в перле есть разница (и существенная) между байтовыми и символьными строками. Разумеется, нам хотелось, чтобы все текстовые строки автоматически становились символьными после прочтения из базы, что потребовало анализа всего ввода/вывода на предмет того двоичные данные передаются или текстовые, пришлось пройтись по всем вызовам pack/unpack, чтобы после распаковки пометить все нужные строки как символьные (либо перед упаковкой наоборот сделать строку байтовой, чтобы длина считалась в байтах, а не символах).
Проблему того, что параметры HTTP-запроса могут прийти либо в CP1251, либо в UTF-8 (в зависимости от того, в какой кодировке была загружена referer-страница) сначала хотели решить при помощи передачи дополнительного параметра в запросе. Но потом, проанализировав то, как кодируется CP1251 и UTF-8 пришли к выводу, что всегда можем однозначно отличать кириллицу в CP1251 от кириллицы в UTF-8 путем проверки строки на то, является ли она валидным UTF-8 (только из русских букв в CP1251 практически невозможно составить валидный UTF-8).
- забыть про то, что у строк есть флаг SVf_UTF8 (он полезен только при отладке), вместо этого лучше относиться к строкам как к байтовым и символьным, забыть про то, что внутреннее представление перловой строки с флагом SVf_UTF8 — UTF-8;
- забыть про функции Encode::_utf8_on(), Encode::_utf8_off(), utf8::upgrade(), utf8::downgrade(), utf8::is_utf8(), utf8::valid();
- использовать utf8::encode() при конвертации символьной Unicode-строки в UTF-8;
- учитывать, что для перла кодировка UTF-8 и utf8 — это слегка разные кодировки: для первой валидны только code point <= 0x10FFFF (как и определено стандартом Unicode), а для второй — любые IV (int32 или int64 в зависимости от архитектуры), закодированные по алгоритму кодирования UTF-8;
- в соответствии с этим, utf8::decode() можно применять только для декодирования из доверенных источников (свои БД), в которых не может быть невалидного UTF-8, а при декодировании внешнего ввода всегда применять Encode::decode(‘UTF-8’, $_), чтобы защититься от невалидных, с точки зрения Unicode, code point’ов;
- не забывать, что результат функции utf8::decode() иногда полезно проверять, чтобы понять была ли байтовая строка валидным utf8, для аналогичных целей проверки на валидный UTF-8 можно использовать третий параметр в Encode::decode();
- учитывать, что верхняя половина таблицы latin1 содержит те же самые символы, что и Unicode code point’ы с теми же номерами, но при этом в UTF-8 они будет кодироваться по-другому. Это сказывается на результате ошибочного двойного вызова utf8::decode(): для строк содержащих только code point из таблицы ASCII или содержащих хотя бы один символ с code point > 0xff все будет в порядке, а вот если строка содержит только символы с code point из верхней половины таблицы latin1 и ascii, то символы из latin1 побьются.
- использовать перл последних версий. На perl 5.8.8 мы наступили на замечательный баг: сочетание use locale и некоторых регулярных выражений при правильных входных данных приводит к бесконечному зацикливанию регулярки. Пришлось максимально ограничить scope применения use locale только для строго необходимого набора функций: sort, cmp, lt, le, gt, ge, lc, uc, lcfirst, ucfirst.
C и UTF-8
В нашем коде на C, к счастью, оказалось не так много строк, как в перле, поэтому пошли по классическому пути: вынесли все кириллические строки в отдельный файл. Это позволило ограничить потенциальные конфликты при мерджах в рамках одного файла, а также упростило последующую локализацию. В процессе конвертации репо в UTF-8 обнаружили забавное — русскоязычные комментарии в коде были во всех 4 кириллических кодировках: cp1251, cp866, koi8-r и iso8859-5. Пришлось при конвертации использовать автоопределение кодировки каждой конкретной строки.
Помимо конвертации репо, в C также нужна была поддержка базовых строковых функций: определение длины в символах, приведение регистров, обрезание строки по длине, и пр. Для работы с Unicode в C есть замечательная библиотечка libicu, но у нее есть определенное неудобство: она в качестве внутреннего представления использует UTF-16. Разумеется, нам хотелось избежать накладных расходов на перекодирование между UTF-8 и UTF-16, поэтому для наиболее часто используемых несложных функций пришлось реализовать аналоги, работающие непосредственно с UTF-8 без перекодирования.
Шаблоны, javascript и UTF-8
С шаблонами, к счастью, все оказалось достаточно просто. На production они раскладываются rpm-пакетами, поэтому логичным решением было впилить перекодирование в процесс сборки rpm. Мы добавили еще один пакет с шаблонами в UTF-8, которые устанавливались в соседнюю директорию, а код (и перловый и сишный) после этого просто выбирал шаблон из соответствующей директории.
С javascript же из коробки не получилось. Большинство браузеров при загрузке javascript учитывают его Content-Type, но все же есть отдельные старые экземпляры, которые этого не делают, а ориентируются на кодировку страницы. Поэтому поставили костыль: при сборке пакета с javascript мы заменяли все не-ASCII-символы на их escape-последовательности в виде номеров code point’ов. При таком подходе размер js увеличивается, но зато любой браузер загружает его корректно.
Что в итоге
В итоге, по прошествии шести месяцев пасьянс сошелся. Админы как раз закончили перекодировать пару сотен баз, разработчики допилили код, процесс тестирования тоже завершился. Мы постепенно переключили ручки на панели управления Миром: сначала в UTF-8 перевели все аккаунты наших коллег, затем одного процента пользователей, после чего стали по 10 серверов переключать backend-сервера и, в конце, frontend’ы. Визуально ничего не менялось, ни страницы проекта, ни графики нагрузки, что не могла не радовать. Единственное внешние изменение, по которому было понятно, что полгода прошли недаром, — это изменение в Content-Type строчки charset=windows-1251 на charset=UTF-8.
С тех пор прошло еще три месяца, наши русскоязычные пользователи уже оценили возможность вставлять в текст emoji и прочие рюшечки, а казахские начали переписываться на родном языке и, с недавнего времени, у них появилась возможность пользоваться web-интерфейсом и мобильными приложениями на родном языке. Интересных задач в последовавшем за unicode’изацией процессе интернационализации и локализации проекта тоже хватало, мы постараемся посвятить этому отдельную статью.