В этом посте речь пойдет о работе РНР5 с multibyte строками посредством preg_*() функций.

Заметил интересное положение дел, вобщем-то давным давно описанное в интернете, но актуальное и по сей день (вопрос всплыл всвязи с недавним постом про trim()).

Для примера приведу небольшой скрипт:

<?<br>    <br>    print "Локаль: " . setLocale(LC_ALL, 0) . "\n";<br>    <br>    /**<br>     * Выводит результаты функции preg_match_all<br>     * @param string $comment  Комментарий<br>     * @param string $pattern  Паттерн для preg_match_all<br>     * @param bool   $usePatch Использовать ли патч<br>     * @return void<br>     */<br>    <br>    function preg_test($comment, $pattern, $usePatch = false) {<br>        <br>        $test = "one два два three";<br>        <br>        print "\n<strong>{$comment}:</strong> <u>{$pattern}</u>\n";<br>        <br>        if ($usePatch) mb_preg_match_all($pattern, $test, $matches, PREG_OFFSET_CAPTURE);<br>        else preg_match_all($pattern, $test, $matches, PREG_OFFSET_CAPTURE);<br>        <br>        foreach ($matches[0] as $v) print "  Подстрока: «{$v[0]}», смещение: {$v[1]}\n";<br>        <br>    }<br>    <br>    /**<br>     * Патч для устранения проблемы с оффсетами, осуществляет только их пересчет<br>     */<br>    <br>    function mb_preg_match_all(<br>        $ps_pattern,<br>        $ps_subject,<br>        &$pa_matches,<br>        $pn_flags = PREG_PATTERN_ORDER,<br>        $pn_offset = 0,<br>        $ps_encoding = NULL<br>    ) {<br>        <br>        // WARNING! - All this function does is to correct offsets, nothing else:<br>        //(code is independent of PREG_PATTER_ORDER / PREG_SET_ORDER)<br>        <br>        if (is_null($ps_encoding)) $ps_encoding = mb_internal_encoding();<br>        <br>        $pn_offset = strlen(mb_substr($ps_subject, 0, $pn_offset, $ps_encoding));<br>        $ret = preg_match_all($ps_pattern, $ps_subject, $pa_matches, $pn_flags, $pn_offset);<br>        <br>        if ($ret && ($pn_flags & PREG_OFFSET_CAPTURE))<br>            foreach($pa_matches as &$ha_match)<br>                foreach($ha_match as &$ha_match)<br>                    $ha_match[1] = mb_strlen(substr($ps_subject, 0, $ha_match[1]), $ps_encoding);<br>        <br>        return $ret;<br>        <br>    }<br>    <br>    preg_test("«В лоб»", "/[\w]+/i");<br>    preg_test("Character range", "/[а-яa-z]+/i");<br>    preg_test("«В лоб» с ключем «/u»", "/[\w]+/ui");<br>    preg_test("Character range с ключем «/u»", "/[а-яa-z]+/ui");<br>    preg_test("Модификатор «\pL», можно даже без «/u»", "/[\pL]+/i");<br>    preg_test("Модификатор «\p{Cyrillic}», можно тоже без «/u»", "/[\p{Cyrillic}]+/i");<br>    preg_test("(!) Модификатор «\pL» с патчем", "/[\pL]+/i", true);<br>    <br>    $source = highlight_file(__FILE__, true);<br>    <br>?>

Рабочий пример лежит по адресу http://test.dis.dj/utf/.

Какие выводы следует сделать из увиденного:
  1. Смещение относительно начала строки считается всегда в байтах:
    3 байта «one» +
    1 байт пробел +
    3×2 байта «два» +
    1 байт пробел +
    3×2 байта «два» +
    1 байт пробел =
    18 байт,
    а должно быть
    3 + 1 + 3 + 1 + 3 + 1 = 12 символов.
  2. Правильно распознает кириллицу только «Character range» с ключем «/u» и модификатор «\pL», означающий «Unicode letter»
  3. Модификатор «\w» с кириллицей не работает вообще, даже ключ «/u» не помогает
  4. На сервере под управлением Windows Server 2008 по неизвестной мне причине отработала самая первая конструкция, а с ключем «/u» уже нет :)

Полезные ссылки:

Ну чтож, ждем PHP6, где обещается нормальная поддержка строк в UTF, включая BOM, который завалит наш сценарий, выведя 3 байта перед header(). Собственно в РНР6 вообще будет много бонусов…

P.S. Пост ни в коем случае не претендует на «открытие Америки» — я лишь собрал известную мне инфу.

UPD. В процессе обсуждения пришли к следующей замене «\w»: либо рекомендованный конгломерат «(?:\p{L}|\p{M}|\p{D}|\p{Pc})», либо «[\p{L}\p{Nd}]» (если хочется покороче). Спасибо khim.