Парсер на РНР - це можливо! h2>
Антон Калмиков h2>
У даній коротенькій статті я хочу продемонструвати,
що РНР може дуже добре справлятися з функцією синтаксичного розбору
виразів. Для тих, хто ніколи не стосувався даної тематики, я думаю, стаття
буде так само цікава, оскільки в ній ми розглянемо метод програмування в
вигляді кінцевих автоматів. p>
Почну з твердження, що метод програмування з
застосуванням кінцевих автоматів дуже простий, оскільки більша частина програми
міститься всередині автомата, який ви готуєте заздалегідь у вигляді матриці і
використовуєте в своїй програмі. p>
Що ж таке автомат? p>
Уявіть собі дискретну функцію від двох аргументів
Ft (d, Ft-1). Як перший аргумент ми використовуємо кінцеве лічильний
безліч (масив даних), яка поступає ззовні. На кожному кроці у функцію
надходить тільки одне число з даного масиву. Другим аргументом функції
є значення функції на попередньому кроці. Додам ще одна умова. Область
значень даної функції являє собою кінцеве рахункове безліч. p>
У чому привабливість такої функції? Вся краса полягає в
тому, то ми можемо уявити її у вигляді матриці, де номери рядків будуть задавати
дані, що надходять, а номери стовпців будуть представляти область значень
функції. Тоді, записавши в клітинку (рядок, стовпець) число з безлічі значень
функції, ми отримаємо матрицю, яка описує залежність функції від вхідних
даних і всього спектру значень. Будемо називати число з безлічі значень
СТАНОМ, а функцію Автомата. p>
Якщо ви не зрозуміли попереднього абзацу, то не лякайтеся, на
практиці все виглядає набагато простіше. Давайте розглянемо простий приклад.
Припустимо, нам треба побудувати розбір звичайного арифметичного виразу. Для
цього нам доведеться створити два автомати. Перший буде виділяти
"слова" з буфера даних (тобто сканувати його). Другий буде
перевіряти граматичний порядок проходження слів у виразі. p>
Почнемо зі сканера. Словами є знаки операцій +, -,
*,/І послідовності символів, якщо вони не містять роздільників, такі
як новий рядок, пробіл і символ табуляції. Роздільники ми будемо просто
ігнорувати. Автомат для сканера, в цьому випадку, буде наступним. P>
//
стану 0, 1, 2 p>
"0"
=> Array (0, -1, -1),// розділювач p>
"1"
=> Array (2, -1, -1),// слово з одного символу p>
"2"
=> Array (1, 1, -1),// символ p>
Номери рядків задають тип символу, оскільки нам треба
виділити знаки операцій в окремі слова. Стану (номери стовпчиків) будуть
означати наступне. p>
-1 слово
готове, пора повертати p>
0 початок
сканування p>
1 отримали
символ, треба збирати поки це символ p>
2 отримали
зумовлене слово з одного символу p>
в стані 1 ми будемо збирати символи, щоб повернути їх
як слово в змозі -1. Наш сканер буде викликатися з парсера і завершувати
свою роботу, коли він розпізнає хоча б одне "слово", тому немає
сенсу вводити стан -1 до таблиці автомата. Для парсера автомат буде
такий. p>
//
стану 0, 1,
2, 3, 4, 5 p>
"0"
=> Array (1, -1, 1, 1,
1, 1),// оператор p>
"1"
=> Array (2, 4, -1, 2, -1, -1),// операнд p>
"2"
=> Array (3, 3, -1, 3, -1, -1),// ліва дужка p>
"3"
=> Array (-1, -1, 5, -1, 5, 5),
//Права дужка p>
а стану відповідно p>
-1 Помилка p>
0 Початок розбору p>
1 Отримали
оператор, чекаємо правий операнд p>
або ліву
дужку p>
2 Отримали лівий
операнд (треба перевірити чи число це), p>
чекаємо оператор
або праву дужку p>
3 Отримали ліву
дужку, p>
очікуємо
оператор або ліву дужку p>
4 Отримали правий
операнд (треба перевірити чи число це), p>
очікуємо
оператор або праву дужку p>
5 Отримали праву
дужку, чекаємо оператор p>
Парсер завершить роботу, коли сканер поверне FALSE або при
виникненні помилки - стан -1. З тієї ж причини, що і в сканері ми
можемо не вносити стан -1 до таблиці автомата p>
Далі привожу код програми з докладними коментарями,
які замінять подальші пояснення. Я не будую дерева операцій у прикладі
даного парсера. Ви можете зробити це самі, адже у відповідних станах
автомата ви отримаєте оператор і операнди. p>
php p>
class ExpressionParser ( p>
var $ pos,// Позиція в буфері для розбору p>
$ length,// Довжина буфера p>
$ line,// Поточний номер рядка p>
$ column,// Поточний номер колонки в рядку p>
$ data,// Буфер даних p>
$ brackets,// Кількість відкритих дужок p>
$ state,// Поточний стан парсера p>
$ errorstr,// Рядок діагностики помилки p>
$ instates,// Код слова що подається на вхід автомата p>
$ prevstate,//
Попереднє стан парсера p>
$ automat;// Таблиця автомата парсера p>
/********************************************** ************************
p>
* Конструктор * p>
************************************************** ********************/ p>
function
ExpressionParser ($ str) ( p>
$ this-> data = $ str; p>
$ this-> length = strlen ($ str); p>
$ this-> pos = 0; p>
$ this-> line = 1; p>
$ this-> column = 0; p>
$ this-> brackets = 0; p>
p>
// Коди слів, виданих сканером, що подаються
на вхід парсера p>
// Решта слова
мають код 1 p>
$ this-> instates
= Array ( "+" => 0, "*" => 0, "-" => 0, "/"
=> 0, "(" => 2, ")" => 3); p>
p>
// Автомат парсера p>
$ this-> automat = array ( p>
/* p>
-1 Помилка p>
0 Початок розбору
p>
1 Отримали оператор,
чекаємо правий операнд або ліву дужку p>
2 Отримали лівий
операнд (треба перевірити чи число це), чекаємо оператор або праву дужку p>
3 Отримали ліву
дужку, чекаємо оператор або ліву дужку p>
4 Отримали правий
операнд (треба перевірити на число), чекаємо оператор або праву дужку p>
5 Отримали праву
дужку, чекаємо оператор p>
*/ p>
// стани 0, 1, 2, 3, 4, 5 p>
"0" => array (
1, -1, 1, 1, 1, 1),// оператор p>
"1" => array (
2, 4, -1, 2, -1, -1),// операнд p>
"2" => array (
3, 3, -1, 3, -1, -1),// ліва дужка p>
"3" => array (-1, -1, 5, -1,
5, 5),// права дужка p>
); p>
$ this-> state = $ this-> prevstate = 0; p>
) p>
/********************************************** ************************
p>
* Сканер * p>
*********************************************** ***********************/
p>
function Scan () (
p>
// Роздільники,
які ігноруємо p>
$ delimiters = array ( "
"," t "," r "," n "); p>
// Слова з одного символу p>
$ words = array ("+","-","*","/","(",")");
p>
// автомат сканера
p>
$ automat = array (
p>
/* p>
-1 слово готове,
час повертати p>
0 початок сканування
p>
1 отримали символ,
треба збирати поки це символ p>
2 отримали зумовлене
слово з одного символу p>
*/ p>
// стани 0, 1, 2 p>
"0" => array (
0, -1, -1),// розділювач p>
"1" => array (
2, -1, -1),// слово з одного символу p>
"2" => array (
1, 1, -1),// символ p>
); p>
$ state = 0; p>
$ word = "";
p>
p>
// Цикл сканування
p>
while ($ this-> pos <$ this-> length) (
p>
// Встановлюємо код що подається на
вхід автомата символу. p>
if
(in_array ($ this-> data [$ this-> pos], $ delimiters)) p>
$ instate = 0;
p>
elseif
(in_array ($ this-> data [$ this-> pos], $ words)) p>
$ instate = 1; p>
else p>
$ instate = 2; p>
p>
// Отримуємо стан
автомата p>
$ state = $ automat [$ instate] [$ state];
p>
p>
// Наші дії
по станів автомата p>
switch ($ state) ( p>
case 0:// початок сканування p>
if
($ this-> data [$ this-> pos] == "n") ( p>
$ this-> line + +; p>
$ this-> column = 0; p>
) p>
$ word = ""; p>
break; p>
case -1:// слово
готове, пора повертати p>
if (strlen ($ word)) return $ word; p>
break; p>
case 1:// отримали
символ, треба збирати поки це символ p>
$ word .= $ this-> data [$ this-> pos]; p>
break; p>
case 2:// отримали
зумовлене слово з одного символу p>
$ word = $ this-> data [$ this-> pos]; p>
break; p>
) p>
$ this-> pos + +; p>
$ this-> column + +; p>
if
($ this-> pos == $ this-> length & & strlen ($ word)) return $ word; p>
) p>
return
false; p>
) p>
/********************************************** ************************
p>
* Парсер * p>
************************************************** ********************/ p>
function
Parse () ( p>
// Змінна $ first дорівнює нулю, якщо
функція розбору була викликана перший раз p>
$ first = $ this-> pos; p>
// Цикл станів p>
while (1) ( p>
p>
// Отримуємо слово від сканера p>
$ word = $ this-> Scan ();
p>
p>
// Якщо слів більше
ні, то перериваємо цикл p>
if ($ word === false)
break; p>
p>
// Встановлюємо
код, що подається на вхід автомата, слова p>
$ instate = isset ($ this-> instates [$ word])?
$ this-> instates [$ word]
: 1; p>
p>
// Отримуємо стан
автомата парсера p>
$ this-> state = $ this-> automat [$ instate] [$ this-> state];
p>
p>
// Якщо помилкове стан, то перериваємо
цикл p>
if ($ this-> state ==- 1) ( p>
$ this-> errorstr = "Помилка в рядку: $ this-> line, колонка: $ this-> column
"; p>
break; p>
) p>
p>
// Наші дії
по станів автомата парсера p>
switch ($ this-> state)
( P>
case 1:// Отримали
оператор, чекаємо правий операнд або ліву дужку p>
p>
// Якщо перше слово
оператор, то це може бути тільки "+" або "-" p>
if (($ this-> prevstate == 3 | |
$ this-> prevstate == 0) & & $ word !="-" & &
$ word !="+") ( p>
$ this-> errorstr = "Помилка в рядку: $ this-> line, колонка: $ this-> column
"; p>
return false; p>
) p>
break; p>
case 2:// Отримали
лівий операнд (треба перевірити чи число це), чекаємо оператор p>
// або праву
дужку p>
// Перевіряємо число
Чи це так? p>
if
(! preg_match ("/^[ 0-9 ]+(.[ 0-9 ]+)?$/",$ word)) ( p>
$ this-> errorstr = "Помилка в рядку: $ this-> line, колонка: $ this-> column
"; p>
return false; p>
) p>
break; p>
case 3:// Отримали
ліву дужку, чекаємо оператор або ліву дужку p>
// Збільшуємо кол-во
відкритих дужок на 1; p>
$ this-> brackets + +;
p>
p>
// Зручно використовувати
рекурсію, тому що дані в дужках p>
// можна розглядати
як самоcтоятельние вирази. p>
// Ми повернемося з
функції у разі помилки, кінця даних або p>
// після отримання закритої дужки p>
if
(! $ this-> Parse ()) return false; p>
break; p>
case 4:// Отримали правий операнд
(треба перевірити чи число це), чекаємо оператор p>
// або праву
дужку p>
// Перевіряємо число
Чи це так? p>
if
(! preg_match ("/^[ 0-9 ]+(.[ 0-9 ]+)?$/",$ word)) ( p>
$ this-> errorstr = "Помилка в рядку: $ this-> line, колонка: $ this-> column
"; p>
return false; p>
) p>
break; p>
case 5:// Отримали
праву дужку, чекаємо оператор p>
p>
// Зменшуємо кол-во
відкритих дужок на 1 p>
$ this-> brackets -; p>
return true;
p>
)// end
switch p>
p>
// Запам'ятовуємо поточний стан для
наступного кроку циклу p>
$ this-> prevstate = $ this-> state; p>
)// end
while p>
// Так як у нас відсутня стан
кінця розбору, то треба p>
// Перевірити в якому
стані ми завершили розбір p>
// Це треба робити
тільки один раз на самому першому виклику p>
// функції розбору.
Це перший виклик, якщо $ first == 0 p>
// Отже, ми повинні
повернути помилку, якщо в нас є зайві дужки, p>
// або якщо ми не
отримали правого операнда або правої дужки, p>
// тобто розбір завершився
"на середині". p>
p>
if (! $ first & & ($ this-> brackets
| | $ This-> state! = 4 & & $ this-> state! = 5)) return false; p>
p>
return true;
p>
) p>
p>
) p>
$ p = new
ExpressionParser ( "-4.25 * ((2 +3) * 4 +1)/5"); p>
print $ p-> data. "
"; p>
if ($ p-> Parse ()) p>
print "Вираз коректно.
";
p>
else p>
print
$ p-> errorstr; p>
?> p>
Список літератури h2>
Для підготовки даної роботи були використані матеріали
з сайту http://www.realcoding.net/
p>