20 заметок с тегом

php и utf-8

UTF-8: быстрые регулярные выражения в PHP

Утром я написал пост о производительности различных движков регулярных выражений, а весь день перебирал разные варианты, смотрел подробности и так далее.

Как я уже писал, мне понравилась библиотека RE2, я даже скомпилировал её и посмотрел, к сожалению, оказалось, что она использует треды, а в PHP с этим не очень (не все модули PHP thread safe, поэтому чаще всего PHP собирается без поддержки тредов).

Я уже начал смотреть другие библиотеки, когда Алексей Захлестин напомнил мне про модуль PHP Multibyte String (функции mb_*), который содержит в себе движок регулярных выражений Oniguruma.

Я всегда плохо относился к этому движку, так как считал, что у него очень бедные возможности для работы с UTF-8, так как там не работают \p, \P и есть ещё пара недостатков (возможно они имеют место из-за того, что в PHP используется версия 4.7.1, тогда как последняя 5.9.2).

Сегодня я присмотрелся к движку получше. Конечно, PCRE он не заменит, но некоторых случаях вполне сможет его подменить как быстрый аналог, только надо применять несколько правил (речь идёт о версии 4.7.1, которая в PHP):

Во-первых, модификатор „i“ не действует ни на какие буквы, кроме латинских, я перебрал весь Unicode, чтобы в этом убедиться.

Во-вторых, \b и \B внутри […] относятся к backspace, а не к word boundary, а \h и \H к hex digit, а не к horizontal whitespace.

В-третьих, не работают \p и \P, зато (хоть какая-то замена) с UTF-8 работают :…: (всякие там :space: и прочие).

В-четвёртых, есть синтаксис [a-w&&[^c-g]z], то есть «&» надо экранировать внутри диапазонов.

В-пятых, части синтаксиса PCRE просто нет, например, нет рекурсии, условных регулярок, но тут уже надо обращаться к документации.

Я погонял тесты, сравнивал PCRE в режиме полном UTF-8 (с «глаголом» UCP) и Oniguruma, по результатам последний в 4…4,5 раза быстрее:

bolk-dev ~/regexp-benchmark $ php test.php
PCRE Full UTF-8: 1.0458080768585
Oniguruma:         0.24942803382874
2010   php   php и utf-8   utf8   программирование

UTF-8: как быстрее получить длину строки

Как я уже писал, сегодня ночью я наткнулся на вариант измерения длины UTF-8 строки, написанный с применением ассемблерных иструкций SSE2. У этого кода есть недостаток — на каких-нибудь ARM он не заработает, с другой стороны, у меня нет такой потребности, запускать PHP на этих процессорах.

Сегодня я почистил код (в PHP известна бинарная длина строки, поэтому я убрал проверки на символ с кодом ноль и соптимизировал код), сгенерировал таблицу HammingWeight, которая отсутствует в коде автора и перевёл проверку в двух крайних циклах на подобную таблицу.

Потом это всё скомпилировал и погонял тесты.

Сначала я получил очень странные результаты и только потом понял что случилось. В общем, разрыв в скорости функций (mb_strlen и той, что на SSE2) тем больше, чем ближе тестируемая строка к однобайтовой. Чаще всего, с SSE2 быстрее встроенной в PHP в 5—8 раз:

thasonic-dev ~/uni/futf-0.1 $ php ../test/test_strlen.php | sort -k2 -t:
SSE2 UTF-8:                 0.025604963302612
mb_strlen:                   0.15917205810547
strlen/utf8_decode:   0.62052989006042

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

В общем, о приросте в 24 раза остаётся только мечтать.

Похоже, это почти предел на сегодняшний момент. Единственная возможность ускорения, которая приходит в голову — использовать GPU на видеокарте. Например, видеокарта GTX275 управляется с 16000 потоков (!), что позволяет подсчитать длину строки порциями по 15,5КБ!

Но это больше из разряда задач-головоломок, чем чего-то практического — у нас на серверах таких видеокарт нет и вряд ли появятся, пока интерпретаторы-флагманы не заявят у себя поддержку GPU.

Так что оставлю как есть, займусь переписываем substr в SSE2, а потом выложу всё в открытый доступ.

Добавлено позднее: в следующем году Intel и AMD выпустят процессоры, поддерживающие набор комманд AVX, интересно там то, что размер регистров SIMD (которые используются и в SSE) расширяется до 256 бит. То есть через годик-другой можно будет читать строку сразу по 32 байта за раз.

2010   php   php и utf-8   utf8   программирование

UTF-8: как быстро получить подстроку (+новый вариант strlen)

Из кода быстрой функции strlen для UTF-8 можно получить функцию substr. Николай Захаров, который работает со мной в группе внутренних сервисов, переделал strlen в substr.

Я вкомпилил эту функцию в PHP, помог найти ошибки и отладить и сделал замеры:

bolk-dev ~/uni/futf-0.1 $ php ../test/test_substr.php  | sort -k2 -t:
Fast UTF-8: 0.18010711669922
mb_substr:  0.87068700790405

Разница примерно в 4 раза. Я ещё попробовал отрезать строку через функцию preg_match, сравнимо по скорости с mb_substr, но медленнее и есть ограничение — квантификатор preg_match не захватывает больше 65535 символов.

Добавлено позднее: после того как мы отладили эту функцию, я вспомнил про команды ассемблера SSE — это наборы команд (сейчас их пять — SSE1, 2, 3, 4.1 и 4.2), которые оперируют восемью регистрами размером 128 бит.

Как я рассказывал, самый быстрый алгоритм для strlen, который я нашёл, работает следующим образом: сначала указатель на строку первым циклом выравнивается по границе 4 или 8 байт (в зависимости от архитектуры процессора), чтобы процессор смог быстрее читать дальше, потом, работая сразу с числом длиной 4 или 8 байт, одной операцией подсчитывается сколько в выделенной строке содержится начальных байт символов.

Если внутри нашего рабочего участка встречается символ с кодом ноль (признак конца строки), то происходит выход в последний цикл, который занимается тем, что просматривает побайтно получившийся «хвост».

128-битные регистры, соотвественно, позволяют читать участки сразу по 16 бит, что должно дать солидную экономию на длинных текстах. Хороши они ещё и тем, что многие операции позволяют работать с регистром, как с набором байт, производя групповые операции над каждым байтом в регистре.

Час назад я поставил себе gcc под Windows и стал потихоньку писать примерный код, читать документацию, изучать команды в интернете, пока совершенно случайно не наткнулся на уже готовый вариант, написанный с применением инструкций SSE2.

Нашёл в интернете тест для таких функций, включил туда найденную и сделал замеры:

C:\MinGW\bin\test.exe
cp_strlen_utf8:          0.105706
cp_strlen_utf8_sse2: 0.030002

В 3,5 раза обгоняет предыдущий вариант, то есть, смею надеяться, обгонит встроенную в PHP функцию примерно в 24 раза. Завтра попробую.

Немаловажно, что даже на машинах с архитектурой 32 бита рассматриваемый вариант покажет ту же скорость, что и на машинах 64 бита.

2010   php   php и utf-8   utf8   программирование

UTF-8: как быстрее измерить длину строки в PHP

Я потихоньку буду писать как получается оптимизировать самые важные функции работы с UTF-8 в PHP. Оформлять буду как продолжение своей эпопеи перевода наше внутренней «Вики» на UTF-8 (в ближайшее время добавлю оглавление).

Сейчас, после перевода «Вики» мы столкнулись с тем, что некоторые операции занимают непозволительно много времени. Кое-какие проблемы решаются моими итераторами для строки (прямым и обратным), но и другие оптимизации приходится делать.

Простые оптимизации я уже применил: выкинул из класса UTF-8 проверки на self::ON (по этой константе я определял включена ли в проекте поддержка UTF-8, сейчас в ней необходимости уже нет) и сделал другие микрооптимизации, теперь пора посмотреть на методы моего класса UTF.

Одна из самых частоиспользуемых функций — strlen, которая определяет длину строки. Я всегда считал, что самый быстрый способ подсчитать длину UTF-строки в PHP, это вызывать strlen(utf8_decode(…)).

Мои тесты показывают что это не так. Самый быстрый способ (по крайней мере в PHP 5.3.2) — это mb_strlen, strlen/utf8_decode — на втором месте, потом идёт sizeof+preg_split, а самый медленный — iconv_strlen.

Для интереса я вкомпилил в PHP функцию для быстрого подсчёта длины строки в UTF-8, найденную в интернете, выигрыш по сравнению с mb_strlen незначителен:

bolk-dev ~/uni/ $ php ../test/test_strlen.php | sort -k2 -t:
Fast UTF-8: 0.68232107162476
mb_strlen: 0.88710689544678
strlen/utf8_decode: 1.5211808681488

Это тысяча итераций на файле в 400КБ.

Я оставил функцию mb_strlen.

Добавлено позднее: другая быстрая функция дала ещё худший результат: 0,79, против прежнего варианта в 0,68.

Добавлено ещё позже: а вот третья функция показала действительно впечатляющие результаты:

bolk-dev ~/uni/ $ php ../test/test_strlen.php | sort -k2 -t:
More Fast UTF-8:  0.12948799133301
mb_strlen: 0.88710689544678
strlen/utf8_decode: 1.5211808681488

Это уже дело — она почти в 7 раз быстрее лучшего результата PHP.

Вкратце, алгоритм такой: строка циклом выравнивается по границе четырёх (на 64-битных системах — 8) байт, далее читается сразу по четыре (или восемь) байт и внутри каждого кусочка одним выражением считается сколько там байт, с которых начинается символ. Поэтому на моей 64-битной системе семикратный выигрыш (на 32 битах будет примерно в три раза).

Добавлено через несколько дней: есть и ещё более быстрая функция.

2010   php   php и utf-8   utf8   программирование

PHP, UTF-8: восьмой этап, заключительный

Прошло уже немало времени, с тех пор как я описал предыдущий этап перевода на UTF-8 нашей внутренней Вики. Этот этап заключительный, после него пришлось поправить несколько шероховатостей и сегодня с него, так сказать, сняли строительные леса.

Последний этап — много ручной мороки, всякие мелочи и прочие вещи подобного уровня.

Для начала надо понять что же делать с чтением и записью файлов. Нет никакого автоматического способа решить что делать с этими операциями в вашем проекте, хорошие новости заключаются в том, что, скорее всего, ничего делать не придётся.

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

Даже в том случае, если вы производите с файлом какие-то операции, не всегда может понадобиться что-то изменить в коде, чтобы он работал с UTF-8. Например, если в файле заменяются однобайтовые символы (все символы с кодом меньше, чем 128), то проблем не будет.

Для другой операции, нам понадобится четыре метода в классе работы с UTF (напоминаю, что константа ON этого класса определяет включена ли поддержка UTF): метод charset возвращает «windows-1251» или «UTF-8» в зависимости от флага ON, метод charsetCP возвращает «CP1251» или «UTF8», метод from1251 перекодирует из cp1251 в UTF-8, если ON установлен в false, иначе возвращает строку как есть, метод to1251 перекодирует обратно, если ON установлен в false, иначе возвращает строку как есть.

Замены очевидны: везде, где в коде встречается «windows-1251» я поставил вызов метода charset, там где cp1251 — charsetCP, там где используется перекодирование (функции iconv, mb_convert_encoding, recode, recode_string) вызываем соответственно from1251 или to1251.

Потом нужно проверить все места, где встречаются вызовы chr и ord («egrep ’\\b(ord|chr)’ -R .») и проверить не нужно ли переписать какие-то места (я не помню писал ли я об этом уже).

После этого осталось только перекодировать исходный код:

# вот эти файлы перекодировать не надо, они у меня уже в UTF-8
SKIPIT='./php/Utf.php|./php/UtfReverseString.php|./php/UtfString.php'
tmp=`tempfile`

for line in $(find -name '*.php' -or -name '*.tpl' -or -name '*.inc' | egrep -v "$SKIPIT"); do
    iconv -fcp1251 -tutf8 "$line" -o "$tmp"
    [ -e "$tmp" ] && mv -f "$tmp" "$line"
done

И базу:

mysqldump -pпароль -uпользователь --add-drop-table --create-options --default-character-set=utf8 --order-by-primary -q база |
sed -e 's/cp1251;$/utf8;/mg' |
mysql -pпароль -uпользователь база

В процессе пуска проекта выяснилась ещё одна немаловажная подробность (спасибо Саше Покатилову): оказывается, несмотря на то, что установки локали (setlocale) никак не влияет на функции сортировки в PHP, она влияет на некоторые функции работы с файлами (dirname, basename и некоторые другие), вот цитата из описания функции dirname:

dirname() is locale aware, so for it to see the correct directory name with multibyte character paths, the matching locale must be set using the setlocale() function.

Поэтому не забывайте выставлять локаль, если пользуетесь этими функциями.

В общем-то, это всё, после выполнения всех описанных мною операций, проект заработал.

Дополнено: внезапно нашлись неприятные грабли, будьте осторожны: функция preg_match_all (и preg_match) с ключём PREG_OFFSET_CAPTURE возвращает бинарное смещение в строке (т. е. указывающее не на байт, а на символ), даже в UTF-режиме со включенным «глаголом» UCP.

Я поступил просто — написал простую функцию, которая корректирует выходные значения. Идея простая: отрезаем при помощи substr строку до смещения, потом измеряем её UTF-8-длину. Тут важно не забыть, что preg_match может вернуть смещение -1, если совпадение было пустым.

Еще позднее: оказывается и параметр offset (последний параметр в preg_match и preg_match_all) работает с бинарным смещением, а не с UTF-8.

И ещё позже: а вот вам ещё сюрприз: модуль Multibyte String считает, что кодировка UTF-16 это UCS-2BE, тогда как iconv кодирует правильно.

И ещё сильно позже: strftime тоже использует локаль.

2010   php   php и utf-8   utf8   программирование

PHP, UTF-8: седьмой этап, давайте что-нибудь сделаем с регулярными выражениями

В прошлый раз я рассматривал, по сути, самый сложный этап — замену всех вхождений $var[$index] и им подобных на вызов моей функции. В процессе замены я несколько раз корректировал заметку и доводил некоторые вещи руками.

Осталось совсем немного, но я столкнулся с неприятной проблемой.

В самой первой части я рассказывал о том как можно решить проблему с UTF-8 в регулярных выражениях — нужно поставить «глагол» (в терминах PCRE) UCP и добавить модификатор «u». Тогда в PCRE начинают адекватно, по отношению к UTF-8, работать «\w», «\W», «\s», «\S» и т. п.

К сожалению, этот «глагол» появился только в PCRE 8.10, а в самом последнем PHP, на сегодняшний момент, содержится 8.02. Как я говорил ранее, новый PCRE можно вкомпилить в PHP при помощи специального ключа.

Внутренние сервисы, где я работаю, используют Debian-системы и Debian-пакеты, все наши проекты выкладываются через эти пакеты. Чтобы поставить нестандартный PHP+PCRE, нужно собрать три пакета (php-cgi, php-cli и модуль к Apache).

Поскольку мы сейчас переходим постепенно на новую версию Ubuntu (с Hardy Heron на Lucid Lynx), то часть машин у нас пока на Цапле, а часть — уже на Рыси. Это значит, что пакетов нужно будет собрать шесть.

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

Мне удалось сделать, модуль «свежего PCRE» (можно скачать в разделе «Храню»). Модуль называется ygx, все функции начинаются с префикса «ygx_», константы — с «YGX_».

Что это за сокращение, не спрашивайте, когда я начинал работать, я это знал, а сейчас забыл.

Сложнее всего было корректно переименовать только те функции, типы, константы и так далее, которые видно в PHP, не задев всё то, что импортируется из внешней библиотеки PCRE.

Ставится модуль примерно следующим образом:

mkdir /tmp/pcre
cd !:1
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.10.tar.bz2
tar xvfj pcre-8.10.tar.bz2
cd pcre-8.10
./configure --prefix=/usr/local/pcre-8.10 --enable-utf8 --enable-unicode-properties --disable-static
make && make install
cd -

wget /files/php_fresh_pcre.zip
unzip php_fresh_pcre.zip
phpize
./configure --with-ygx-regex=/usr/local/pcre-8.10
make && sudo make install
phpize --clean
cd ; rm -rf /tmp/pcre

потом нужно прописать в php.ini строку «extension=ygx.so», перезапустить на FastCGI-сервер или веб-сервер с модулем PHP (смотря чем вы пользуетесь) и, вуаля, у вас есть новые функции — ygx_relace, ygx_match и т. д.

Поскольку к этому моменту все вызовы функций PCRE у меня заменены на вызовы одноимённых методов статического класса UTF, то мне нужно поменять «preg_» на «ygx_» только в одном месте — внутри методов этого класса.

2010   php   php и utf-8   php5   utf8   программирование

PHP, UTF-8: шестой этап, она же «строки, часть II». Заменяем

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

Я такой парсер начал писать несколько дней назад и очень быстро увидел, куда меня этот путь ведёт. К сожалению, из-за выбранных предпосылок код пришлось выкинуть — я парсил всё регулярными выражениями.

Код я выкинул и начал писать заново, основываясь на токенах (лексемах), которое выдаёт встроенное в язык API токинайзера. По ходу процесса я часто уточнял случаи и производил тестирование. Получился довольно неряшливый парсер, к тому же с некоторыми багами, которые я счёл несущественными. Например, не обрабатывается ситуация isset($var, $var[$idx]).

В PHP isset (а так же empty и unset) — конструкции, а не функции и принимают на вход только переменные. У меня получение символа по индексу заменяется на Utf::get(…), а внутри строки на другую конструкцию — $_ENV[0x5f3759df]->get(…), так что если произойдёт замена внутри isset, будет синтаксическая ошибка.

Т. е. у меня в коде, внутри файла с классом Utf есть ещё и такая строка — «$_ENV[0x5f3759df] = new Utf();». $_ENV я выбрал за то, что это переменная (а переменную внутри строки проще заменять на другую переменную, пусть и с вызовом метода, PHP это позволяется) и это, в терминах PHP, «суперглобальный» массив, то есть он доступен отовсюду.

Итак, я сказал, что парсер имеет баги, но на весь довольно большой код у меня было всего четыре случая, правда один довольно странный — пропала треть исходного кода, так что, если будете применять — сохраните оригинальный код (мой лежал в SVN).

Первая операция, которую вам нужно сделать ещё до запуска парсера — это запустить следующий скрипт. У меня парсер работает с UTF-8, а исходники у меня пока ещё в CP1251. Так что парсер сначала перекодирует код в UTF-8, потом обратно в CP1251. Но я беспокоился — не испортит ли двойная перекодировка что-то в исходниках.

Так что я перекодировал код туда и обратно и сравнил что получилось:

for file in $(find -name '*.php' -or -name '*.inc' -or -name '*.tpl'); do
    sum_new=`iconv -fcp1251 -tutf8 "$file" | iconv -futf8 -tcp1251 | md5sum -b | cut -d' ' -f1`
    sum_orig=`md5sum -b "$file" | cut -d' ' -f1`

    if [ $sum_new != $sum_orig ]; then
        echo $sum_new $sum_orig $file
    fi
done

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

find -name '*.php' -or -name '*.inc' -or -name '*.tpl' | xargs php -l

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

Добавлено позднее: а вот как выглядит метод get в классе Utf:

static public function get($str, $index)
    {
        if (!is_scalar($str)) {
            return isset($str[$index]) ? $str[$index] : null;
        }

        return self::substr($str, $index, 1);
    }

Добавлено позже: нашёл ещё два бага: неверно отрабатываются «$prop[$idx]->value» и «$str[$idx] ==». Второе надо править в регулярном выражении. Я их править не буду, они запросто ищутся утилитой «grep» и правятся руками. Кроме того, не заменяются переменные в heredoc (мне это не нужно).

Конструкции heredoc я нашёл при помощи «grep -FR ’<<<’ *» и просмотрел их все, их было очень мало и ни одна из них не содержала переменных.

Добавлено ещё позже: опаснее оказалась ситуация, которую просто так можно и не заметить: замена вызова func($var[$index]) на func(Utf::get($var, $index), где прототип func описан как func(&$var). Синтаксис PHP вполне допускает такие ситуации, а на деле в массив могут не попасть изменения, которые должны были переданы быть по ссылке.

Способов исправить такую ситуацию два. Первый — логгировать все вызовы Utf::get, которые применяются к массивам и править их на получение переменной по индексу. Второй — найти и просмотреть все такие ситуации. Я выбрал второй способ:

find \( -name '*.php' -or -name '*.tpl' -or -name '*.inc' \) |
xargs egrep -oh 'ion[\t ]+\w+[\t ]*([^{]+' | awk -F'[( ]' '/&\$/{print tolower($2)}' |
sort -u | egrep -v '^preg_' | xargs -I{} egrep -iR  --exclude-dir=.svn {}'[\t ]*(.*?Utf::get' *

Этот скрипт (который очень долго работает) показывает все подозрительные строки (у меня их 7 оказалось), их надо просмотреть, выявить потенциально проблемные и поправить, если проблема действительно есть.

И ещё позже: так же есть набор встроенных функций, которые используют передачу массива по ссылке, я их нашёл вот таким способом:

(cat<<FUNCS
'\b(?:array_multisort|array_pop|array_push|\
array_replace_recursive|array_replace|\
array_shift|array_splice|\
array_unshift|array_walk_recursive|\
array_walk|arsort|asort|current|each|\
end|key|krsort|ksort|natcasesort|natsort|\
next|pos|prev|reset|rsort|shuffle|\
sort|uasort|uksort|usort)[ \t]*\([\t ]*[^$)]'
FUNCS
) | xargs -I[] find \( -name '*.tpl' -or -name '*.php' -or -name '*.inc' \) -exec \
egrep [] {} \; -print
2010   php   php и utf-8   php5   utf8   программирование

PHP, UTF-8, всё ещё строки: упрощаем задачу, часть I

Итак, я достаточно долго думал над задачей как мне заменить все операции получения символа из строки по индексу, чтобы понять насколько она сложна. Решить её можно, но скучно и не нужно. Я подумал, что очень вряд ли у меня в коде встречаются все возможные случаи, которые я рассмотрел в прошлый раз, поэтому я решил посмотреть что же у меня встречается на самом делом.

Выделив типичные случаи использования, частые и простые можно заменить автоматом, редкие и сложные — просто руками. Первым делом было бы неплохо узнать каково общее количество строк, с которыми мне придётся работать:

bolk@dev:~/daproject$ find \( -name '*.php' -or -name '*.inc' -or -name '*.tpl' \) -exec \
egrep '\$\{?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*[\[\{]' -- {} \; |
sort -u | tee out-vars-1.log | wc -l

3670

Около 3,5 тысяч. Я временами дьявольски усидчив, но рассматривать каждую строку из нескольких тысяч, это для меня перебор. Что же делать?

Для начала мне хочется упростить своё регулярное выражение. Конечно, по синтаксису PHP имена переменных можно писать, например, по-русски (в UTF-8), но в реальности, у меня имён, которые содержат не только цифры и латинские буквы, только шестнадцать (в этом я убедился, сделав «egrep -v ’\$\{?\w+’ out-vars.log | wc -l») и все эти строки содержат ерунду — совпадение внутри строк в апострофах, поэтому я с чистым сердцем выбрасываю все такие строки и упрощаю себе жизнь.

Безусловно, моё регулярное выражение захватило очень много лишнего, надо эти ситуации обработать. Например, можно исключить добавление элемента в массив, это легко узнаваемая конструкция ($arr[] = $elm):

bolk@wiki:~/daproject$ IFS='\n'
for line in $(cat out-vars-1.log); do
(echo $line | sed -r 's/\$\w+\[\]//g' | egrep -q '\$\{?\w+[\[\{]') && echo $line
done | sed -r 's/^\s+//' | awk '!t[$0]++' | tee out-vars-2.log | wc -l

3357

Ситуация мало изменилась, давайте попробуем улучшить её значительно. Например, удалить все строки, где происходит только явное обращение к массиву: $arr[’index’] (но не $arr[’index’][0]):

bolk@dev:~/daproject$ IFS='\n'
for line in $(cat out-vars-2.log); do
(echo $line | sed -r 's/\$\w+[\[\{](["'\''])[^\1]*\1(\]|\})([^\[\{])/\2/g' | egrep -q '\$\{?\w+[\[\{]') && echo $line
done | tee out-vars-3.log | wc -l

1353

Ощутимая разница. Открываем получившийся файл и смотрим что бы мы ещё могли выкинуть…

Продолжение следует, я сегодня рано встал, пойду домой.

2010   php и utf-8   php5   utf8   программирование

PHP, UTF-8, рассуждения по поводу строк

Тёмная материя (18.16КиБ)

Работаю с низким приоритетом над автоматической заменой операций получения символа в строке по индексу на такую же операцию, но которая умела бы работать с UTF-8. Оказалось, что задача очень сложная.

Во-первых, нужно учесть, что в PHP для способа получить символ по индексу — это квадратные скобки (\$str[0]) и фигурные ($str{0}). Причём, если мы встречаем фигурные скобки, то это точно строка, если мы встречаем фигурные, это может быть массив. Различать массив и строку в контексте абстрактных квадратных скобок, без полной интерпретации, невозможно. К счастью, мне это и не нужно, мой статический метод (я его назвал UTF::get(…)), проверяет является ли аргумент массивом и, если да, работает с ним как с массивом.

Во-вторых, если мы выделили какое-то выражение, а оно стоит слева от равно, то надо его пропустить — это присваивание и это с очень большой вероятностью массив. За все годы работы с PHP даже в самом кошмарном коде я встречал исчезающе мало присваиваний символа по индексу.

В-третьих, индексы могут идти подряд, например, $arr[0][2]{3}. Надо не забывать об этом и брать выражение целиком.

В-четвёртых, внутри индекса может идти любое годное PHP выражение, в том числе, внутри так же может быть операция взятия символа по индексу. Например: $str[1 + $str[0] + sin($arr[$a])].

В-пятых, есть переменные переменные, к примеру: $$$$$var[1], их тоже надо уметь обрабатывать, кроме того, они могут быть взяты в фигурные скобки, а в скобках могут быть вызовы функций, методов, имена переменных: ${$$$$var[1]}, ${$this->my()}{1}, ${str}{0}.

В-шестых, в PHP в строках опеределённого типа (в кавычках и heredoc) переменные интерполируются, в том числе интерполируются строковые переменные с получением символа по индексу, а кроме этого эти переменные могут быть в фигурных скобках: «{$aaa[1]} и можно ещё так: ${aaa[1]}», тут же надо не забывать, что переменные могут экранироваться, вот так: «\${aaa[1]}».

В общем, задача сложная, я над ней думаю. Пока в сторону парсеров синтаксиса (colorer, Text_Highlighter и PHP tokinizer). Из исследования выяснилось, что все они могут мне помочь примерно в равной степени, так что я думаю, что возьму PHP tokinizer.

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

2010   php   php и utf-8   utf8   программирование

PHP и UTF-8: пятый с половиной этап

После последнего, на данный момент, пятого этапа, меня опять потянуло на нецелые числа. Что поделать — я работаю над другим проектом, поэтому на полноценный этап я ничего не наскрёб, но работу потихоньку продолжаю. В прошлый раз я занимался оптимизацией — сделал итератор по UTF-8 строке.

Тогда же я упомянул, что мне понадобится ещё один итератор, чтобы итерировать строку с конца. Сегодня я его написал, решил выложить, наверняка ещё кому-то понадобится.

<?
    class UtfReverseString implements Iterator
    {
        protected $str;   // строка
        protected $idx;   // бинарный индекс строки
        protected $pos;   // UTF-8-индекс
        protected $curend;  // бинарный указатель на конец текущего символа

        protected $idx_start; // бинарный индекс откуда стартуем
        protected $pos_start; // UTF-8-индекс откуда стартуем

        // проверка — байт не является началом символа
        protected function isNotBegin($ch)
        {
            return (ord($ch) & 192) == 128;
        }

        public function __construct($str, $pos_start = 0)
        {
            $len = strlen($str);

            $this->str = $str;
            $this->idx = strlen($str); // этот указатель будет уточнён тут же, ниже
            $this->pos = strlen(utf8_decode($str)) - 1 - $pos_start; // лучше использовать mb_strlen, если есть возможность

            while ($pos_start-- >= 0) {
                $this->curend = --$this->idx;

                while ($this->idx > 0 && $this->isNotBegin($this->str[$this->idx])) {
                    $this->idx--;
                }
            }

            $this->idx_start = $this->idx;
            $this->pos_start = $this->pos;
        }

        public function current()
        {
            return substr($this->str, $this->idx, $this->curend - $this->idx + 1);
        }

        public function key()
        {
            return $this->pos;
        }

        public function next()
        {
            $this->pos--;
            $this->curend = $this->idx - 1;

            for ($this->idx--; $this->isNotBegin($this->str[$this->idx]); $this->idx--);
        }

        public function rewind()
        {
            $this->idx = $this->idx_start;
            $this->pos = $this->pos_start;
        }

        public function valid()
        {
            return $this->idx >= 0;
        }

        public function __toString()
        {
            return $this->str;
        }
    }

Использовать так же просто, как и предыдущий, «прямой» итератор:

$str = new UtfReverseString('Привет', 1);

    foreach ($str as $ch) {
        echo $ch, ' ';
    }

Второй параметр указывает с какого символа (с конца) нужно начинать итерироваться.

2010   php   php и utf-8   программирование
Ранее Ctrl + ↓