PHP, PREG и UTF-8

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

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

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

    <?<br>    <br>    print "Локаль: " setLocale(LC_ALL0) . "\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 ($usePatchmb_preg_match_all($pattern$test$matchesPREG_OFFSET_CAPTURE);<br>        else preg_match_all($pattern$test$matchesPREG_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_subject0$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_subject0$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.
    Share post

    Comments 17

      +2
      немного офтопика: в php есть константа PHP_EOL и введена она не спроста :)
        0
        Да, в курсе про такую :) Я уже просто привык к *nix'овым окончаниям… благо везде, где хостимся — *nix.

          0
          Это Вы про мои print'ы? ;)
            0
            Да я именно о принтах. а по поводу никсовых окончаний: PHP_EOL платформозависима и в никсах будет именно то что нужно ;)
              0
              Знаю, знаю :) Я ж пишу то, что мне привычнее \n написать, чем рвать строку и вставлять константу. Хотя в реально критичных местах всегда ее использую.
          0
          Спасибо за топик. Не знал, что /u работает не со всеми сочетаниями…
            0
            Ключ «/u» вообще отвратно работает, по большому счету… Костыль так себе.
              +1
              Ключ /u работает так, как и должен работать. Если вы ожидаете что он будет работать как-то по другому — обоснуйте. В документации объясняется почему \w для Unicode смысла не имеет, например. Вы можете использовать \p{L} или (рекомендуемый консорциумом) когломерат (?:\p{L}|\p{M}|\p{D}|\p{Pc}) или ещё чего — это от вашей фантазии зависит…
                0
                Я подразумевал, что логичнее было бы в режиме /u рассматривать \w как любой символ буквы или цифры, по аналогии с не-юникодным режимом.

                Иными словами [\w] рассматривать как [\p{L}\p{Nd}], тогда, по сути, большинство паттернов стали бы юникодными после простого добавления ключа /u. Это с точки зрения совместимости при переходе с однобайтной кодировки на UTF-8.

                Но раз этого нет, то скорее всего на это есть объективные причины. Если знаете — поделитесь, в документации про ключ /u ничего не нашел (смотрел в разделе «PASSING MODIFIERS TO THE REGULAR EXPRESSION ENGINE» и просто поиском по документу).
                  0
                  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.

                  Всё чётко и понятно, по-моему. Конечно для PHP все эти ухищрения особого смысла не имеют (такого идиотского интерфейса к регулярным выражениям как в PHP я нигде и никогда не видел), но библиотека-то не под PHP затачивалась! Потому для быстрой работы с символами до 127 остались \w и \d (которые никогда не матчатся на символы с кодами более 127), а для всех остальных — пожалуйста, \P. Куда обиднее что нету \b нормального — его заменить на набор \p не удастся…
                    0
                    Ну да, все логично, я просто не то искал :)

                    Пожалуй, бОльшая в разы скорость работы все же поважнее совместимости, хотя из-за этого старый проект на UTF будет проблемно переставлять, если много где используются регулярные выражения. Но что-то мне подсказывает, что там, где в однобайтном использовалось \w, кириллица и прочее должны match'иться, т.к. иначе бы использовалось [a-z0-9]…
            +2
            Знаете, что меня больше всего бесит в кодировках?

            Неоднозначность названий… Где-то надо win-1251, где-то cp1251, где-то windows-1251, а еще где-то win1251… Даже UTF-8 можно (или даже нужно) иногда писать как UTF8…

            По-моему правильно — это CP1251 и UTF8|UTF16|UCS2BE и т.д. и никаких тире или пробелов, ну почему хоть это-то никак не могут стандартизовать…

            Я вчера весь мозг сломал, вспоминая как правильно — UCS-2BE или UCS2-BE…
              0
              недавно перешли с 1251 на UTF-8.
              Всем довольны, все прекрасно, но уже третий день наблюдаю странную вещь — часть (меньшая, к счастью) пользователей шлет данные формы в cp1251

              как победить — идей пока нет
                0
                Следует проанализировать заголовки запросов от таких клиентов. Собственно, заголовки в студию, без них что-то определенное сказать не получится…
                  0
                  воткнул apache_request_headers(), посмотрим. Я так понимаю, смотреть надо на AcceptLanguage?
                    0
                    Accept-Charset скорее…
                      0
                      вот парочка

                      'Accept' => 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/vnd.ms-powerpoint, */*',
                      'Accept-Language' => 'ru',
                      'Accept-Encoding' => 'gzip, deflate',
                      'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; MRA 4.10 (build 01952))',
                      'Connection' => 'Keep-Alive',

                      — 'Accept' => '*/*',
                      'Accept-Language' => 'ru',
                      'UA-CPU' => 'x86',
                      'Accept-Encoding' => 'gzip, deflate',
                      'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; MRA 4.10 (build 01952); MRSPUTNIK 1, 8, 0, 17 SW)',
                      'Connection' => 'Keep-Alive',

              Only users with full accounts can post comments. Log in, please.