Статьи

Кодировка символов: проблемы культурной интеграции

Я столкнулся с классической проблемой с кодировками в приложении, над которым я сейчас работаю. Как и стандарт для PHP, все строки обрабатываются как latin1 , но теперь нам нужно разрешить более широкий диапазон кодировок в нескольких местах.

Решение золотого стандарта — конвертировать все в utf-8. Так как utf-8 охватывает весь диапазон Юникода , он может представлять любой символ, который может использовать латиница 1. К сожалению, это гораздо проще сделать с самого начала, чем с большим работающим приложением. И даже тогда могут быть сторонние коды и расширения, которые предполагают использование латиницы 1. Я бы предпочел продолжить с использованием latin1 по умолчанию и прыгать через обручи только в тех немногих местах, где мне действительно нужна полная емкость utf-8.

Поэтому, подумав, меня осенило другое решение. Чтобы быть справедливым, взлом , вероятно, более описательный, чем решение , но тем не менее. Идея заключается в следующем:

  • Используйте latin1, но обслуживайте страницы в utf-8, кодируя его на выходе.
  • Встраивать строки utf-8 в latin1 и как-то не кодировать его (но все равно кодировать все остальное).

Просто, а?

Latin1 внутри, utf-8 снаружи.

При рендеринге HTML-страниц тривиально захватывать выходные данные с помощью выходного буфера и передавать его через utf8_encode . Таким образом, страница обслуживается в utf-8, хотя внутри все латинское1. В этом нет особой выгоды, так как это все еще ограничивает нас в использовании диапазона символов, охватываемых символом latin1.
На самом деле мы уже делаем это, просто чтобы уменьшить количество проблем для внешних сервисов, взаимодействующих с нашей системой. В частности, XmlHttpRequest по умолчанию имеет значение utf-8, независимо от кодировки страницы.

По сути, следующий фрагмент иллюстрирует:

// declare that the output will be in utf-8 header("Content-Type: text/html; charset=utf-8"); // open an output buffer, capturing all output ob_start('output_handler'); // when the script ends, the buffer is piped through this functions, encoding it from latin1 to utf-8 function output_handler($buffer) { return utf8_encode($buffer); } 

Вставить utf-8 в латинице.

Это сложная часть. Вместо простой передачи всего буфера через utf8_encode, строка может быть проанализирована таким образом, что-нибудь между набором специальных тегов (например, [[charset:utf8]] ... [[/charset:utf8]] ) остается как есть в то время как остальное предполагается латинским и кодируется с помощью utf8_encode, как и раньше. Это обеспечивает полную обратную совместимость, позволяя при этом использовать настоящий utf-8

Давайте изменим наш обработчик вывода:

 header("Content-Type: text/html; charset=utf-8"); ob_start('output_handler'); function output_handler($buffer) { return preg_replace_callback( '~[[charset:utf8]](.*?)[[/charset:utf8]]~', 'utf8_decode_first', utf8_encode($buffer)); } function utf8_decode_first($match) { return utf8_decode($match[1]); } 

Вот и все. Теперь мы можем встроить полные строки utf-8 в наше приложение с [[chaset:utf8]] его в [[chaset:utf8]] . Чтобы сделать вещи немного более читабельными, я добавил вспомогательную функцию:

 function utf8($utf8_encoded_byte_stream) { return '[[charset:utf8]]' . $utf8_encoded_byte_stream . '[[/charset:utf8]]'; } 

И теперь мы можем построить строку так просто, как:

 echo utf8("blÃ¥bær") . "grød"; 

Для получения результата: blåbærgrød

примечание: как указал Коре , было бы проблемой, если бы сам разделитель (например, [[charset:utf8]] ) был частью данных. Чтобы исправить это, было бы безопаснее использовать более уникальный разделитель. Вы можете просто заменить charset:utf8 чем-то, что вряд ли когда-либо произойдет. Это все еще не полностью пуленепробиваемый, но это достаточно хорошо для большинства практических применений.

Обработка ввода.

Вы можете знать об этом или не знать об этом, но при отправке формы браузеры отправляют данные в той же кодировке, что и обслуживаемая страница. Поскольку наше приложение преимущественно латинское1, нам нужно, чтобы пользовательский ввод был латинским1, чтобы сохранить BC . Таким образом, весь ввод должен быть декодирован с utf-8 до latin1. Это достаточно просто; Нам просто нужно передать весь пользовательский ввод ($ _GET, $ _POST и т. Д.) Через utf8_decode . Поскольку мы уже работаем по схеме latin1-on-the-inside-utf-8-on-the-outside, в нашем случае это уже имело место.

Это, однако, создает проблему, когда пользователю необходимо отправить utf-8, как это потребуется нашим пользователям при ответе на почту. Таким образом, в этих местах нам пришлось бы явно получить доступ к «сырой» строке через альтернативный механизм. В нашем случае нам нужно было изменить нашу оболочку http-запроса, но поскольку это расширяет API, проблем с BC нет.

С появлением PHP6 такие хаки, возможно, не понадобятся в будущем, но сейчас это дает работающее, ненавязчивое решение.