pyparsing
для комбинаторного парсинга в Python. В комментариях появилась ссылка на ещё одну библиотеку, о которой я вначале не знал, а именно на funcparserlib
, написанную Андреем Власовских.В общем-то, я посмотрел на новую библитечку, и она мне тоже понравилась. Подкупает сравнительная простота самой библиотеки, ясные исходники и подробно написанные руководства — понять как работает библиотека нетрудно. Правда, при чтении документации нужно быть знакомым с нотацией типов, принятой в Haskell (
ArgType -> ResultType
). Общее впечатление: имеющиеся в funcparserlib
комбинаторы практически полностью ортогональны друг другу, записываются кратко, места на экране занимают мало, называются понятно.Комбинаторов в библиотеке всего-то:
some
, a
, many
, finished
, maybe
, skip
, oneplus
, forward_decl
, +
, |
и >>
. Действие большинства комбинаторов угадывается из названия. Рабочие лошадки: some(функция-предикат)
берёт токен, удовлетворяющий условию, a(токен)
берёт токен, указанный в аргументе, many(парсер)
, maybe(парсер)
, oneplus(парсер)
— множественное, возможное или хотя бы однократное срабатывание парсера, skip(парсер)
отбрасывает всё найденное парсером. Плюс (+
) последовательно применяет два парсера, «или» (|
) пробует альтернативные варианты, >>
подставляет результат парсера в функцию и создаёт новый парсер (очень полезный комбинатор!). Ещё есть tuple
для группировки.Для пробы я решил написать пример разбора того же файлового формата, что и в заметке о pyparsing. Мне кажется, что наиболее эффективно применять
funcparserlib
с предварительным лексическим анализом (разбиением текста на токены-лексемы). В принципе, готовый токенизатор — часть стандартной библиотеки Python, поэтому это не проблема. Удобная обёртка приведена в руководстве к funcparserlib
. Однако мне хотелось написать пример в том же стиле, что и для pyparsing
, поэтому в примере ниже я разбираю текст на уровне отдельных символов.Полный текст примера с тестами кладу в pastebin. Далее пояснения.
Половина кода пришлась на разбор чисел. Вообще, по-моему, в такого рода библиотеках было бы хорошо класть готовые парсеры для чисел, дат, e-mail и тому подобных обыденных вещей где-нибудь в разделе contrib... Впрочем, написать разбор чисел было даже занятно.
Чтение знака числа. Если ни плюса, ни минуса нет, подразумеваю плюс. Волшебный комбинатор
>>
позволяет обработать результат и сразу подготовить ответ, или -1
, или +1
:sign = maybe(a("-")|a("+")) >> (lambda c: c == "-" and -1 or +1)
Целые числа состоят из знака и последовательности цифр. Так и запишем:
digits = many(some(lambda c: c.isdigit()))int_num = sign + (digits >> to_int) >> mk_int
Здесь я пользуюсь опять комбинатором
>>
и двумя вспомогательными функциями. Одна превращает последовательность символов-цифр в число (to_int
), другая умножает результат на -1
, если необходимо (mk_int
). Эти функции вспомогательные, можно пропустить: powers = lambda digs: zip(digs,xrange(len(digs)-1,-1,-1))add_digit = lambda acc, dp: acc+int(dp[0])*10**dp[1]to_int = lambda digs: reduce(add_digit, powers(digs), 0)mk_int = lambda (s,i): s*i
Аналогично строю и парсер для рациональных чисел, только прибавляю ещё и дробь, и, соответственно, ещё две вспомогательных функции:
to_frac = lambda digs: to_int(digs)*1.0/10**len(digs)mk_frac = lambda (s,i,f): s*(i+f)frac_num = sign + (digits >> to_int) + (skip(maybe(a("."))) + (digits >> to_frac)) >> mk_frac
Вот и всё. Использовать примерно так:
>>> frac_num.parse("-1.25")-1.25
Второй половиной кода оказалось написание парсеров для аналогов
Literal
и Word
из pyparsing
. Это необходимо, потому что без токенизации приходится распознавать цепочки токенов. Дополнительно создал парсер ws
для пропуска пробелов: pack = lambda cs: ''.join(cs)literal = lambda s: reduce(lambda a,b: a+b, map(a,s)) >> packword = lambda p: oneplus(some(p)) >> packws = skip(many(some(lambda c: c.isspace())))
После этих определений собственно парсер выбранного формата укладывается в три выражения:
varname = word(lambda c: c.isalpha())var = (ws + varname + ws + frac_num + ws)custom_format = skip(literal("Inspection")) + \ws + skip(literal("#")) + \ws + int_num + \ws + skip(literal("SHOULD")) + \ws + skip(literal("Ref. Sys")) + \ws + int_num + \many(var)
Смотрим на результат:
$ python test.py < input.txt (2, 1, [('X', 28.749300000000002), ('Y', 78.995999999999995), ('Z', -1.0014000000000001)])
В общем, впечатления хорошие. Документация у библиотеки приличная. Использовать приятно. Только одно досадное неудобство было связано с тем, что
setup.py install --prefix=...
из дистрибутивного тарбола не сработал как надо. Впрочем, библиотека такая маленькая, что можно положить все три её файла прямо в свой проект, без общесистемной установки.Тонкости: нужно быть осторожным, не помещая универсально успешный парсер внутрь
many
, чтобы избежать вечного цикла. Короче, нельзя внутрь many
помещать many
, maybe
и pure
. Подробнее — см. FAQ.См. также заметку про pyparsing.
Дополнение: слайды презентации funcparserlib на DevConf 2010: