Синтаксичний розбір рядків і кінцеві автомати h2>
Андрій Боровський
h2>
У цій статті мова піде про те, як
аналізувати інформацію, передану у вигляді послідовності символів (рядок)
і виділяти з неї значущі елементи. Ми розглянемо порівняно прості
ситуації, з якими програмістам доводиться стикатися при рішенні самих
різних завдань: розбір виразів з простою синтаксичною структурою, але з
досить вільними правилами запису. p>
Припустимо, у програмі, яку ви пишете, потрібен
модуль, що аналізує текст HTML-сторінки. Ми напишемо функцію, яка, отримавши
рядок, що містить тег, отримувала б з цього рядка всі атрибути тега та їх
значення. Структуру тега можна схематично представити наступним чином:
<ТЕГ атрібут1 = "значення" атрібут2 = "значення" ...>
На перший погляд завдання здається дуже простою, однак ситуація ускладнюється
через досить м'яких правил мови HTML. Між ім'ям атрибута, знаком
рівності і значенням може стояти будь-яке число розділових символів
(прогалин, символів табуляції і навіть символів переходу на нову строку), або ж
розділові символи можуть взагалі бути відсутнім. Значення атрибутів можуть
бути або у лапках, або ні, при цьому значення, укладене в
подвійні лапки, може містити символи одинарних лапок, і навпаки. Крім
того, не всім атрибутів тегів присвоюються значення. p>
Для вирішення зазначеної проблеми ми напишемо функцію
ParseTag, що аналізує переданий їй тег й створює списки атрибутів тега і
їх значень. Функція ParseTag діє за принципом кінцевого автомата.
Кінцеві автомати та подібні їм структури широко застосовуються при обробці
рядків. Сфери найбільш частого застосування кінцевих автоматів входить пошук
підрядка по заданому зразком, обробку регулярних виразів (regular
expressions), лексичний та синтаксичний аналіз. Кінцеві автомати широко
застосовуються в трансляторах і інтерпретатора (не кажучи вже про такі завдання,
як проектування логічних пристроїв). p>
Суворе визначення кінцевих автоматів можна знайти в
будь-якому підручнику з теорії алгоритмів, ми ж тут обмежимося інтуїтивним
визначенням. У кожен даний момент часу кінцевий автомат може перебувати
в одному з можливих станів (число станів, у яких може знаходитися
кінцевий автомат - звичайно). Автомат послідовно зчитує символи вхідного
тексту (рядки). Кожен лічений символ або переводить автомат в нове
стан, або залишає його в колишньому стані. Формально автомат можна описати
за допомогою функції переходів. Аргументами цієї функції є попереднє
стан автомата і черговий лічений символ, а значенням - новий стан
автомата. p>
Безліч станів для нашого автомата включає: p>
ReadTag - читає ім'я тега; p>
WaitAttr - очікує ім'я атрибута; p>
WaitAttrOrEq - очікує ім'я атрибута або символ '='; p>
ReadAttr - читає ім'я атрибута; p>
WaitValue - очікує значення ознаки; p>
ReadValue - читає значення атрибуту без лапок; p>
ReadValueSQ - читає значення атрибута в одинарних лапках;
p>
ReadValueDQ - читає значення атрибута в подвійних
лапках. p>
Дотримуючись термінології кінцевих автоматів, ми можемо
назвати стану WaitAttr, WaitAttrOrEq, ReadAttr і ReadValue допускають.
Це означає, що якщо після обробки переданої рядка автомат знаходиться в
будь-якому іншому стані, отже, тег містить помилку (автомат не перевіряє,
завершується чи рядок символом '>', це - завдання блоку, що викликає функцію
ParseTag). P>
Процес програмної реалізації автомата можна
спростити, побудувавши для нього діаграму переходів. Далі наводиться діаграма
переходів для нашого автомата. Цифри на діаграмі відповідають номерам
станів, перерахованих вище. p>
p>
Малюнок 1 p>
Пояснення до діаграми: p>
a - символ-розділювач p>
b - будь-який символ окрім роздільник p>
c - символ "=" p>
d - будь-який символ окрім роздільник або символ
"=" p>
e - за будь-символ крім роздільник і лапок p>
f - символ одинарних лапок p>
g - символ подвійних лапок p>
Нижче наводиться текст функції ParseTag і
допоміжної функції GetSubString. У функції ParseTag є чотири параметри:
рядок, що містить тег, укладений у '<' і '>', рядок, у якому
повертається ім'я тега, і об'єкти типу TStringList, що містять імена і значення
атрибутів відповідно. Якщо цього атрибуту не зіставляючи ніяке
значення, в списку значень назви ознаки відповідає порожній рядок. У
разі успішного виконання функція повертає значення 0, інакше --
1. p>
Автомат реалізований в тілі циклу функції ParseTag.
Додавання нового елемента в список здійснюється в момент переходу з
стану ReadXXX в який-небудь інший стан. Крім цього в цикл добавлена
перевірка помилок синтаксису, наприклад, двох символів '=', наступних підряд.
Після завершення циклу ми аналізуємо стану автомата. Якщо автомат
знаходиться в одному з станів ReadXXX, відбувається додавання останнього
елемента у відповідний список. Якщо автомат не знаходиться ні в одному з допускають
станів, функція повертає повідомлення про синтаксичну помилку. p>
function GetSubString (const S: String; Start, Stop: Integer): p>
String; p>
begin p>
SetLength (Result, Stop-Start); p>
Move (S [Start], Result [1],
Stop-Start); p>
end; p>
function ParseTag (const Tag: String; var TagName: String; p>
Attrs, Values: TStringList):
Integer; p>
type p>
// Можливі стану p>
TState = (ReadTag, WaitAttr,
WaitAttrOrEq, ReadAttr, WaitValue, p>
ReadValue, ReadValueSQ,
ReadValueDQ); p>
const p>
// Значення, що повертаються функцією GetLink p>
resOK = 0;// розбір пройшов успішно p>
resBadSyntax = -1;// синтаксична помилка p>
// Набір можливих розділових символів p>
Delimeters = [ '', # 9, # 13, # 10]; p>
var p>
State: TState; p>
StartPos, i: Integer; p>
begin p>
Result: = resOK; p>
// очищаємо список елементів p>
Attrs.Clear; p>
Values.Clear; p>
State: = ReadTag;// вхідна стан автомата p>
i: = 2;// пропускаємо символ'<' p>
while (Tag [i ]<>'>')
and (i
begin p>
case State of p>
ReadTag: p>
if Tag [i] in Delimeters
then p>
begin p>
// читання імені тега закінчено p>
TagName: =
GetSubString (Tag, StartPos, i); p>
State: = WaitAttr; p>
end; p>
WaitAttr: p>
if (Tag [i] in Delimeters)
= False then p>
begin p>
if Tag [i] = '=' then p>
begin p>
Result: =
resBadSyntax; p>
Exit; p>
end; p>
StartPos: = i; p>
State: = ReadAttr; p>
end; p>
ReadAttr: p>
if (Tag [i] in Delimeters)
or (Tag [i] = '=') then p>
begin p>
// читання імені атрибута закінчено,
додаємо ім'я атрибута до списку p>
Attrs.Add (GetSubString (Tag,
StartPos, i )); p>
if Tag [i] = '=' then
State: = WaitValue p>
else State: =
WaitAttrOrEq; p>
end; p>
WaitAttrOrEq: p>
if (Tag [i] in Delimeters)
= False then p>
begin p>
if Tag [i] = '=' then
State: = WaitValue else p>
begin p>
// починається читання назви ознаки p>
// попереднього атрибути не
присвоєно ніяких значень, p>
// додаємо пустий рядок в список
Values p>
Values.Add (''); p>
State: = ReadAttr; p>
StartPos: = i; p>
end; p>
end; p>
WaitValue: p>
if (Tag [i] in Delimeters)
= False then p>
begin p>
if Tag [i] = '=' then p>
begin p>
// два символи '=' поспіль p>
Result: = resBadSyntax; p>
Exit; p>
end; p>
if Tag [i] = ' "'
then p>
begin p>
// читання значення почнеться з
наступного символу після лапок: p>
StartPos: = i + 1; p>
State: = ReadValueDQ; p>
end else p>
if Tag [i] =''''then p>
begin p>
// читання значення почнеться з наступного символу
після лапок: p>
StartPos: = i + 1; p>
State: = ReadValueSQ; p>
end else p>
begin p>
// читання значення без лапок p>
StartPos: = i; p>
State: = ReadValue; p>
end; p>
end; p>
ReadValue: p>
if Tag [i] in Delimeters
then p>
begin p>
// читання значення закінчено p>
Values.Add (GetSubString (Tag, StartPos, i )); p>
State: = WaitAttr; p>
end; p>
ReadValueDQ: p>
if Tag [i] = ' "' then p>
begin p>
// читання значення в подвійних лапках
закінчено p>
Values.Add (GetSubString (Tag,
StartPos, i )); p>
State: = WaitAttr; p>
end; p>
ReadValueSQ: p>
if Tag [i] =''''then p>
begin p>
// читання значення в одинарних
лапках закінчено p>
Values.Add (GetSubString (Tag,
StartPos, i )); p>
State: = WaitAttr; p>
end; p>
end;// case State of p>
Inc (i); p>
end;// while
(Body [i ]<>'>') and (i
// перевіряємо стан автомата після обробки рядка p>
// останнім символом рядка повинен бути
'>' p>
case State of p>
ReadValue:
Values.Add (GetSubString (Tag, StartPos, i)); p>
ReadAttr: Attrs.Add (GetSubString (Tag,
StartPos, i )); p>
ReadTag: TagName: =
GetSubString (Tag, StartPos, i); p>
WaitAttr, WaitAttrOrEq:;//
нічого не робимо p>
else Result: = resBadSyntax;// інші стани неприпустимі p>
end; p>
end; p>
Однією з важливих особливостей такого підходу до розбору
строк є те, що аналіз виконується в міру зчитування символів, з
використанням інформації про поточний символі і символів, прочитаних раніше. Це
дозволяє вести обробку даних, що передаються по деякому послідовному
каналу, безпосередньо в процесі їх надходження. p>
Фактично представлена функція виконує дві
операції: виділяє в переданої рядку синтаксичні елементи (tokens) і
визначає, що являє собою цей елемент (ім'я тега, ім'я атрибуту,
значення атрибута). Рішення про те, чим є наступний елемент, що приймається
заздалегідь, на підставі даних про попередньому елементі і простих правил: за ім'ям
тега слід ім'я атрибута; за ім'ям атрибута слід або ім'я атрибута, або
символ '='; за символом '=' слід значення атрибута. p>
Процедури, засновані на кінцевих автоматах, широко
застосовуються для перевірки синтаксису. В якості прикладу розглянемо функцію
CheckMath, яка виконує синтаксичний аналіз математичного вирази: p>
function CheckMath (const S: String): Integer; p>
type p>
TState = (Start, InDigit,
AfterDigit, InOp, InLPrnt, InRPrnt); p>
const p>
resLPrntMissing = -1; p>
resRPrntMissing = -2; p>
var p>
State: TState; p>
i, ParCount: Integer; p>
begin p>
Result: = 0; p>
ParCount: = 0;// лічильник дужок p>
State: = Start; p>
for i: = 1 to Length (S) do p>
case State of p>
Start:// вхідна стан p>
case S [i] of p>
'':;// стан не змінюється p>
'0 '.. '9': State: = InDigit; p>
'-': State: = InOp;// символ '-'
перед числом або дужкою p>
'(': p>
begin p>
Inc (ParCount); p>
State: = InLPrnt; p>
end; p>
else p>
begin p>
// Синтаксична помилка p>
Result: = i; p>
Exit; p>
end; p>
end; p>
InDigit: p>
case S [i] of p>
'0 '.. '9':;// стан не змінюється p>
'+', '-', '*', '/':
State: = InOp; p>
')': p>
begin p>
Dec (ParCount); p>
State: = InRPrnt; p>
end; p>
'': State: =
AfterDigit; p>
else p>
begin p>
Result: = i; p>
Exit; p>
end; p>
end; p>
AfterDigit: p>
case S [i] of p>
'':; p>
'+', '-', '*', '/':
State: = InOp; p>
')': p>
begin p>
Dec (ParCount); p>
State: = InRPrnt; p>
end; p>
else p>
begin p>
Result: = i; p>
Exit; p>
end; p>
end; p>
InOp: p>
case S [i] of p>
'':; p>
'0 '.. '9': State: =
InDigit; p>
'(': p>
begin p>
Inc (ParCount); p>
State: = InLPrnt; p>
end; p>
else p>
begin p>
Result: = i; p>
Exit; p>
end; p>
end; p>
InLPrnt: p>
case S [i] of p>
'0 '.. '9': State: =
InDigit; p>
'-': State: = InOp; p>
'(': Inc (ParCount); p>
'':; p>
else p>
begin p>
Result: = i; p>
Exit; p>
end; p>
end; p>
InRPrnt: p>
case S [i] of p>
'+', '-', '*', '/':
State: = InOp; p>
')': Dec (ParCount); p>
'':; p>
else p>
begin p>
Result: = i; p>
Exit; p>
end; p>
end; p>
end;// case State of p>
if State in [InLPrnt, InOp]
then// Неприпустимі стану p>
Result: = Length (S); p>
if ParCount> 0 then Result
: = ResRPrntMissing else p>
if ParCount <0 then Result
: = ResLPrntMissing; p>
end; p>
Вхідна математичний вираз може містити
цілочисельні константи, символи арифметичних операцій і дужки. Між
символами операцій, дужками і числами допустимо будь-яку кількість пробілів.
Функція CheckMath повертає значення 0, якщо передане їй вираз не містить
помилок. Якщо вираз містить помилку, функція повертає позитивне число,
відповідне позиції символу, в якій було виявлено помилку. Якщо число
відкритих дужок не дорівнює числу закритих, функція повертає або -1, або -2, в
залежно від того, яких дужок не вистачає. p>
У даній функції задіяні наступні стани: p>
Start - початковий стан; p>
InDigit - прочитана цифра; p>
AfterDigit - прочитаний роздільник після цифри; p>
InOp - прочитаний символ арифметичної операції; p>
InLPrnt - прочитана відкриває дужка; p>
InRPrnt - прочитана закриває дужка. p>
Символи пробілу не змінюють попереднього стану, за
винятком стану InDigit. Остання зроблено для того, щоб не допустити
появи прогалин між символами, що становлять чисельну константу. p>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>