Все уже на UTF-8, а PHP как-то не успел запрыгнуть на поезд. Я сейчас исследую тему перевода одного большого веб-сервиса на UTF-8, а он, так случилось, написан на PHP.
Увы, с UTF-8 в PHP всё очень тяжело. «Голый» PHP c UTF-8 работать, можно сказать, не умеет. Положение несколько скрашивает модуль Multibyte String, особенно тем фактом, что он умеет заменять некоторые функции на их Unicode-аналоги автоматически. Увы, не для всех строковых функций PHP в этом модуле есть аналоги, например, для strspn его не существует. Таких функций, увы, больше, чем хотелось быть.
Немного праздника в этот однобайтный склеп привносит тот замечательный факт, что UTF-8, в общем-то, придумали неглупые люди — по виду байта всегда можно отличить где мы — в начале последовательности символа или нет. Это позволяет таким функциям, как str_replace или strtr (когда вызывается с двумя параметрами) работать и с UTF-8 тоже.
Печальнее всего с двумя вещами.
Во-первых, PHP самой распоследней версии всегда считает, что строки содержат символы размером в один байт, на практике это значит, что $str[$index] вернёт на не $index-й символ, а $index-й байт последовательности символов, а это существенная разница. Найти все такие места в коде и не перепутать их с адресацией к массиву — задача непростая. Я придумал для этой цели использовать специальную функцию, которая, если на вход ей подаётся массив, работает с ним правильно, тихонько логгируя все такие вызовы.
Вторая проблема заковыристее. В PHP испокон веков аж три модуля для работы с регулярными выражениями — PCRE, Regular Expression (POSIX Extended) и уже знакомый нам Multibyte String. Первый работает с Perl-совместимыми регулярными выражениями, оставшиеся — с POSIX-синтаксисом, причём «Multibyte String» содержит UTF-8-аналоги всех функций из «Regular Expression (POSIX Extended)».
PCRE в таких помощниках не нуждается, потому что сам умеет работать с UTF-8, у него даже ключ специальный есть — «u» («Unicode»). Это в теории. На практике код
if (preg_match('/(\w)/iu', 'ПриветN', $m)) {
var_dump($m);
}
Выдаст нам… букву «N». Почему? Находить в строке UTF-8-символы куда сложнее, чем символы размером в один байт, поэтому для производительности разработчики решили, что… Впрочем, слово разработчикам:
Matching characters by Unicode property is not fast, because PCRE has to search a structure that contains data for over fifteen thousand characters. That is why the traditional escape sequences such as \d and \w do not use Unicode properties in PCRE
То есть «\d» (цифры), «\w» (буквы) и, кстати, «\b» (граница слова) и «\s» (пробельные символы) работают только с однобайтовыми кодировками. Ужасно? Не то слово! Всё потеряно? К счастью, нет.
Вообще-то в PCRE есть специальные последовательности для различных классов Unicode-символов, например, «\p{L}» — это буквы, «\p{N}» — цифры и так далее. Можно заменить все не-Unicode последовательности этими аналогами. Но и это достаточно тяжело, особенно на тех объёмах, с которыми я имею дело. Простым поиском с заменой не обойтись — «[\w\s]» должен раскрываться уже иначе, не так же как «(\w|\s)».
Если внимательно читать документацию по PCRE, то найдётся много интересного, в том числе решение и этой проблемы. В одной из версий этой библиотеки появилась экспериментальная конструкция, называемая «глаголом» (VERB), «глаголы» умеют очень многое, но нас интересует специфический «глагол» «UCP», который (ура) занимается тем, что переключает «\w», «\s», «\d» и «\b» в полностью юникодный режим.
То есть такой код даст нам ожидаемый результат:
if (preg_match('/(*UCP)(\w)/iu', 'ПриветN', $m)) {
var_dump($m);
}
Победа? Ничего подобного. Этот глагол работает начиная с PCRE 8.10, в самой последней стабильной версии PHP (5.3.3) содержится PCRE 8.02, там этот глагол ещё не поддерживается, я проверял.
Нужная нам версия 8.10 войдёт в PHP 5.3.4. Но, во-первых, её ещё нужно дождаться (пока нет даже кандидата в релизы), во-вторых, 5.3 — это совсем другая ветка, ещё пока недостаточно стабильная, я как-то уже упоминал, что пара попыток перевести кое-какие проекты на PHP 5.3 закончились провалом, причём из-за багов этой версии PHP.
Но не стоит опускать руки, 4-5 лет назад я уже сталкивался с подобной ситуацией и помню, что при компиляции PHP есть возможность указать внешнюю PCRE-библиотеку (ключ «—with-pcre-regex=DIR»), что полностью решает проблему, я вчера проверил.
Подведём итоги. Для перевода (большого) проекта на UTF-8 необходимо найти все функции, которые обращаются к строкам, заменить их на Unicode-аналоги (если они есть в модуле Multibyte String, то оттуда, если их там нет — на аналоги, написанные на PHP), функции implode, explode, str_replace, strtok (возможно, какие-то ещё) в аналогах не нуждаются, операции $str[$index] заменить на функцию, которая умеет работать и с Unicode-строками и с массивами, вкомпилить в PHP PCRE версии 8.10 и в регулярных выражениях добавить ключ «u» и «глагол» «UCP».
Ну а так же не забыть про внешние источники данных — перевести базу на UTF-8, сконвертировать шаблоны, комментарии в коде и тому подобные места.
Комментарии 29
середину от конца нельзя отличить, они все из промежутка0x80-0xBF
Неверное выразился, сейчас попробую переформулировать.
долго втыкал, что вместо «размеров» должно быть «размером», начал было уже думать, что речь очём-то а-ля строк в паскале, предваряемых байтом с длиной строки :)
Спасибо, поправил :) Перед сном писал, могут быть опечатки неточности.
Prooflink, пожалуйста.
я пользовал не API от CMF а свой код писал.
ну и другие заморочки были.
те кто «живет» на латинице — просто этих проблем не замечают. вот и все.
http://api.drupal.org/api/function/node_teaser/5
http://api.drupal.org/api/function/truncate_utf8/5 — обрезает до числа байт. не символов.
я об это уперся. у меня то язык русский и надо было посимвольно кромсать :)
В 6ой версии кажись та же пестня.
Комментарий для brainstorm.name:
Конечно проблема известна, я далеко не первый с ней сталкиваюсь (и не в первый раз, конечно же).
Только никто её от начала до конца системно не исследовал. А я этим занимаюсь.
Комментарий для brainstorm.name:
Странные костыли. В PHP есть функция mb_substr
Комментарий для brainstorm.name:
есть диакритика. которая не вся влезает в 1252.
Комментарий для brainstorm.name:
модуль Multibyte String есть в PHP4.
как то так. if(function_exists(….) ) почему было позабыто. увы.
Потому что он на самом деле может быть не поставлен. Например, его нет в PHP 4.4.2 или 5.1.2, кроме того, его можно запретить использовать.
Правда, я не очень понимаю, почему бы в этом случае просто не сэмулировать их наличие.
Если раньше все работало с однобайтной кодировкой, что изменилось в последнее время кроме моды?
Унификация, как правильно заметили комментарием ниже. У нас в интранете несколько крупных сервисов, все интегрированы друг в друга и только одному приходится устраивать переконвертацию.
вообще-то, отдают в UTF-8, а наш однобайтный друг ждёт их в CP1251, так вот — часть символов просто потеряется, им нет места в CP1251.
UTF-8, только намного хуже.
Переконвертация отвратительна ещё и тем, что данные другие сервисы,
Ну и проще с UTF8, у меня сайт на UTF8, я, особо не изворачиваясь, могу хоть тайский источник процитировать, он будет сохранён в натуральном виде, а не в виде диких html entities, которые суть тот же
Поддержка нескольких языков — это понятно.
А есть ли еще причины для перехода? Скажем мне не нужна поддержка разных языком и спецсимволов.
Интересно, есть ли менее очевидные преимущества, чем универсальность.
Не рассуждал на эту тему, я меня цель вполне конкретная.
Прочел много литературы, с «глаголами» «UCP» в регулярках еще не знакомился.
Спасибо, разберусь что к чему.
Мой тернистый путь таков:
1. перекодирование всех файлов в проекте в
2. Указание заголовков PHP
3. выставление локали setLocale(LC_ALL,
3. определение
4. настройка сервера БД под
5. конвертация БД в
6. в php.ini не переопределял параметр ХХХ для вызова
Вместо этого написал отдельный файл с функциями с нижним подчеркиванием, например: _strlen, _substr… Список параметров и их порядковые позиции соответствуют функциям без подчеркивания. Реализация функций зависит от наличия Multibyte Extension для нее или нет, а также от текущей кодировки проекта. Далее по всему коду проекта производим грамотную замену строковых функций на их реализованные выше аналоги с нижним подчеркиванием.
Таким образом мы можем регулировать поведение этих функций, а также производить их поиск и замену в программном коде, например, с помощью регулярных выражений IDE search, зная, что отличие от native название отличается нижним подчеркиванием.
Есть смысл оформить этот список функций как статические методы класса, но тогда нужно править также те участки кода, которые используют callback вызов строковых функций. Тут поступайте как удобно Вам.
Примерный список функций, требующих адаптации:
strlen
strtolower
strtoupper
ucfirst
lcfirst
ucwords
substr
str_replace
str_ireplace
str_repeat
substr_replace
substr_count
strstr
strchr
strrchr
strrichr
stristr
strtr
strpos
stripos
strripos
strrpos
str_split
strrev
str_shuffle
parse_str
trim
ltrim
rtrim
wordwrap
htmlspecialchars
htmlspecialchars_decode
htmlentities
html_entity_decode
preg_grep
preg_split
preg_filter
preg_match
preg_match_all
preg_replace_callback
preg_replace
substr_compare
Отдельная статья с регулярными выражениями. Ниже по тексту есть ссылки на исследование и решение проблем.
Пока еще не ознакомился с авторской заметкой про regexp «UCP», представлю функцию модификации
function cp1251_to_utf8_pattern($pattern) {
if($pattern) $pattern .= 'u';
$alpha = '\p{L}';
$w = '\p{L}|\p{Nd}|_';
$d = '\p{Nd}';
$W = '^'.$w;
$D = '^'.$d;
$map = array(
'\w' => $w,
'\d' => $d,
'\W' => $W,
'\D' => $D,
'^\W' => $w,
'^\D' => $d,
);
$pattern = _strtr($pattern, $map);
return $pattern;
}
Для простых регулярок такого преобразование будет вполне достаточно, проверял на некоторых образцах. Как поведет себя регулярка при комбинации различных заменяемых паттернов — вопрос не из легких.
Полезные ссылки:
http://habrahabr.ru/blogs/php/45910/
http://test.dis.dj/utf/
http://www.regular-expressions.info/unicode.html
Мои эксперименты показывают, что это ни на что не влияет.
Их в любом случае нужно править — функции это или статические методы.
Часть этих функций не требует адаптации. Например, str_replace, str_repeat, strtr и так далее. Посмотрите мой список.
Так делать нельзя. Ваша модификация разрушит шаблон вида [\w\r\n] и тому подробные.
В ВакоВики, форк которого мы используем, регулярные выражения очень сложный и их много. В любом случае, в проекте, охватить который взглядом я не в силах (и оттестировать полностью — тоже) я бы не рискнул использовать такие замены.
Не пользуйтесь суррогатами, есть документация по PCRE: http://www.pcre.org/pcre.txt
По поводу замены символов в регулярках — Вы правы: в сложных комбинациях простой замены будет недостаточно. Выход один? PCRE_UCP?
Комментарий для Alex Karpinskiy:
Я в светеUTF-8 имел ввиду.
Простой выход — такой. Но можно рассмотреть все вариаты и сделать правильные замены.
Она ещё не закончена. Потом сделаю, видимо.