Сообщений 24 Оценка 308 Оценить |
Множества всех строк, записанных по некоторым правилам, называются формальными языками. Так, любой язык программирования, язык написания чисел и адресов e-mail - это формальные языки.
Формальный язык описывается с помощью так называемых "правил вывода", которые позволяют построить все возможные строки языка (хотя обычно никто такую задачу не ставит). Правила вывода составляют "грамматику языка".
Наиболее известная форма представления грамматики - это "форма Бэкуса-Наура" (БНФ): множество правил вида
<элемент> ::= цепочка из символов и/или <элементов> |
что означает: в исходной цепочке можно заменить <элемент> (некоторый метасимвол, не входящий в алфавит языка) на другую цепочку.
Для сокращения записи используются регулярные выражения:
БНФ с элементами регулярных выражений называют Расширенной БНФ (РБНФ).
Конечный автомат (КА) - устройство, имеющее конечный набор состояний (им в данном случае соответствуют метасимволы), и конечный набор реакций на конечные же наборы входных и выходных сигналов (алфавиты).
состояние, входной сигнал -> новое состояние, выходной сигнал. |
Программа, реализующая КА, очень проста. Номер (идентификатор) состояния хранится в регистре. При подаче входного сигнала находится правило с совпадающими текущим состоянием и входом. В соответствии с правилом, устанавливается новое состояние и выдается выходной сигнал.
Если есть разные правила с одинаковым условием, такой автомат называется недетерминированным (фактически, непредсказуемым). Если же у всех правил условия различаются - детерминированным (однозначным).
Детерминированные КА (ДКА) можно реализовать как алгоритмически (последовательная проверка условий), так и в табличной либо функциональной форме:
новое_состояние [ состояние, входной_сигнал ] выходной_сигнал [ состояние, входной_сигнал ] |
Грамматику языка, состоящую только из правил вида
<A> ::= символ <B> |
(или которую можно привести к такому виду), называют "автоматной", так как существует конечный автомат, порождающий любые цепочки данного языка.
Автомат, порождающий строки языка, в общем случае является недетерминированным (на его входе, фактически, тактирующие импульсы). То есть, имея правила
A -> x B A -> y C |
Можем из состояния А выдать как x, так и y.
Собственно, это и приводит к богатству языка (ДКА может порождать только либо бесконечную строку из одинаковых символов, либо 1-символьную, либо пустую).
Обработка исходной строки сводится к следующим задачам:
Естественно, что трансляция включает в себя проверку: если строка неправильна, то перевести ее не удастся.
Для задач разбора (parsing, синтаксический анализ), то есть проверки и трансляции, строится автомат, на вход которого посимвольно подается исходная строка, а на выходе - какие-либо диагностические сигналы (правильно/неправильно/продолжать).
Этот автомат должен быть детерминированным (однозначным); в противном случае его работа непредсказуема.
ПРИМЕЧАНИЕ Вообще, разбирающий автомат может не быть конечным (так, для Паскаля и Си используются "магазинные" (стековые) автоматы, а для Фортрана и этого недостаточно). |
Фактически, такой автомат получается из порождающего:
порождающий: A, tact -> x, B разбирающий: A, x -> B, diagnostics |
Если получился недетерминированный автомат, то есть
A, x -> B A, x -> C |
то порождающий автомат можно перепроектировать, чтобы этого не было:
A -> x, B заменяется на A -> x, N (вводим новое состояние) A -> x, C заменяется на A -> x, N |
Ко всем правилам, содержащим в условии B и С, добавляются новые:
B -> y, D добавляется N -> y, D |
Ниже приведены способы написания правил для случаев, которые нас интересуют.
Выражение | Правила порождения | Правила разбора |
---|---|---|
последовательность x1 x2 x3 ... | R0 -> x1, Rx1 Rx1 -> x2, Rx2 ... | R0, x1 -> Rx1 Rx1, x2 -> Rx2 ... |
ветвление x1 x_branch | y1 y_branch | ... | R0 -> x1, Rx R0 -> y1, Ry ... | R0, x1 -> Rx R0, y1 -> Ry ... |
условная вставка [ x1 x_branch ] z1 ... эквивалентно x1 x_branch z1... | z1... | R0 -> x1, Rx R0 -> z1, Rz | R0, x1 -> Rx R0, z1 -> Rz |
повторение ( x1 ... xn )* z1... | R0 -> x1, Rx1 R0 -> z1, Rz Rxn -> x1, Rx1 Rxn -> z1, Rz | R0, x1 -> Rx1 R0, z1 -> Rz Rxn, x1 -> Rx1 Rxn, z1 -> Rz |
Здесь, R0 - исходное состояние перед порождением/разбором строки, а под z1 имеются в виду первые символы, которые могут идти в выражении, следующим за скобками.
Для каждого случая (целое число, вещественное число, адрес e-mail):
В БНФ, регулярных выражениях и сводах правил используется следующая нотация. Метасимволы и состояния - пишутся с большой буквы, базовые множества символов (такие как <digit> - цифры) - с маленькой. При этом имеем в виду, что
A, <digit> -> B |
развертывается в
A, 0 -> B; ... A, 9 -> B |
Итак, рассмотрим синтаксис искомых строк.
Самый простой случай - десятичное число (или в любой другой, наперед заданной системе счисления):
БНФ
<Number> ::= [ <sign> ] <Digits> <Digits> ::= <digit> [ <Digits> ] |
Регулярное выражение
<Number> ::= <sign> <digit> <digit>* |
Получаем достаточно очевидное решение (функции isSign, isDigit, isEnd могут быть реализованы любым способом, но наиболее простой способ - это сравнения).
Листинг 1. Функция checkIntБНФ
<Real_number> ::= <Mantissa> [ <E> <Exponent> ] <Mantissa> :: = [<sign>] <Integer_part> [ <dot> [<Fractional_part>] ] <Exponent> ::= [<sign>] <Unsigned_number> <Integer_part> ::= <Unsigned_number> <Fractional_part> ::= <Unsigned_number> <Unsigned_number> ::= <digit> <digit>* <sign> ::= + | - <e> ::= E | e <dot> ::= . |
Регулярное выражение
[<sign>] <digit> <digit>* [<dot> <digit>* ] [ <e> [<sign>] <digit> <digit>* ] |
Правила для автомата
Start = Mantissa1 Mantissa1 - начало мантиссы Mantissa1, <sign> -> Mantissa2 Mantissa1, <digit> -> Mantissa3 Mantissa2 - начало целой части мантиссы Mantissa2, <digit> -> Mantissa3 Mantissa2, <end> -> Success Mantissa3 - наличие хотя бы одной цифры Mantissa3, <digit> -> Mantissa3 Mantissa3, <dot> -> Mantissa4 Mantissa3, <e> -> Exponent1 Mantissa3, <end> -> Success Mantissa4 - дробная часть Mantissa4, <digit> -> Mantissa4 Mantissa4, <e> -> Indicator1 Mantissa4, <end> -> Success Exponent1 - начало порядка Exponent1, <sign> -> Exponent2 Exponent1, <digit> -> Exponent3 Exponent2 - начало числа в порядке Exponent2, <digit> -> Exponent3 Exponent2, <end> -> Success Exponent3 - продолжение числа в порядке Exponent3, <digit> -> Exponent3 Exponent3, <end> -> Success (Во всех остальных случаях -> Error) |
Возможны два способа построения:
Первый способ экономнее, так как для каждого состояния валидными являются не все типы символов.
Листинг 2. Функция checkFloat_SС другой стороны, если число типов символов меньше, чем число состояний, то можно пойти по второму пути.
Табличная реализация ускоряет работу, так как вместо множества проверок выполняются лишь:
БНФ
<Email> ::= <Name> @ <Domain> <Name> ::= <Words> <Domain> ::= <Words> <Words> ::= <Word> [ . <Words> ] <Word> ::= <letter> <letter>* <letter> - любые символы в диапазоне #33 (-) до #126 (~), кроме ()<>@,;:\/.[] (согласно RFC 822) |
Регулярное выражение
<letter> <letter>* ( <dot> <letter> <letter>* )* <at> <letter> <letter>* ( <dot> <letter> <letter>* )* |
Правила для автомата
Start = Name1 Name1 - начало слова в имени (должен быть буквенно-цифровой символ) Name1, <letter> -> Name2 Name2 - продолжение слова Name2, <letter> -> Name2 Name2, <dot> -> Name1 (после точки должно быть новое слово) Name2, <at> -> Domain1 Domain1 - начало имени в домене Domain1, <letter> -> Domain2 Domain2 - продолжение домена Domain2, <letter> -> Domain2 Domain2, <dot> -> Domain1 Domain2, <end> -> Success (Во всех остальных случаях -> Error) |
Для сравнения была написана "бенчмарка", которая вычисляла каждую из функций 100.000 раз, измеряя длительность работы в тиках (1 тик = 55 мс). Испытания проводились на Pentium-II-400, под управлением Windows 2000. Компилятор - MSVC-6 (без оптимизации). Погрешность измерений составляет приблизительно 10 тиков (0.5 с), что связано, по-видимому, с загрузкой компьютера фоновыми процессами. Измерения проводились как для правильных, так и неправильных строк (тем самым, проверяется скорость принятия решения).
Превосходство табличного метода - налицо.
Заодно проверим скорость системных функций...
Исходная строка | checkFloat_S (switch) | checkFloat_C (char types) | checkFloat_T (table) | atof | sscanf |
---|---|---|---|---|---|
1 | 160 | 90 | 70 | 440 | 530 |
111 | 220 | 170 | 120 | 540 | 720 |
-1.2e-3 | 330 | 510 | 210 | 860 | 1050 |
+1.2E+3 | 321 | 521 | 210 | 840 | 1040 |
+ | 70 | 110 | 80 | 100 | 110 |
. | 70 | 100 | 40 | 80 | 110 |
e | 80 | 120 | 50 | 80 | 110 |
? | 70 | 150 | 40 | 80 | 110 |
Исходная строка | checkEmail_S (switch) | checkEmail_C (char types) | checkEmail_T (table) |
---|---|---|---|
a@b | 220 | 290 | 130 |
a.b@c | 300 | 360 | 170 |
a.b@c.d | 390 | 470 | 210 |
@ | 60 | 130 | 50 |
a. | 150 | 180 | 90 |
a..b@c.d | 130 | 220 | 90 |
? | 50 | 140 | 40 |
Сообщений 24 Оценка 308 Оценить |