Здравствуйте. Думаю, что большинство веб-программистов знает, как работает php-интерпретатор.
Для тех, кто не знает:
Вначале, написанный нами код разбирается лексическим анализатором. Далее, полученные лексемы, передаются в синтаксический анализатор. Если синтаксический анализатор дал добро, то лексемы передаются транслятору, а он, в свою очередь, генерирует так называемые opcodes (operation codes). И только после этого, в дело вступает виртуальная машина PHP (та самая Zend Engine) которая и выполняет наш алгоритм из получившихся opcodes. Opcodes так же называют эдаким php-шным ассемблером.
Данная статья расскажет вам о том, какие opcodes и в каких случаях генерируются. Конечно, рассказать про все opcodes в рамках одной статьи не получится, но в данной статье будет рассмотрен конкретный пример и на его основе мы попытаемся разобраться что к чему у этих opcodes. На мой взгляд, самое главное, что вы узнаете прочитав статью, это то, как на самом деле происходит выполнение ваших исходных текстов и, возможно, это поможет вам в лучшем понимании языка php.
Советую вам налить себе чашечку капучино или просто зеленого чая, т.к. под катом листинги opcodes и php-кода…
Не так давно, сидя на одном из php-форумов, я наткнулся на топик, в котором ТС просил помочь ему составить алгоритм, который бы из строки:
Делал примерно следующие:
Как вы уже наверное поняли, необходимо было найти все вхождения {...} и достать оттуда фразы разделенные символом "|". Затем, всё вхождение {...} заменить на одну из тех фраз. Фразу нужно было выбрать случайным образом.
Здесь отчетливо видны две глобальные группы:
и
По условиям изначальной задачи, уровень вложенности роли не играл, то есть все фразы, разделенные символом "|" в пределах глобальной группы, имели одинаковый вес.
Я предложил следующий вариант решения задачи:
Регулярное выражение просто выбирает все последовательности типа:
В данном случае получатся 2 вхождения по 2 группы в каждом (группы выделены скобками):
и
Очищаем их от "{" и "}", чтобы можно было применить explode() к чистенькой строке. И в конце заменяем всю последовательность на случайную фразу из группы, полученную через explode() и rand(). Здесь, думаю, сложностей ни у кого не возникнет, т.к. алгоритм довольно простой.
Сразу скажу, что у данного варианта есть некоторые недочеты. Вот некоторые из них:
Но это скорее недостатки составленной регулярки.
Позже я покажу еще 2 варианта которые лишены данных ограничений, а пока предположим, что работаем с идеальным, для данной регулярки, текстом.
Давайте наконец посмотри, что же нам нагенерировал транслятор:
«Ну вот, куча чего-то непонятного» — можете сказать вы.
Но, уверяю вас, тут нет ничего сложного, давайте разбираться.
Сразу можно увидеть, что дамп как бы разделен на 2 части:
Когда мы пишем наш код, мы используем переменные для хранения значений. Интерпретатор PHP делает то же самое когда разбирает наш код, только в чуть большем объеме.
Opcode, в пределах скрипта или функции, имеет несколько характеристик:
Если говорить проще, то opcode берет операнды, производит над ними какие-то действия и возвращает результат в указанную внутреннюю переменную.
Внутренние переменные — это участки памяти, которые выделяет интерпретатор при выполнения opcodes. Эти переменные имеют порядковые номера, начинающиеся с нуля и имею 3 основных типа:
Давайте начнем с первого opcode (пока рассматриваем первую часть дампа) и посмотрим что получится:
Видно, что это первая строка скрипта и нулевой по счету opcode. Судя из названия, opcode ASSIGN присваивает одно значение другому. Вот и в данном случае мы присвоили наш тестовый текст переменной !0, а судя по дампу, !0 — это наша $str в скрипте (посмотрите на первую строку кода чтобы убедиться, что происходит присвоение текста).
Opcode в данном случае ничего не возвращает, т.к. выполняет присваивание и результат уже будет храниться в переменной !0.
Теперь давайте рассмотрим, что происходит при вызове функции. Чтобы вызвать функцию, нам нужно предать ей на вход определенные аргументы в качестве параметров. Собственно этим и занимаются opcodes под номерами 1,3 и 4
Давайте сопоставим аргументы, которые мы передали в preg_replace_callback и соответствующие opcodes:
Вы наверное заметили, что в одном случае используется SEND_VAL, а в другом — SEND_VAR.
Связано это с тем, что в одних случаях, в качестве параметра, мы передаем переменную (SEND_VAR), а в других — чистое значение (SEND_VAL).
В соответствии с этим, текст регулряки и лямбду — мы передаем как значения, а исходную строку — как переменную (коей она и является).
Всё, аргументы переданы и теперь остается только вызвать функцию:
Здесь, opcode с именем DO_FCALL вызывает функцию 'preg_replace_callback' и помещает результат ее работы во внутреннюю переменную $2.
Заметьте, не в физическую переменную $str (как написано в скрипте), а пока что именно во внутреннюю.
Ну и чтобы положить результат в $str (!0), используется уже знакомый нам ASSIGN:
Присваивает значение $2 переменной !0 (наша $str). Из этого следует, что значения возвращаемые функциями не присваиваются ждущим их переменным напрямую, а только через временную переменную.
Поехали дальше.
Последняя строка скрипта. Необходимо просто вывести получившуюся строку, но мы видим тут аж 3 opcode. Давайте посмотрим:
Собственно, вот и весь основной скрипт.
Стоит отметить, что всё это мероприятие завершается через opcode RETURN (хотя функции мы пока не рассматривали). Это такая особенность php — скрипт или функция обязательно должны вернуть что-нибудь в конце, даже если return явно не указан. Окончания скриптов возвращают единицу, а функции возвращают null (если не указано что-либо другое).
Теперь давайте рассмотрим анонимную функцию (вторая часть дампа).
Ее логика проста:
Рассмотрим opcodes и сразу обратим внимание на compiled vars для данной функции.
Первый opcode — RECV
Он получает переданный в функцию аргумент и кладет его содержимое в !0.
Теперь нам нужно произвести ряд манипуляций для вызова функции str_replace():
Из этого следует, что чтение значений из массивов происходит в 2 действия: чтение во временную переменную и уже потом использование содержимого этой переменной.
Тут мы сначала вызываем функцию str_replace() и результат кладем в $3.
Далее довольно интересный момент…
Внятной документации по ASSIGN_DIM нет, а по OP_DATA какой-либо документации в принципе нет, поэтому могу предположить, что ASSIGN_DIM проецирует элементы массива на ячейки памяти и возвращает указатель на ту область памяти, где находится нужный нам элемент. Указатель неявно помещается в $4. В данном случае, судя по операндам и коду, нас интересует элемент с индексом 1 в массиве !0.
Далее, OP_DATA пишет результат функции str_replace() из $3 в область памяти, указатель на которую хранится в $4.
Не понятно почему в данном случае не используется ASSIGN. Видимо это связано со спецификой хранения данных в виде массива. Хотя при модификации указателей тоже используется ASSIGN (правда там ASSIGN_REF).
Если кто-то точно знает, что за связка ASSIGN_DIM\OP_DATA и как она действительно работает, напишите пожалуйста в комментах, буду благодарен.
Далее по коду, мы вызываем explode(), для того чтобы разбить строку по символу "|":
Далее, мы выбираем рандомный элемент из получившегося массива:
Сейчас я сознательно не расписываю всё более детально, т.к. считаю, что это уже итак понятно, ведь я всё это расписывал ранее.
Вот мы собственно и разобрали наш первый вариант алгоритма, что называется, по косточкам.
Теперь давайте двинемся дальше.
Немного подумав, я решил, что в тексте может быть любой порядок слов, не обязательно {...} текст. Да и в тексте могут встречаться такие символы как "{", "}" и "|" которые не должны быть обработаны как разделители или группы вариантов.
Исходя из этой задумки, решил, что если необходимо использовать "{", "}" и "|" в тексте, то их надо просто заэкранировать слешем, например "\{".
Да и нелепые ограничения типа необходимости пробелов в конце строки и между стоящими рядом группами, немного напрягали.
Составил себе следующую тестовую строку:
Из которой должно получится что-то вроде:
Для реализации такого варианта, потребовалось переработать регулярное выражение.
Именно во время обдумывание данного варианта, я набил руку в использовании утверждений в регулярных выражениях. Отсюда вывод — придумывайте себе задачки, для решения которых нужно иметь знания, которых у вас нет. Или есть, но не было времени их укрепить.
Получилось следующее:
Регулярка работает следующим образом:
Да, на этот вариант не действую ограничения прошлого варианта, но и у него есть свои тонкости. Например, данную регулярку можно легко свести с толка, вставив после любого закрывающего символа "}", символ "|".
Например:
Если так сделать, то алгоритм просто сожрет часть текста, посчитав его вариантом.
Но тут, как и в прошлом варианте, дело, скорее всего, в составленной регулярке.
Позже мы увидим как обойти и это ограничение. А пока, давайте посмотрим что еще изменилось в алгоритме и рассмотрим дамп opcodes.
Итак, в данном варианте, помимо регулярки, поменялось еще и содержимое лямбды и конец скрипта.
В конце скрипта, мы, с помощью preg_replace(), убираем все не экранированные слеши, чтобы превратить такие места как:
в
В лямбде, с помощью preg_replace(), мы чистим строку от не экранированных символов "{" и "}", чтобы остались только голые варианты, разделенные символом "|".
Затем, через preg_split(), мы получаем только те фразы, которые разделены символом "|", которому не предшествует слеш, то есть экранированный "|" не будет считаться разделителем.
Ну и возвращаем результат в виде рандомного элемента массива.
Попробуйте сами разобрать этот дамп, тут ничего нового нет.
Ну вот я и рассказал про второй вариант. Остался последний и самый интересный, как с точки зрения регулярки, так и с точки зрения рассмотрения opcodes.
После того, как дискуссия на ветке форума была прекращена, в топик пришел некий товарищ и сказал, что мол «задачка уже старая и решали ее давно» и привел пример предложения, которое надо было разобрать таким образом:
И тут мне стало понятно, что нужно вводить поддержку уровня вложенности группы вариантов, а следовательно — нужна рекурсия. Причем ясно, что количество проходов равно максимальному уровню вложенности в тексте.
Начал думать. В результате чего и родился последний вариант, которым я доволен. Барабанная дробь…
Единственное, я придумал свой вариант исходной строки, в котором было бы задействовано всё, что только можно:
После обработки, данная строка, по идее, может превратиться в нечто такое:
Сначала, давайте рассмотрим регулярку и лямбду.
Находим вхождение не экранированного символа "{"
Далее, нам надо забрать любые символы до первого не экранированного символа "}", то есть, говоря другими словами, мы ищем все самые глубокие группы
Забор происходит по условию. Условие построено таким образом, чтобы механизм регулярных выражений точно определял начало и конец строки, как бы не оставляя ему другого выбора. Без такого условия, были бы проблемы со строками типа:
Т.к. в данном случае, регулярное выражение посчитало бы началом группы — "{Виктор", а на самом деле, началом первой группы является "{Антон".
В общем вся эта круговерть продолжается до тех пор, пока в тексте остались не экранированные "{". То есть после каждого прохода preg_replace_callback() — происходит подъем уровня вложенности и процесс повторяется до наступления указанного выше события.
Вы конечно можете сказать что всё это можно сделать и более простым регулярным выражением, но вспомните про поддержку "{", "}" и "|".
Да, она здесь тоже есть. И что самое главное — отсутствуют ограничения на формат текста (на сколько я мог заметить во время тестирования алгоритма).
Теперь давайте разберем то, ради чего мы все тут собрались.
Лямбду разбирать не будем, она простая до безобразия, а вот основной код разберем, тем более что я уже слышу недоумевающие и полные интереса возгласы, которые прямо таки вопрошают рассказать им про появившиеся стрелочки (хотя мы видели их и раньше) и новый для нас вид opcodes — JMP*
Давайте по порядку. Обратим внимание на:
Это while в нашем коде. Тут мы передаем в функцию preg_match() аргументы в виде регулярки и строки, которую будем проверять на вхождения. Вызываем функцию, помещаем результат в $1.
Теперь внимательно. Opcode JPMZ — это управляющий opcode, который делает следующее:
Если операнд равен нулю, то opcode передает управление в другое место, потому JMPZ расшифровывается как «Jump If Zero».
Судя по дампу, мы можем смело сказать, что если содержимое $1 будет равно 0, то управление перейдет к opcode под номером 12.
А что там?
А там 10 строка нашего скрипта, то есть мы уже находимся за пределами while.
Теперь вспоминаем как работает while.
Если условное выражение, которое мы передаем в while, вернет нам что-либо отличное от нуля, то выполнится тело while, но если оно вернет на 0 или эквивалент (пустая строка, пустой массив, false и т.д.), то нас выкинет из while. Что мы здесь собственно и наблюдаем.
Еще раз взгляните на участок opcodes отвечающих за реализацию while.
Если то, что вернула нам функция preg_match() равно 0, то выйти из цикла. preg_match() вернет нам 0, если совпадения небыли найдены и нас выкинет из while.
Далее посмотрите на еще один JMP:
Это, так называемая, безусловная передача управления, находится она, как видим, на 9 строке скрипта. А там у нас закрывающая скобка while. Что произойдет? Правильно! Очередная проверка условия перед телом цикла. Смотрим на opcode — JMP отправляет нас к opcode под номером 1. А что там? А там то, что мы уже рассматривали.
Вот собственно и вся логика. Обратите внимание, что перед именами некоторых opcodes, есть стрелки указывающие направо. Но здесь нас интересует не направление стрелок и а их расположение. Если стрелка расположена по левому краю своего столбца, то это означает вход в участок кода. Это может быть цель условного или безусловного перехода, или просто открывающаяся командная скобка.
Стрелки выровненные по правую сторону, означают выход из участка кода. Это может быть условный или безусловный переход, или закрывающаяся командная скобка. Так же обратите внимание, что перед opcode RETURN стрелка прижата к правому краю. Думаю здесь не нужно ничего пояснять.
Ну что, вот собственно и всё, о чем я хотел рассказать в этой статье.
Конечно, можно было бы замерит производительность всех трех вариантов (и я это делал), но, уверяю вас, что всё зависит от задачи и на разных текстах производительность будет разная.
Для дампа opcodes, использовался модуль VLD
PHP версии 5.5.14
Остальные ссылки указаны в тексте.
Надеюсь, что дал вам новые знания и вы смогли лучше понять работу php.
Спасибо за внимание и всего вам самого хорошего!
Для тех, кто не знает:
Вначале, написанный нами код разбирается лексическим анализатором. Далее, полученные лексемы, передаются в синтаксический анализатор. Если синтаксический анализатор дал добро, то лексемы передаются транслятору, а он, в свою очередь, генерирует так называемые opcodes (operation codes). И только после этого, в дело вступает виртуальная машина PHP (та самая Zend Engine) которая и выполняет наш алгоритм из получившихся opcodes. Opcodes так же называют эдаким php-шным ассемблером.
Данная статья расскажет вам о том, какие opcodes и в каких случаях генерируются. Конечно, рассказать про все opcodes в рамках одной статьи не получится, но в данной статье будет рассмотрен конкретный пример и на его основе мы попытаемся разобраться что к чему у этих opcodes. На мой взгляд, самое главное, что вы узнаете прочитав статью, это то, как на самом деле происходит выполнение ваших исходных текстов и, возможно, это поможет вам в лучшем понимании языка php.
Советую вам налить себе чашечку капучино или просто зеленого чая, т.к. под катом листинги opcodes и php-кода…
Постановка задачи
Не так давно, сидя на одном из php-форумов, я наткнулся на топик, в котором ТС просил помочь ему составить алгоритм, который бы из строки:
Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}}, как {твои|ваши} дела
Делал примерно следующие:
Привет Виктор, как твои дела
Как вы уже наверное поняли, необходимо было найти все вхождения {...} и достать оттуда фразы разделенные символом "|". Затем, всё вхождение {...} заменить на одну из тех фраз. Фразу нужно было выбрать случайным образом.
Здесь отчетливо видны две глобальные группы:
{{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}}
и
{твои|ваши}
По условиям изначальной задачи, уровень вложенности роли не играл, то есть все фразы, разделенные символом "|" в пределах глобальной группы, имели одинаковый вес.
Я предложил следующий вариант решения задачи:
$str = 'Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} как {твои|ваши} дела';
$str = preg_replace_callback('#(\{[\s\S]+?\})([^\|\{\}]+)#', function($mathces)
{
$mathces[1] = str_replace(array('}','{'), '', $mathces[1]);
$arr = explode('|', $mathces[1]);
return $arr[array_rand($arr)].$mathces[2];
}, $str);
echo "$str\n";
Регулярное выражение просто выбирает все последовательности типа:
{.....} какой-то текст
В данном случае получатся 2 вхождения по 2 группы в каждом (группы выделены скобками):
( {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} ) ( как )
и
( {твои|ваши} ) ( дела )
Очищаем их от "{" и "}", чтобы можно было применить explode() к чистенькой строке. И в конце заменяем всю последовательность на случайную фразу из группы, полученную через explode() и rand(). Здесь, думаю, сложностей ни у кого не возникнет, т.к. алгоритм довольно простой.
Сразу скажу, что у данного варианта есть некоторые недочеты. Вот некоторые из них:
- Если текст заканчивается символом "}", то в конце текста нужно вставить пробел
- Если рядом стоят две пары фигурных скобок — {...}{...}, то между ними нужен хотя бы пробел
Но это скорее недостатки составленной регулярки.
Позже я покажу еще 2 варианта которые лишены данных ограничений, а пока предположим, что работаем с идеальным, для данной регулярки, текстом.
Давайте наконец посмотри, что же нам нагенерировал транслятор:
Opcodes dump
filename: /www/patterns/www/scan/simple.php
function name: (null)
number of ops: 11
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут много символов'
2 1 SEND_VAL 'регулярка'
2 DECLARE_LAMBDA_FUNCTION 'имя анонимной функции'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4
10 > RETURN 1
Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f:
filename: /www/patterns/www/scan/simple.php
function name: {closure}
number of ops: 22
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1
4 SEND_VAL ''
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11
7 21* > RETURN null
End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f.
«Ну вот, куча чего-то непонятного» — можете сказать вы.
Но, уверяю вас, тут нет ничего сложного, давайте разбираться.
Сразу можно увидеть, что дамп как бы разделен на 2 части:
- 1 часть — дамп основного кода скрипта
- 2 часть — дамп кода анонимной функции, которую мы передаем в preg_replace_callback.
Исследуем opcodes
Когда мы пишем наш код, мы используем переменные для хранения значений. Интерпретатор PHP делает то же самое когда разбирает наш код, только в чуть большем объеме.
Opcode, в пределах скрипта или функции, имеет несколько характеристик:
- Номер строки в файле
- Порядковый номер opcode в скрипте или функции
- Имя
- Область видимости
- Дополнительная информация (пока что нашел ей только одно применение о котором когда-нибудь расскажу)
- Внутренняя переменная в которую будет сохранен результат работы opcode
- Операнды (данные) над которыми opcode производит какие-то действия
Если говорить проще, то opcode берет операнды, производит над ними какие-то действия и возвращает результат в указанную внутреннюю переменную.
Внутренние переменные — это участки памяти, которые выделяет интерпретатор при выполнения opcodes. Эти переменные имеют порядковые номера, начинающиеся с нуля и имею 3 основных типа:
- Физические (начинается с символа "!")
- Виртуальные (начинается с символа "$")
- Временные (начинается с символа "~")
Давайте начнем с первого opcode (пока рассматриваем первую часть дампа) и посмотрим что получится:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут много текста'
Видно, что это первая строка скрипта и нулевой по счету opcode. Судя из названия, opcode ASSIGN присваивает одно значение другому. Вот и в данном случае мы присвоили наш тестовый текст переменной !0, а судя по дампу, !0 — это наша $str в скрипте (посмотрите на первую строку кода чтобы убедиться, что происходит присвоение текста).
Opcode в данном случае ничего не возвращает, т.к. выполняет присваивание и результат уже будет храниться в переменной !0.
Теперь давайте рассмотрим, что происходит при вызове функции. Чтобы вызвать функцию, нам нужно предать ей на вход определенные аргументы в качестве параметров. Собственно этим и занимаются opcodes под номерами 1,3 и 4
Давайте сопоставим аргументы, которые мы передали в preg_replace_callback и соответствующие opcodes:
- Передаем регулярку: SEND_VAL 'текст регулярки'
- Объявляем лямбда-функцию: DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'
- Результат (функция) неявно помещается во внутреннюю временную переменную ~1
- Передаем лямбда-функцию: SEND_VAL ~1
- Передаем целевую строку: SEND_VAR !0
Вы наверное заметили, что в одном случае используется SEND_VAL, а в другом — SEND_VAR.
Связано это с тем, что в одних случаях, в качестве параметра, мы передаем переменную (SEND_VAR), а в других — чистое значение (SEND_VAL).
В соответствии с этим, текст регулряки и лямбду — мы передаем как значения, а исходную строку — как переменную (коей она и является).
Всё, аргументы переданы и теперь остается только вызвать функцию:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 DO_FCALL 3 $2 'preg_replace_callback'
Здесь, opcode с именем DO_FCALL вызывает функцию 'preg_replace_callback' и помещает результат ее работы во внутреннюю переменную $2.
Заметьте, не в физическую переменную $str (как написано в скрипте), а пока что именно во внутреннюю.
Ну и чтобы положить результат в $str (!0), используется уже знакомый нам ASSIGN:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 ASSIGN !0, $2
Присваивает значение $2 переменной !0 (наша $str). Из этого следует, что значения возвращаемые функциями не присваиваются ждущим их переменным напрямую, а только через временную переменную.
Поехали дальше.
Последняя строка скрипта. Необходимо просто вывести получившуюся строку, но мы видим тут аж 3 opcode. Давайте посмотрим:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4
- Добавляем к временной переменной ~4 (сейчас там ничего нет) нашу переменную !0 ($str)
- Добавляем к временной переменной ~4(в которой уже лежит значение от !0) символ переноса строки \n (код символа — 10)
- Выводим содержимое ~4 (заметьте, что echo — это не функция, а оператор языка)
Собственно, вот и весь основной скрипт.
Стоит отметить, что всё это мероприятие завершается через opcode RETURN (хотя функции мы пока не рассматривали). Это такая особенность php — скрипт или функция обязательно должны вернуть что-нибудь в конце, даже если return явно не указан. Окончания скриптов возвращают единицу, а функции возвращают null (если не указано что-либо другое).
Теперь давайте рассмотрим анонимную функцию (вторая часть дампа).
Ее логика проста:
- Чистим содержимое группы от "{" и "}"
- Разделяем ее по символу "|"
- Выбираем случайный вариант
Рассмотрим opcodes и сразу обратим внимание на compiled vars для данной функции.
Первый opcode — RECV
Он получает переданный в функцию аргумент и кладет его содержимое в !0.
Теперь нам нужно произвести ряд манипуляций для вызова функции str_replace():
- Инициализировать массив с символами замены и предать его в качестве первого параметра
- Передать второй параметр — пустую строку
- Передать первую группу из вхождения в качестве третьего параметра
line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1
- Инициализируем массив помещая в него символ "}" (сам массив кладем в ~1)
- Добавляем к массиву символ "{", результат помещаем туда же
- Передаем аргумент функции
line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 SEND_VAL ''
- Передаем в функцию пустую строку
line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2
- Берем из массива !0 элемент с индексом 1 и помещаем результат в $2
- Передаем в функцию значение $2
Из этого следует, что чтение значений из массивов происходит в 2 действия: чтение во временную переменную и уже потом использование содержимого этой переменной.
line # * op fetch ext return operands
---------------------------------------------------------------------------------
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4
Тут мы сначала вызываем функцию str_replace() и результат кладем в $3.
Далее довольно интересный момент…
Внятной документации по ASSIGN_DIM нет, а по OP_DATA какой-либо документации в принципе нет, поэтому могу предположить, что ASSIGN_DIM проецирует элементы массива на ячейки памяти и возвращает указатель на ту область памяти, где находится нужный нам элемент. Указатель неявно помещается в $4. В данном случае, судя по операндам и коду, нас интересует элемент с индексом 1 в массиве !0.
Далее, OP_DATA пишет результат функции str_replace() из $3 в область памяти, указатель на которую хранится в $4.
Не понятно почему в данном случае не используется ASSIGN. Видимо это связано со спецификой хранения данных в виде массива. Хотя при модификации указателей тоже используется ASSIGN (правда там ASSIGN_REF).
Если кто-то точно знает, что за связка ASSIGN_DIM\OP_DATA и как она действительно работает, напишите пожалуйста в комментах, буду благодарен.
Далее по коду, мы вызываем explode(), для того чтобы разбить строку по символу "|":
line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6
- Отправляем в качестве параметра символ "|"
- Считываем из массива !0 элемент с индексом 1 и записываем результат в $5
- Передаем $5 в качестве параметра
- Вызываем explode(), записывая результат в $6
- Копируем содержимое $6 в !1
Далее, мы выбираем рандомный элемент из получившегося массива:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11
- Отправляем !1 (массив) на вход функции array_rand(), которая, в свою очередь возвращает результат в $8
- Затем записываем в $9 элемент массива !1 с индексом из $8
- Записываем в $10 элемент массива !0 с индексом 2
- Объединяем $9 и $10 в ~11
- Возвращаем содержимое ~11
Сейчас я сознательно не расписываю всё более детально, т.к. считаю, что это уже итак понятно, ведь я всё это расписывал ранее.
Вот мы собственно и разобрали наш первый вариант алгоритма, что называется, по косточкам.
Теперь давайте двинемся дальше.
Поддержка экранированных символов в тексте
Немного подумав, я решил, что в тексте может быть любой порядок слов, не обязательно {...} текст. Да и в тексте могут встречаться такие символы как "{", "}" и "|" которые не должны быть обработаны как разделители или группы вариантов.
Исходя из этой задумки, решил, что если необходимо использовать "{", "}" и "|" в тексте, то их надо просто заэкранировать слешем, например "\{".
Да и нелепые ограничения типа необходимости пробелов в конце строки и между стоящими рядом группами, немного напрягали.
Составил себе следующую тестовую строку:
В языке {{C++|C}|{JavaScript|PHP}|C#|Java}, блоки кода можно объединять в фигурные скобки, например \{{ВАШ КОД|КАКОЙ-ТО КОД};\}\nУсловия записываются так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}
Из которой должно получится что-то вроде:
В языке Java, блоки кода можно объединять в фигурные скобки, например {ВАШ КОД;}
Условия записываются так if(1||0){do_some_work();}
Для реализации такого варианта, потребовалось переработать регулярное выражение.
Именно во время обдумывание данного варианта, я набил руку в использовании утверждений в регулярных выражениях. Отсюда вывод — придумывайте себе задачки, для решения которых нужно иметь знания, которых у вас нет. Или есть, но не было времени их укрепить.
Получилось следующее:
$str = 'В языке {{C++|C}|{JavaScript|PHP}|C#|Java} блоки кода можно объединять в фигурные скобки, например \{{ВАШ КОД|КАКОЙ-ТО КОД};\}<br>Условия записываются так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}';
$str = preg_replace_callback('#(?<!\\\)(\{[\s\S]+?(?<!\\\)\})(?![\|\}])#', function($mathces)
{
$mathces[1] = preg_replace('#(?<!\\\)\{|(?<!\\\)\}#', '', $mathces[1]);
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str)."\n";
echo $str;
Регулярка работает следующим образом:
- Находим вхождение символа "{" перед которым не стоит слеш (это показатель того, что символ не экранирован, а значит является началом группы вариантов)
- Забираем любые символы до символа "}", перед которым нет слеша. Это значит, что даже если нам попадутся экранированные "}", они не будут считаться концом группы, т.к. по условию, мы забираем все символы до не экранированного "}"
- (?![\|\}]) — этим утверждением мы как бы говорим, что после закрывающегося символа "}" не должно быть символов "|" и "}". Тем самым, мы хотим быть уверены, что обрабатываем группу минимального уровня вложенности (то есть глобальную группу)
Да, на этот вариант не действую ограничения прошлого варианта, но и у него есть свои тонкости. Например, данную регулярку можно легко свести с толка, вставив после любого закрывающего символа "}", символ "|".
Например:
В языке {{C++|C}|{JavaScript|PHP}|C#|Java}| блоки кода...........
Если так сделать, то алгоритм просто сожрет часть текста, посчитав его вариантом.
Но тут, как и в прошлом варианте, дело, скорее всего, в составленной регулярке.
Позже мы увидим как обойти и это ограничение. А пока, давайте посмотрим что еще изменилось в алгоритме и рассмотрим дамп opcodes.
Итак, в данном варианте, помимо регулярки, поменялось еще и содержимое лямбды и конец скрипта.
В конце скрипта, мы, с помощью preg_replace(), убираем все не экранированные слеши, чтобы превратить такие места как:
\{do_something();\}
в
{do_something();}
В лямбде, с помощью preg_replace(), мы чистим строку от не экранированных символов "{" и "}", чтобы остались только голые варианты, разделенные символом "|".
Затем, через preg_split(), мы получаем только те фразы, которые разделены символом "|", которому не предшествует слеш, то есть экранированный "|" не будет считаться разделителем.
Ну и возвращаем результат в виде рандомного элемента массива.
Opcodes данного примера
filename: /www/patterns/www/scan/advence.php
function name: (null)
number of ops: 21
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'много символов'
2 DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 INIT_ARRAY ~4 '%5C%7B'
8 ADD_ARRAY_ELEMENT ~4 '%5C%7D'
9 ADD_ARRAY_ELEMENT ~4 '%5C%7C'
10 SEND_VAL ~4
11 INIT_ARRAY ~5 '%7B'
12 ADD_ARRAY_ELEMENT ~5 '%7D'
13 ADD_ARRAY_ELEMENT ~5 '%7C'
14 SEND_VAL ~5
15 SEND_VAR !0
16 DO_FCALL 3 $6 'str_replace'
17 CONCAT ~7 $6, '%0A'
18 ASSIGN !0, ~7
9 19 ECHO !0
20 > RETURN 1
Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19:
filename: /www/patterns/www/scan/advence.php
function name: {closure}
number of ops: 18
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0
4 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7B%7C%28%3F%3C%21%5C%5C%29%5C%7D%23'
2 SEND_VAL ''
3 FETCH_DIM_R $1 !0, 1
4 SEND_VAR $1
5 DO_FCALL 3 $2 'preg_replace'
6 ASSIGN_DIM !0, 1
7 OP_DATA $2, $3
5 8 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
9 FETCH_DIM_R $4 !0, 1
10 SEND_VAR $4
11 DO_FCALL 2 $5 'preg_split'
12 ASSIGN !1, $5
6 13 SEND_VAR !1
14 DO_FCALL 1 $7 'array_rand'
15 FETCH_DIM_R $8 !1, $7
16 > RETURN $8
7 17* > RETURN null
End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19.
Попробуйте сами разобрать этот дамп, тут ничего нового нет.
Ну вот я и рассказал про второй вариант. Остался последний и самый интересный, как с точки зрения регулярки, так и с точки зрения рассмотрения opcodes.
Рекурсия
После того, как дискуссия на ветке форума была прекращена, в топик пришел некий товарищ и сказал, что мол «задачка уже старая и решали ее давно» и привел пример предложения, которое надо было разобрать таким образом:
{Пожалуйста|Просто} сделайте так, чтобы это предложение {изменялось {Быстро|Мгновенно} случайным образом}
И тут мне стало понятно, что нужно вводить поддержку уровня вложенности группы вариантов, а следовательно — нужна рекурсия. Причем ясно, что количество проходов равно максимальному уровню вложенности в тексте.
Начал думать. В результате чего и родился последний вариант, которым я доволен. Барабанная дробь…
while(preg_match('#(?<!\\\)\{#', $str))
{
$str = preg_replace_callback('#(?<!\\\)\{((?(?!\\\)[^\{]+?|[\s\S]+?))(?<!\\\)\}#', function($mathces)
{
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
}
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str);
echo $str;
Единственное, я придумал свой вариант исходной строки, в котором было бы задействовано всё, что только можно:
{{Сегодня {утром|после обеда}}|Вчера} я {побежал|пошел|поехал{ на автобусе| на машине| на {трамвае|троллейбусе}|}} в {зоо-|компьютерный|интимный|продуктовый} магазин чтобы {купить|украсть} {костюм {совы|{человека паука|бэтмена}|Винни-Пуха|колобка}|презерватив}
После обработки, данная строка, по идее, может превратиться в нечто такое:
Сегодня утром я побежал в зоо- магазин чтобы украсть презерватив
Сначала, давайте рассмотрим регулярку и лямбду.
Находим вхождение не экранированного символа "{"
Далее, нам надо забрать любые символы до первого не экранированного символа "}", то есть, говоря другими словами, мы ищем все самые глубокие группы
Забор происходит по условию. Условие построено таким образом, чтобы механизм регулярных выражений точно определял начало и конец строки, как бы не оставляя ему другого выбора. Без такого условия, были бы проблемы со строками типа:
Привет {{Виктор|{Антон|Антонио|Антошка}|Сергей}|{Господин|Сэр|Товарищ}} как {твои|ваши} дела
Т.к. в данном случае, регулярное выражение посчитало бы началом группы — "{Виктор", а на самом деле, началом первой группы является "{Антон".
В общем вся эта круговерть продолжается до тех пор, пока в тексте остались не экранированные "{". То есть после каждого прохода preg_replace_callback() — происходит подъем уровня вложенности и процесс повторяется до наступления указанного выше события.
Вы конечно можете сказать что всё это можно сделать и более простым регулярным выражением, но вспомните про поддержку "{", "}" и "|".
Да, она здесь тоже есть. И что самое главное — отсутствуют ограничения на формат текста (на сколько я мог заметить во время тестирования алгоритма).
Теперь давайте разберем то, ради чего мы все тут собрались.
Opcode dump
filename: /www/patterns/www/scan/advence2.php
function name: (null)
number of ops: 25
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'много символов'
2 1 > SEND_VAL регулярка для проверки на существование в строке не экранированных "{"
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12
4 5 > SEND_VAL 'длинная регулярка'
6 DECLARE_LAMBDA_FUNCTION 'внутреннее имя анонимной функции'
8 7 SEND_VAL ~2
8 SEND_VAR !0
9 DO_FCALL 3 $3 'preg_replace_callback'
10 ASSIGN !0, $3
9 11 > JMP ->1
10 12 > INIT_ARRAY ~5 '%5C%7B'
13 ADD_ARRAY_ELEMENT ~5 '%5C%7D'
14 ADD_ARRAY_ELEMENT ~5 '%5C%7C'
15 SEND_VAL ~5
16 INIT_ARRAY ~6 '%7B'
17 ADD_ARRAY_ELEMENT ~6 '%7D'
18 ADD_ARRAY_ELEMENT ~6 '%7C'
19 SEND_VAL ~6
20 SEND_VAR !0
21 DO_FCALL 3 $7 'str_replace'
22 ASSIGN !0, $7
11 23 ECHO !0
24 > RETURN 1
Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526:
filename: /www/patterns/www/scan/advence2.php
function name: {closure}
number of ops: 11
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 0 > RECV !0
6 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
2 FETCH_DIM_R $0 !0, 1
3 SEND_VAR $0
4 DO_FCALL 2 $1 'preg_split'
5 ASSIGN !1, $1
7 6 SEND_VAR !1
7 DO_FCALL 1 $3 'array_rand'
8 FETCH_DIM_R $4 !1, $3
9 > RETURN $4
8 10* > RETURN null
End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526.
Лямбду разбирать не будем, она простая до безобразия, а вот основной код разберем, тем более что я уже слышу недоумевающие и полные интереса возгласы, которые прямо таки вопрошают рассказать им про появившиеся стрелочки (хотя мы видели их и раньше) и новый для нас вид opcodes — JMP*
Давайте по порядку. Обратим внимание на:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 1 > SEND_VAL 'регулярка для проверки на существование в строке не экранированных "{"'
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12
Это while в нашем коде. Тут мы передаем в функцию preg_match() аргументы в виде регулярки и строки, которую будем проверять на вхождения. Вызываем функцию, помещаем результат в $1.
Теперь внимательно. Opcode JPMZ — это управляющий opcode, который делает следующее:
Если операнд равен нулю, то opcode передает управление в другое место, потому JMPZ расшифровывается как «Jump If Zero».
Судя по дампу, мы можем смело сказать, что если содержимое $1 будет равно 0, то управление перейдет к opcode под номером 12.
А что там?
line # * op fetch ext return operands
---------------------------------------------------------------------------------
10 12 > INIT_ARRAY ~5 '%5C%7B'
А там 10 строка нашего скрипта, то есть мы уже находимся за пределами while.
Теперь вспоминаем как работает while.
Если условное выражение, которое мы передаем в while, вернет нам что-либо отличное от нуля, то выполнится тело while, но если оно вернет на 0 или эквивалент (пустая строка, пустой массив, false и т.д.), то нас выкинет из while. Что мы здесь собственно и наблюдаем.
Еще раз взгляните на участок opcodes отвечающих за реализацию while.
Если то, что вернула нам функция preg_match() равно 0, то выйти из цикла. preg_match() вернет нам 0, если совпадения небыли найдены и нас выкинет из while.
Далее посмотрите на еще один JMP:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
9 11 > JMP ->1
Это, так называемая, безусловная передача управления, находится она, как видим, на 9 строке скрипта. А там у нас закрывающая скобка while. Что произойдет? Правильно! Очередная проверка условия перед телом цикла. Смотрим на opcode — JMP отправляет нас к opcode под номером 1. А что там? А там то, что мы уже рассматривали.
Вот собственно и вся логика. Обратите внимание, что перед именами некоторых opcodes, есть стрелки указывающие направо. Но здесь нас интересует не направление стрелок и а их расположение. Если стрелка расположена по левому краю своего столбца, то это означает вход в участок кода. Это может быть цель условного или безусловного перехода, или просто открывающаяся командная скобка.
Стрелки выровненные по правую сторону, означают выход из участка кода. Это может быть условный или безусловный переход, или закрывающаяся командная скобка. Так же обратите внимание, что перед opcode RETURN стрелка прижата к правому краю. Думаю здесь не нужно ничего пояснять.
Ну что, вот собственно и всё, о чем я хотел рассказать в этой статье.
Конечно, можно было бы замерит производительность всех трех вариантов (и я это делал), но, уверяю вас, что всё зависит от задачи и на разных текстах производительность будет разная.
Для дампа opcodes, использовался модуль VLD
PHP версии 5.5.14
Остальные ссылки указаны в тексте.
Надеюсь, что дал вам новые знания и вы смогли лучше понять работу php.
Спасибо за внимание и всего вам самого хорошего!