PHP и UTF-8: четыре и шесть десятых или ещё проблемные функции
С прошлого этапа я, в данном проекте, был занят тем, что писал UTF-8-аналоги для используемых строковых функций. Всплыли некоторые проблемы, о некоторых я не думал, для других до этого момента не продумывал пути решения.
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» эта функция не разрывает! Так же нужно не забыть про то, что пробелы, вообще говоря, сохраняются, заменяется на символ переноса только необходимый минимум.
Переход на UTF8 у вас пошёл «по этапу» ;-)
Сам сижу на UTF8 уже много лет, в этом году перевёл на него рабочий проект.
Моя техника перевода выглядела так.
Я искал в коде все PHP функции (в т.ч. preg_*), которые работают со строками, далее делал в IDE контекстные замены по всему проекту, часто используя регул. выражения. В некоторых случаях (их оказалось немного) делал правки в коде на месте.
Кое-где выкинулось лишнее перекодирование типа cp1251 -> UTF8 -> cp1251.
На всяк. случай оставляю ссылку на мой PHP класс для обработки текста в кодировке UTF-8: http://forum.dklab.ru/viewtopic.php?t=17146
Комментарий для 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 много быстрее
Комментарий для Евгения Степанищева:
Спасибо за замечания, Евгений.
Делая класс UTF8, я преследовал несколько целей:
Можно, но это потребует более современной библиотеки PCRE и модификатора /u, что не всегда уместно. С /u данные на валидность ещё проверяются. Это значит, PCRE будет это делать для целой строки. Если строка длинная, на это потребуется некоторое время. Возможно, PCRE имеет оптимизацию на случай, если строка не соответствует рег. выражению.
В том примере используются не все символы кодировки cp1251 (только буквы), ненадёжно!
Добавил в TODO, нужно будет сравнить мой и Ваш код по скорости
Да, я замерял.
Не очень понял, что Вы имели ввиду. Зачем это нужно в классе UTF8? Это делается 1 раз по всему проекту, рецепт для PCRE 8.10 Вы уже дали.
Спасибо, заменил на call_user_func_array(array(__CLASS__, ’str_limit’), func_get_args())
Можно, но работать будет медленнее. PCRE имеет оптимизацию, в объединённом рег. выражении она не сработает.
Учитывает, вообще-то.
Возможно, я этот метод позаимствовал и он требует дополнительного тестирования.
Методы добавляются по мере необходимости. UTF8::substr() есть!
Стандартные вещи непременно используются. К сожалению, кроме object и array PHP пока ничего не поддерживает, к тому же нельзя указать несколько возможных типов одновременно. Именно поэтому есть ReflectionTypeHint. Преимущество класса выявляется в режиме разработки кода, а в «боевом» режиме ReflectionTypeHint полностью отключается! if (! assert_options(ASSERT_ACTIVE)) return true;
P.S.
Сделайте, пож-та, поле для ввода ответа с автоматическим изменением высоты, в зависимости от размера введённого текста. Неудобно писать большие тексты :)
Комментарий для rin-nas.moikrug.ru:
Этот модификатор работает с PHP 4.2.3, \p работают с PHP 4.4.0/5.1.0
Валидность проверяется по ходу дела, затрат нет.
Поэтому и написано «большинство». Не читайте по диагонали.
Я не знал, что вы заменяете это по всему проекту. Не забудьте, что замены в случаях [\w] и (?:\w) делаться должны по-разному.
Причину вы не устранили. Посмотрите справку по func_get_args
Прошу прощения, упустил.
Какой-то я невнимательный. Кстати, на форуме dklab есть реализации многих функций для работы с UTF8, они оптимальны по скорости.
Сначала смена дизайна, потом всё остальное :)
Да, ссылочный параметр я упустил из виду.
Как же тогда будет работать рег. выражение preg_match(’//u’, $data) для проверки UTF8? :)
Нельзя.
Нельзя.
Выяснил, что \p{L} работает без модификатора /u
Нет, не работает :(
Комментарий для rin-nas.moikrug.ru:
Да, сейчас просмотрел код, действительно, вызывается функция проверки, если установлен флаг «u». Я не очень хорошо помню ваш класс, но спор начался с
и
Одна валидация на UTF-8, мне думается, должна выполняется быстрее, чем усложнённое регулярное выражение, перегруженное альтернативами.
Я не понял почему $char_re нельзя заменить на \p{L}, судя по названию (а я на него и опирался), это регулярное выражение задаёт единичный символ. Нет?
Комментарий для Евгения Степанищева:
регулярное выражение, перегруженное альтернативами.
Здесь всё зависит от рег. выражения:
Первое ’~^тест~s’ и второе ’~^тест~su’. Первое с $char_re отработает гораздо быстрее на 100KB тексте.
\p{L} -- это только буквы. $char_re можно заменить на ’.’, но тогда придётся использовать флаг /u.
Комментарий для rin-nas.moikrug.ru:
Мне это кажется очень странной оптимизацией. Понятно же, что в каких-то случаях выигрыш даёт один подход, в каких-то — другой. Распределение случаев неизвестно.
Понятно. Смутило название переменной. Я бы назвал $any_char_re.
Я попробовал прогнать простой тест на файле в 2МБ (книга в UTF-8), два регулярных выражения: «!(?:$char_re)</FictionBook>!xs» и «!.</FictionBook>!xus». Выбранное мною выражение встречается только в самом конце файла.
Оба сработали, первое ищет за 0,83 секунды, второе — за 0,14. Как видите, оптимизация излишняя.
Вот ссылка на тест: http://narod.ru/disk/27200012000/bench.zip.html
Выяснил, что для проверки кодировки preg_match(’//u’, $data) работает до 4 раз быстрее, чем mb_check_encoding($data, ’UTF-8’)! На больших объёмах данных скорость очень высокая.
Отказался везде от $char_re и сдел его @deprecated.
Рег. выражение для буквы я бы назвал $letter_re :)