pyparsing. Ниже на простом примере я покажу, как её можно использовать для разбора пользовательских форматов данных.Нашёл так: читая Real World Haskell, узнал про комбинаторную библиотеку для синтаксического анализа Parsec. Примеры в книжке впечатлили. В отличие от традиционного подхода, при этом нет разделения на лексический анализ (выделение «слов»-лексем) и синтаксический анализ (преобразование потока «слов» в упорядоченную структуру данных) — в комбинаторном парсинге эти два этапа объединяются. Берутся небольшие функции, распознающие элементы текста, и затем они комбинируются в соответветствии с синтаксисом текста. Таким образом, сама комбинация функций непосредственно отражает грамматику, и она же, естественно, является и программой для разбора текста. Как у всякой удачной идеи, у Parsec есть множество подражаний. Для Python комбинаторных парсеров нашлосьцелых двауже триуже четыре — Pysec, Pyparsing, LEPL (для Python 2.6/3.0) и funcparselib. Я буду говорить оpyparsing.
В следующей заметке — Ещё одна библиотека для комбинаторного парсинга — смотрите аналогичный пример для библиотечкиfuncparserlib.
Итак, перейдём к делу. Предположим нужно читать файлы состоящие из записей следующего вида:
Inspection
# 2 SHOULD Ref. Sys 1
X 28.7493
Y 78.9960
Z -1.0014
Всё необходимое импортируем из модуля
pyparsing. При работе поглядываем в документацию к модулю. Для простоты примера импортируем всё:from pyparsing import *
Теперь начинаем описывать грамматику. Например, определим числа как слова, состоящие из цифр, знака точки и дефиса (минуса)
number = Word(nums + ".-")
а значения переменных определим как пару заглавной латинской буквы и числа:
var = Regex("[A-Z]") + numberОбратим внимание, что плюс между двумя простыми парсерами (регулярное выражение и слово) создаёт новый парсер, который распознаёт уже последовательность выражений. По-умолчанию
pyparsing игнорирует все лишние пробелы и переводы строк между элементами разбираемого текста (обычно именно это и нужно), поэтому указывать в грамматике наличие пробелов между элементами необязательно.Уже на этом этапе мы можем попробовать наш парсер переменных. Запускаем интерпретатор и выполняем:
>>> var.parseString("X 42.0")
(['X', '42.0'], {})— на выходе получили структуру данных в соответствии с нашей грамматикой (имя переменной и число за ним).
Допишем всё остальное. Для простоты будем считать комментарием всё после знака «#» до конца строки (комбинатор
restOfLine):comment = "#" + restOfLine
Теперь мы можем описать грамматику всей записи в целом.
record = Suppress("Inspection" + comment) + OneOrMore(var)Запись опознаём по слову «Inspection» в начале (здесь строковой литерал Python автоматически конвертируется в
Literal-парсер, проверяющий буквальное соответствие слову). Далее, обнаружив начало записи, состоящие из слова «Inspection» и следующий за ней комментарий, мы можем их просто пропустить (комбинатор Suppress), а вот то, что следует дальше — нам интересно. Мы ожидаем, что дальше могут идти значения для одной или нескольких переменных (применяем комбинатор OneOrMore).Последний штрих. Нужно указать, что в файле таких записей может быть несколько. Для удобства работы с полученной структурой переменные каждой из записей группируем вместе (комбинатор
Group):datafile = OneOrMore(Group(record))
Всё! Синтаксический анализатор для нашего формата данных готов. Использовать можно, например, так:
import sys
print datafile.parseString(sys.stdin.read())
Проверяем:
$ python example.py << END
> Inspection
> # 2 SHOULD Ref. Sys 1
> X 28.7493
> Y 78.9960
> Z -1.0014
>
> Inspection
> # 3 SHOULD Ref. Sys 1
> X 54.0394
> Y 64.3977
> Z -0.9950
>
> END
[['X', '28.7493', 'Y', '78.9960', 'Z', '-1.0014'],
['X', '54.0394', 'Y', '64.3977', 'Z', '-0.9950']]
Получили вполне пригодную к использованию в программе структуру данных. Вся грамматика — на пять строк. В общем-то, поняв идею и поглядывая в справку, несложно описать и более сложную грамматику.
Например, чтобы разбирать также и строчку с «#» в моём примере, программку можно изменить так:
from pyparsing import *
number = Word(nums + ".-")
var = Regex("[A-Z]") + number
desc = Suppress("#") + Word(nums) + Word(alphas) \
+ Suppress("Ref. Sys") + Word(nums)
record = Suppress("Inspection") + desc + Group(OneOrMore(Group(var)))
datafile = OneOrMore(Group(record))
На выходе этот парсер даст:
[['2', 'SHOULD', '1', [['X', '28.7493'], ['Y', '78.9960'], ['Z', '-1.0014']]],
['3', 'SHOULD', '1', [['X', '54.0394'], ['Y', '64.3977'], ['Z', '-0.9950']]]]
P.S. Нормального тьюториала по
pyparsing в сети я не нашёл, но автор библиотеки написал и продаёт на O’Reilly учебное электропособие за 10 долларов. Справочная же документация и разные примеры в интернете — вполне толковы.См. также заметку про funcparserlib.



Спасибо за полезную статью.
ОтветитьУдалитьС появлением регулярных выражений Perl6 (например, см. http://svn.pugscode.org/pugs/docs/Perl6/Spec/S05-regex.pod), гибкость которых позволяет компактно задавать практически любые грамматики, подобные библиотеки теряют актуальность.
ОтветитьУдалитьЧто ж, может быть. Было бы, однако, интересно, увидеть описание какой-нибудь грамматики в регулярных выражениях perl6, чтобы оценить, насколько оно читаемо.
ОтветитьУдалитьКод Parsec, pyparsing или LEPL — прост и читаем. В этом его большое преимущество перед регулярными выражениями.
> Что ж, может быть. Было бы, однако, интересно, увидеть описание какой-нибудь грамматики в регулярных выражениях perl6, чтобы оценить, насколько оно читаемо.
ОтветитьУдалитьПожалуйста. Привожу эквивалентное приведённому коду на Python регулярное выражение Perl6 для разбора записей из примера.
rule TOP { <record>+ }
rule record { 'Inspection' <description> <variable>+ }
rule description { '#' <number> <ident> 'Ref. Sys' <number> }
rule variable { <ident> <number> }
token number
{
<[ + \- ]>? [ <.digit>* '.' <.digit>+ | <.digit>+ [ '.' <.digit>* ]? ]
[ <[ e E ]> <[ + \- ]>? <.digit>+ ]?
}
Здесь пробелы (и, по желанию, комментарии) разбираются правилом <ws> (его стандартное определение в данном случае подходит — изменять его не пришлось), идентификаторы — готовым правилом <ident>. Встроенного правила для разбора чисел с плавающей точкой в Perl6 нет, однако его легко один раз написать и впоследствии по необходимости включать в те грамматики, где оно должно встречаться.
Как видите, регулярное выражение почти не содержит «лишних» символов в своей записи и выглядит очень естественно.
> Код Parsec, pyparsing или LEPL — прост и читаем. В этом его большое преимущество перед регулярными выражениями.
По всей видимости, Вы судите по регулярным выражениям Perl5 (и им подобным в Python и Ruby). ;)
Выглядит красиво и кратко. Мне нравится.
ОтветитьУдалитьОднако по сути perl6 rules — тоже комбинаторный парсер — но для Perl (есть способы задания именованных правил и достаточно мощные средства для их комбинирования). Думаю, для будущего Perl — это движение в правильном направлении.
Но другие комбинаторные парсеры, начиная с Parsec (на котором, кстати, и реализованы perl6 rules) и все его подобия, на разных языках, всё же останутся актуальны. Как актуальны regex во всех языках, не только в Perl.
Есть ещё библиотека funcparserlib, если можно немного пиара: http://code.google.com/p/funcparserlib/
ОтветитьУдалитьПо производительности и краткости кода конкурент для pyparsing.
Андрей! Большое спасибо за ссылку и вообще за работу над нужной библиотекой!
ОтветитьУдалитьСсылку на проект я уже добавил в заметку, а посмотрю и попробую funcparserlib в ближайшее время.
На гугл-буксах есть эта книга "getting started with pyparsing":
ОтветитьУдалитьhttp://books.google.ru/books?id=uz1polNu948C&pg=PT4&lpg=PT4&dq=getting+started+with+pyparsing&source=bl&ots=1-96T5kRMI&sig=X8zc3iU78U_A5tU6Dl61WIxvvDc&hl=ru&ei=vDLqSoqtIqHsmwPGsqWQDw&sa=X&oi=book_result&ct=result&resnum=6&ved=0CCIQ6AEwBQ#v=onepage&q=&f=false
zencd, спасибо, только «некоторые страницы этой книги нельзя просмотреть», там только первые 20 страниц.
ОтветитьУдалитьСпасибо за статью
ОтветитьУдалить