Skip navigation.

проФорт

Форт и всё такое

Регулярные выражения

,

Регулярные выражения удобны, а точнее просто необходимы при любой сколько-нибудь
интенсивной работе с текстом. Посмотрим как обстоят дела с regexp'ами в форте.
План:
  • Что такое регулярные выражения
  • Преимущества форта для реализации библиотеки регекспов
  • Описание ~ygrek/lib/re
  • Возможности для улучшения

Язык регулярных выражений позволяет конструировать минипрограммы которые отвечают на один вопрос - совпадает ли данный произвольный текст с конкретным образцом. Краткое описание языка: строка текста и регексп просматриваются слева направо и последовательно сопоставляются. Каждый символ регекспа (кроме специальных) сопоставляется только с самим собой. Специальные символы это -- круглые скобки () которые группируют подрегексп в один элемент, a* сопоставляется с последовательностью из нуля или более элементов a (вместо одного символа может быть произвольный регексп заключённый в скобки),a|b совпадёт либо с a либо с b. Например (0|1)(0|1)* сопоставится с записью любого числа в двоичной системе. Для удобства использования добавлены также дополнительные конструкции (спец символы) которые выражаются через вышеописанные. Это a+ == aa* -- один или более, [xyz] == (x|y|z) (где x y z это одиночные символы) -- множество, [123a-e] == [123abcde] -- интервал, [^ab] -- любой символ кроме a и b, \d == [0-9] -- цифра, \s -- пробельный символ, a{n,m} -- сопоставится с последовательностью повторения a длины от n до m, . -- (точка) любой символ, итд. Для того чтобы вставить в образец спецсимвол который бы обозначал себя, а не спецконструкцию - его надо заэкранировать - символом \.

Программная реализация библиотеки регекспов подразумевает написание функции которая принимает строку и образец и возвращает логическое значение. На практике обычно этого недостаточно и хочется уметь выбирать из сопоставившейся строки подстроку совпавшую с данным подрегекспом. Границы этих подрегекспов, которые "ловят" строку определяются круглыми скобками и идентифицируются по номеру открывающей скобки если считать слева направо. Т.е.
(\d{1,3})\.\d{1,3}\.(\d{1,3}\.\d{1,3})
сопоставится с традиционной записью IP адреса при этом подрегексп (или карман) номер один будет соответствовать первому байту адреса, а второй -- последним двум с точкой между ними. За нулевой карман принимается вся совпавшая строка.

Для того чтобы запись регекспов не различалась между разными реализациями -- был разработан стандарт POSIX-регекспов. Другой обширный класс регекспов был сформирован де-факто по реализации регекспов в языке Perl. Эти два вида совпадают в базовых понятиях, но отличаются во многих подробностях. Впрочем во многих реализации есть свои дополнительные "фишки".

Изначально регексп задаётся программистом в виде строки в тексте программы. В языках (читай -- реализациях), где регекспы не являются базовой сущностью (например C/C++, Java, O'Caml, и т.д.), эта строка во время исполнения программы парсится, формируется автомат для разбора или сразу запускается алгоритм сопоставления. В форте же, благодаря наличию first-class компиляции (спасибо true-grue за термин), можно этап парсинга и построения автомата перенести на этап компиляции (так же делается в языках, где регекспы реализованы в ядре языка -- Perl, Ruby (?)). И во время компиляции можно провести редукцию автомата и другие возможные оптимизации, проверить синтаксис регекспа на правильность, и т.п. Это очень важное преимущество. Стоит также отметить что compile-time разбор RE можно реализовать на обычных языках если отказаться от классического представления регекспа в виде строки, а формировать его из меньших составных частей представленных "родными" элементами языка -- это то что называется parser generators (e.g. micmatch (O'Caml), boost::xpressive (C++), Gray (Forth), и т.д.)

В репозитории spf есть несколько обёрток к внешним dll-библиотекам PCRE и BREGEXP. Либа ~ygrek/lib/re реализует базовый функционал регекспов на чистом форте. Регекспы задаются в виде строки как
RE" (hi|hello)\sworld"
При этом во время компиляции проиходит разбор строки, проверка синтаксиса, преобразование в NFA (non-deterministic finite automata) и сохранение структуры автомата в кодофайл. Можно также создавать регекспы во время исполнения с помощью BUILD-REGEX ( a u -- re ) (такие динамические регекспы после использования надо удалять с помощью FREE-REGEX ( re -- )). Реализация стремится соответствовать стандарту POSIX (довольно утомительное чтиво :smile: ), но пока не полностью. Полученный re можно использовать для сопоставления со строкой словом re_match? ( a u re -- ?), информацию о подсовпадениях-карманах можно извлечь словом get-group ( n -- a u ). Пример -- "калькулятор" :smile:
REQUIRE re_match? ~ygrek/lib/re/re.f

: NUM ( a u -- n ) 0 0 2SWAP >NUMBER NIP ABORT" not a number" D>S ;

: calc
RE" \s*(\d+)\s*([+*/-])\s*(\d+)\s*" re_match? NOT ABORT" oops"
\1 NUM \3 NUM \2 EVALUATE ;

: readline ( n -- a u ) PAD SWAP ACCEPT PAD SWAP ;

: run BEGIN 20 readline ['] calc CATCH IF 2DROP ." Nope" ELSE ." Result : " . THEN CR AGAIN ;

run
\2 это более короткая запись для 2 get-group. Обратите внимание что в перечислении знаков операторов '-' оставлен последним, этого требует POSIX, чтобы знак минус не был воспринят как указатель диапазона.
Т.к. RE" это слово форта которое само выбирает строку из входного потока, то оно понимает квотирование кавычки обратным слешем. В ext.f определены некоторые дополнительные стандартные операции с регекспами :
  • re_split ( a u re -- list ) разбить строку по регекспу в список
  • re_search_all ( a u re -- list ) найти все подстроки совпавшие с re и вернуть их список
  • re_search ( a u re -- a1 u1 ) находит первое подстроку совпадающую с re
  • re_divide-> ( a u re --> a2 u2 a1 u1 \ <-- ) бакфорт итератор возвращающий совпавшие и несовпавшие подстроки
Либа покрыта юнит-тестами в количестве 140 штук и надёжно работает. Все экспортируемые слова подробно описаны в начале файла re.f.
В качестве бонуса -- визуализация внутренней структуры автомата. Слово dotto: ( a u "filename" -- ? (из dot.f) сохранит dot диаграмму регекспа a u в файл filename.
S" ~ygrek/lib/re/dot.f" INCLUDED
S" AB+(CD)+|12?3" dotto: regexp.dot . BYE


результат :


Либа реализует NFA-алгоритм. Благодаря этому для неё не существует "патологических" регекспов - т.е. время сопоставления зависит только от длины регекспа и строки. Пример приводящий к экспоненциальному росту времени работы рекурсивных "сопоставителей" см. в конце файла re.f. С другой стороны это значит что на "простых" регекспах рекурсивная реализация в большинстве случаев будет быстрее. Возможное решение проблемы -- переход от NFA к DFA. Вдобавок в этой конкретной реализации не очень эффективно реализовано сохранение подвыражений что прилично сказывается на скорости выполнения (можно и нужно переписать). Ещё один существенный недостаток этой реализации -- отсутствие поддержки юникода, впрочем UTF-8 можно кое-как обрабатывать (по-байтово). Также в планах -- реализовать рекурсивный алгоритм сопоставления. Также для более полного соответствия POSIX надо правильно вычислять границы подвыражений (leftmost longest) и пройти все тесты AT&T.

Оптимизатор SPFРазбор программ на примере

Comments

profiT 16. April 2008, 13:28

Originally posted by EXS:

Возможное решение проблемы -- переход от NFA к DFA.


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

exs 16. April 2008, 19:04

Да, это вещь известная, трясти надо. Но на dfa не получится (легко) реализовать backreference'ы.

garbler 22. May 2008, 08:18

из книг на эту тему посоветую 2-е издание PTAPG от Дика Грюне
(её существенно переработали, по сравнению с первым).
великолепная книга, имхо, лучше дракона будет.

http://www.cs.vu.nl/~dick/PTAPG.html

profiT 22. May 2008, 09:23

Ну так "книга дракона" и вовсе не про это (или точнее -- не только про это)...

/me тут на днях начинает поглядывать на PEG (по наводке Зубинского)...

garbler 22. May 2008, 11:16

"не про это" лучше почитать того же Дика Грюне "Modern Compiler Design" мне тоже понравилось, но электронного линка на неё не подскажу (я в бумаге читал).

profiT> /me тут на днях начинает поглядывать на PEG

угу, есть такое. собственно, алгоритмы с откатами (а не автоматные) широко применяют с динамическими языками: snobol/icon, prolog. там это дело реализуется фактически "в лоб". (в ту же кучу можно добавить построение парсеров на основе типа relation в язые leda).

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

вообще же эти алгоритмы (imho) интересно применять к разбору иерархических/расширяемых грамматик, что-то вроде такого:
Extensible Syntax with Lexical Scoping (1994)
Luca Cardelli, Florian Matthes, Martin Abadi
http://citeseer.ist.psu.edu/9632.html

How to use Quote function:

  1. Select some text
  2. Click on the Quote link

Write a comment

Comment
(BBcode and HTML is turned off for anonymous user comments.)

If you can't read the words, press the small reload icon.


Smilies