Линукс, Vim, LaTeX, полезные скрипты, визуализация данных, численные расчёты, немного ФП

20090712

Необыкновенно лёгкий парсинг в Python

Нашёл просто волшебную библиотечку для парсинга в Python (хм, правильно говорить синтаксического анализа), 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.

10 коммент.:

  1. Спасибо за полезную статью.

    ОтветитьУдалить
  2. С появлением регулярных выражений Perl6 (например, см. http://svn.pugscode.org/pugs/docs/Perl6/Spec/S05-regex.pod), гибкость которых позволяет компактно задавать практически любые грамматики, подобные библиотеки теряют актуальность.

    ОтветитьУдалить
  3. Что ж, может быть. Было бы, однако, интересно, увидеть описание какой-нибудь грамматики в регулярных выражениях perl6, чтобы оценить, насколько оно читаемо.

    Код Parsec, pyparsing или LEPL — прост и читаем. В этом его большое преимущество перед регулярными выражениями.

    ОтветитьУдалить
  4. > Что ж, может быть. Было бы, однако, интересно, увидеть описание какой-нибудь грамматики в регулярных выражениях 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). ;)

    ОтветитьУдалить
  5. Выглядит красиво и кратко. Мне нравится.

    Однако по сути perl6 rules — тоже комбинаторный парсер — но для Perl (есть способы задания именованных правил и достаточно мощные средства для их комбинирования). Думаю, для будущего Perl — это движение в правильном направлении.

    Но другие комбинаторные парсеры, начиная с Parsec (на котором, кстати, и реализованы perl6 rules) и все его подобия, на разных языках, всё же останутся актуальны. Как актуальны regex во всех языках, не только в Perl.

    ОтветитьУдалить
  6. Есть ещё библиотека funcparserlib, если можно немного пиара: http://code.google.com/p/funcparserlib/

    По производительности и краткости кода конкурент для pyparsing.

    ОтветитьУдалить
  7. Андрей! Большое спасибо за ссылку и вообще за работу над нужной библиотекой!

    Ссылку на проект я уже добавил в заметку, а посмотрю и попробую funcparserlib в ближайшее время.

    ОтветитьУдалить
  8. На гугл-буксах есть эта книга "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

    ОтветитьУдалить
  9. zencd, спасибо, только «некоторые страницы этой книги нельзя просмотреть», там только первые 20 страниц.

    ОтветитьУдалить
  10. Спасибо за статью

    ОтветитьУдалить