Пишу, по большей части, про историю, свою жизнь и немного про программирование.

PHP и UTF-8: четыре и шесть десятых или ещё проблемные функции

С прошлого этапа я, в данном проекте, был занят тем, что писал UTF-8-аналоги для используемых строковых функций. Всплыли некоторые проблемы, о некоторых я не думал, для других до этого момента не продумывал пути решения.

Для начала, у нас в проекте используются ord и chr — функции получения кода символа и получения символа по его коду. Понятно, что места, в которых эти функции используются жёстко завязаны на однобайтные кодировки. В некоторых случаях ничего страшного не случится, в других эти места придётся переписать. Я нашёл несколько случаев использования, переписать пришлось пока только одно место, ещё одно я отложил на будущее. Сами функции я переписал следующим образом:

static public function ord($character) {
        if (self::ON) {
            $codes = unpack('N', mb_convert_encoding($character, 'UCS-4BE', 'UTF-8'));
            return reset($codes);
        }

        return ord($character);
    }

    static public function chr($ascii) {
        if (self::ON) {
            return mb_convert_encoding(pack('N', $ascii), 'UTF-8', 'UCS-4BE');
        }

        return chr($ascii);
    }

Более простая, но тоже не самая очевидная функция — addcslashes. Она экранирует указанные символы слешем, представляя все символы с кодом большим 128 как восьмеричное число, например, буква «ф» (в кодировке cp1251), если указать её во втором параметре, будет представлена «\364». Вопрос в следующем — символы в UTF-8 могу занимать больше одного символа, как их заэкранировать?

Ответ в парной функции — stripcslashes. Если последовательно заэкранировать все байты UTF-8-последовательности символа, то stripcslashes вернёт этот символ в первоначальное состояние. Код реализации я приводить не буду, слишком длинный. Что о нём нужно знать — последовательность экранируется только в том случае, если встречается целиком.

Моя задача — эмулировать поведение функции в точности, в этой связи str_pad содержит две загадки: если задана строка дополнения из двух символов и более, то как аргумент дополняется слева и какая сторона дополняется первой, когда дополнять надо обе стороны?

bolk@dev:~/daproject$ php -r 'var_dump(str_pad("..", 3, "12", STR_PAD_BOTH));'
string(3) "..1"
bolk@dev:~/daproject$ php -r 'var_dump(str_pad("..", 5, "12", STR_PAD_LEFT));'
string(3) "121.."

Как видим, сначала дополняется правая сторона, а при дополнении слева символы дополняют в указанном порядке (когда я программировал эту функцию, у меня был соблазн сделать дополнение с конца последовательности).

Вообще, в PHP функций с тонкостями поведения масса. Функция wordwrap (по-умолчанию) преобразовывает длинную строку в абзац заданной ширины, разрывая строку так, чтобы не разорвать слова. А вот какими символами, по мнению этой функции, разделяются слова? Мои эксперименты показывают, что это пробел и «\n». Интересно, что «\r\n» эта функция не разрывает! Так же нужно не забыть про то, что пробелы, вообще говоря, сохраняются, заменяется на символ переноса только необходимый минимум.

13 комментариев
rin-nas.moikrug.ru 2010

Переход на UTF8 у вас пошёл «по этапу» ;-)
Сам сижу на UTF8 уже много лет, в этом году перевёл на него рабочий проект.
Моя техника перевода выглядела так.
Я искал в коде все PHP функции (в т.ч. preg_*), которые работают со строками, далее делал в IDE контекстные замены по всему проекту, часто используя регул. выражения. В некоторых случаях (их оказалось немного) делал правки в коде на месте.
Кое-где выкинулось лишнее перекодирование типа cp1251 -> UTF8 -> cp1251.
На всяк. случай оставляю ссылку на мой PHP класс для обработки текста в кодировке UTF-8: http://forum.dklab.ru/viewtopic.php?t=17146

Евгений Степанищев (bolknote.ru) 2010

Комментарий для rin-nas.moikrug.ru:

Как проходит замена у меня, вы видите. Полагаю, исходный код у меня куда больше вашего и запутаннее.

Немного посмотрел на ваш класс. Вот что заметил, читая по диагонали:

$char_re можно заменить на \p{L}

$cp1259_table — почти не нужна, большинство русских символов конвертируются арифметическими операциями ( http://skill.ru/artwork/1748.shtml )

метод is_utf8 может выглядеть так: return preg_match(’//u’, $data) ( http://bolknote.ru/all/1632 )

функция ord у меня выглядит так: return reset(unpack(’N’, mb_convert_encoding($character, ’UCS-4BE’, ’UTF-8’))); У вас, кажется, идея отказаться от любых модулей конвертации (есть, кстати, ещё recode, который вы в коде не используете), но функция pack вам всё равно поможет в половине случаев.

А unpack поможет в chr.

Правда ли проверка на ascii, а потом, в случае успеха, применение операций, работающих с однобайтовыми кодировками даёт прирост скорости? Вы замеряли? Прокручивая в голове как это должно работать, мне кажется, что становится только медленнее.

Регулярные выражения у вас никак не модифицируется, тут есть проблема, я о ней писал ( http://bolknote.ru/all/2704 ).

Этот кусок кода: call_user_func_array(array(’self’, ’str_limit’), func_get_args()) будет работать только в PHP 5.3+.

В методе trim два регулярных выражения можно объединить в одно через вертикальную черту.

Символ «~», переданный методы trim/ltrim/rtrim разрушит регулярное выражения, из-за того, что _preg_quote_class не учитывает $delimiter.

str_pad работает неверно. Приращение в режимах STR_PAD_BOTH в нативной str_pad делается равномерно, в остальных режимах ваш код может добавится лишнего, если в $pad_str передать строку из нескольких символов.

Функций как-то очень мало, неужели так немного использовалось (где substr, например, банальный $str[1] надо заменять на неё).

Type hinting я бы:
1) заменил, где это возможно, на стандартные func(object $a, array $b), стандартные вещи понятнее остальным членам коллектива
2) остальные выкинул бы вообще, зачем думать о скорости, если по сравнению с очень медленным backtrace и reflection остальное оказывается экономией на спичках
3) а если оставил бы, то выкинул бы многочисленные преобразования strval, intval — если до них не доходит, какой толк? Кроме того, typecast много быстрее

rin-nas.moikrug.ru 2010

Комментарий для Евгения Степанищева:

Спасибо за замечания, Евгений.
Делая класс UTF8, я преследовал несколько целей:

  • Совместимость с интерфейсом стандартных PHP функций, работающих с однобайтовыми кодировками
  • Возможность работы без PHP расширений iconv и mbstring (если они есть, то активно используются)
  • Полезные функции, отсутствующие в ICONV и MBSTRING
  • Высокая производительность, надёжность и качественный код
  • PHP >= 5.1.x

$char_re можно заменить на \p{L}

Можно, но это потребует более современной библиотеки PCRE и модификатора /u, что не всегда уместно. С /u данные на валидность ещё проверяются. Это значит, PCRE будет это делать для целой строки. Если строка длинная, на это потребуется некоторое время. Возможно, PCRE имеет оптимизацию на случай, если строка не соответствует рег. выражению.

$cp1259_table — почти не нужна, большинство русских символов конвертируются арифметическими операциями

В том примере используются не все символы кодировки cp1251 (только буквы), ненадёжно!

функция ord у меня выглядит так...

Добавил в TODO, нужно будет сравнить мой и Ваш код по скорости

Правда ли проверка на ascii, а потом, в случае успеха, применение операций, работающих с однобайтовыми кодировками даёт прирост скорости? Вы замеряли?

Да, я замерял.

Регулярные выражения у вас никак не модифицируется

Не очень понял, что Вы имели ввиду. Зачем это нужно в классе UTF8? Это делается 1 раз по всему проекту, рецепт для PCRE 8.10 Вы уже дали.

Этот кусок кода: call_user_func_array(array(’self’, ’str_limit’), func_get_args()) будет работать только в PHP 5.3+.

Спасибо, заменил на call_user_func_array(array(__CLASS__, ’str_limit’), func_get_args())

В методе trim два регулярных выражения можно объединить в одно через вертикальную черту.

Можно, но работать будет медленнее. PCRE имеет оптимизацию, в объединённом рег. выражении она не сработает.

_preg_quote_class не учитывает $delimiter

Учитывает, вообще-то.

str_pad работает неверно

Возможно, я этот метод позаимствовал и он требует дополнительного тестирования.

Функций как-то очень мало, неужели так немного использовалось (где substr, например, банальный $str[1] надо заменять на неё).

Методы добавляются по мере необходимости. UTF8::substr() есть!

Type hinting я бы...
заменил, где это возможно, на стандартные func(object $a, array $b), стандартные вещи понятнее остальным членам коллектива

Стандартные вещи непременно используются. К сожалению, кроме object и array PHP пока ничего не поддерживает, к тому же нельзя указать несколько возможных типов одновременно. Именно поэтому есть ReflectionTypeHint. Преимущество класса выявляется в режиме разработки кода, а в «боевом» режиме ReflectionTypeHint полностью отключается! if (! assert_options(ASSERT_ACTIVE)) return true;

P.S.
Сделайте, пож-та, поле для ввода ответа с автоматическим изменением высоты, в зависимости от размера введённого текста. Неудобно писать большие тексты :)

Евгений Степанищев (bolknote.ru) 2010

Комментарий для rin-nas.moikrug.ru:

Можно, но это потребует более современной библиотеки PCRE и модификатора /u, что не всегда уместно

Этот модификатор работает с PHP 4.2.3, \p работают с PHP 4.4.0/5.1.0

С /u данные на валидность ещё проверяются. Это значит, PCRE будет это делать для целой строки. Если строка длинная, на это потребуется некоторое время

Валидность проверяется по ходу дела, затрат нет.

В том примере используются не все символы кодировки cp1251 (только буквы), ненадёжно!

Поэтому и написано «большинство». Не читайте по диагонали.

Не очень понял, что Вы имели ввиду. Зачем это нужно в классе UTF8? Это делается 1 раз по всему проекту, рецепт для PCRE 8.10 Вы уже дали.

Я не знал, что вы заменяете это по всему проекту. Не забудьте, что замены в случаях [\w] и (?:\w) делаться должны по-разному.

Спасибо, заменил на call_user_func_array(array(__CLASS__, ’str_limit’), func_get_args())

Причину вы не устранили. Посмотрите справку по func_get_args

Учитывает, вообще-то.

Прошу прощения, упустил.

Методы добавляются по мере необходимости. UTF8::substr() есть!

Какой-то я невнимательный. Кстати, на форуме dklab есть реализации многих функций для работы с UTF8, они оптимальны по скорости.

Сделайте, пож-та, поле для ввода ответа с автоматическим изменением высоты, в зависимости от размера введённого текста. Неудобно писать большие тексты :)

Сначала смена дизайна, потом всё остальное :)

rin-nas.moikrug.ru 2010

Причину вы не устранили. Посмотрите справку по func_get_args

Да, ссылочный параметр я упустил из виду.

Валидность проверяется по ходу дела, затрат нет.

Как же тогда будет работать рег. выражение preg_match(’//u’, $data) для проверки UTF8? :)

rin-nas.moikrug.ru 2010

$char_re можно заменить на \p{L}

Нельзя.

rin-nas.moikrug.ru 2010

$char_re можно заменить на \p{L}

Нельзя.

rin-nas.moikrug.ru 2010

Выяснил, что \p{L} работает без модификатора /u

rin-nas.moikrug.ru 2010

Нет, не работает :(

Евгений Степанищев (bolknote.ru) 2010

Комментарий для rin-nas.moikrug.ru:

Как же тогда будет работать рег. выражение preg_match(’//u’, $data) для проверки UTF8? :)

Да, сейчас просмотрел код, действительно, вызывается функция проверки, если установлен флаг «u». Я не очень хорошо помню ваш класс, но спор начался с

С /u данные на валидность ещё проверяются. Это значит, PCRE будет это делать для целой строки

и

$char_re можно заменить на \p{L}

Одна валидация на UTF-8, мне думается, должна выполняется быстрее, чем усложнённое регулярное выражение, перегруженное альтернативами.

Я не понял почему $char_re нельзя заменить на \p{L}, судя по названию (а я на него и опирался), это регулярное выражение задаёт единичный символ. Нет?

rin-nas.moikrug.ru 2010

Комментарий для Евгения Степанищева:

Одна валидация на UTF-8, мне думается, должна выполняется быстрее, чем усложнённое

регулярное выражение, перегруженное альтернативами.
Здесь всё зависит от рег. выражения:
Первое ’~^тест~s’ и второе ’~^тест~su’. Первое с $char_re отработает гораздо быстрее на 100KB тексте.

Я не понял почему $char_re нельзя заменить на \p{L}

\p{L} -​-​ это только буквы. $char_re можно заменить на ’.’, но тогда придётся использовать флаг /u.

Евгений Степанищев (bolknote.ru) 2010

Комментарий для rin-nas.moikrug.ru:

Первое ’~^тест~s’ и второе ’~^тест~su’. Первое с $char_re отработает гораздо быстрее на 100KB тексте.

Мне это кажется очень странной оптимизацией. Понятно же, что в каких-то случаях выигрыш даёт один подход, в каких-то — другой. Распределение случаев неизвестно.

\p{L} -​-​ это только буквы. $char_re можно заменить на ’.’, но тогда придётся использовать флаг /u.

Понятно. Смутило название переменной. Я бы назвал $any_char_re.

Я попробовал прогнать простой тест на файле в 2МБ (книга в UTF-8), два регулярных выражения: «!(?:$char_re)</FictionBook>!xs» и «!.</FictionBook>!xus». Выбранное мною выражение встречается только в самом конце файла.

Оба сработали, первое ищет за 0,83 секунды, второе — за 0,14. Как видите, оптимизация излишняя.

Вот ссылка на тест: http://narod.ru/disk/27200012000/bench.zip.html

rin-nas.moikrug.ru 2010

Выяснил, что для проверки кодировки preg_match(’//u’, $data) работает до 4 раз быстрее, чем mb_check_encoding($data, ’UTF-8’)! На больших объёмах данных скорость очень высокая.

Отказался везде от $char_re и сдел его @deprecated.

Рег. выражение для буквы я бы назвал $letter_re :)