Автоматизация технических расчётов
0,0
рейтинг
3 октября 2014 в 12:03

Разработка → Парсим на Python: Pyparsing для новичков из песочницы

Парсинг (синтаксический анализ) представляет собой процесс сопоставления последовательности слов или символов — так называемой формальной грамматике. Например, для строчки кода:

import matplotlib.pyplot  as plt

имеет место следующая грамматика: сначала идёт ключевое слово import, потом название модуля или цепочка имён модулей, разделённых точкой, потом ключевое слово as, а за ним — наше название импортируемому модулю.

В результате парсинга, например, может быть необходимо прийти к следующему выражению:

{ 'import': [ 'matplotlib', 'pyplot' ], 'as': 'plt' }

Данное выражение представляет собой словарь Python, который имеет два ключа: 'import' и 'as'. Значением для ключа 'import' является список, в котором по порядку перечислены названия импортируемых модулей.

Для парсинга как правило используют регулярные выражения. Для этого имеется модуль Python под названием re (regular expression — регулярное выражение). Если вам не доводилось работать с регулярными выражениями, их вид может вас испугать. Например, для строки кода 'import matplotlib.pyplot as plt' оно будет иметь вид:

r'^[ \t]*import +\D+\.\D+ +as \D+'

К счастью, есть удобный и гибкий инструмент для парсинга, который называется Pyparsing. Главное его достоинство — он делает код более читаемым, а также позволяет проводить дополнительную обработку анализируемого текста.

В данной статье мы установим Pyparsing и создадим на нём наш первый парсер.


Вначале установим Pyparsing. Если Вы работаете в Linux, в командной строке наберите:

sudo pip install pyparsing

В Windows Вам необходимо в командной строке, запущенной с правами администратора, предварительно зайти в каталог, где лежит файл pip.exe (например, C:\Python27\Scripts\), после чего выполнить:

pip install pyparsing

Другой способ — это зайти на страницу проекта Pyparsing на SourceForge, скачать там инсталлятор для Windows и установить Pyparsing как обычную программу. Полную информацию о всевозможных способах установки Pyparsing можно получить на странице проекта.

Перейдём к парсингу. Пусть s — следующая строка:

s = 'import matplotlib.pyplot as plt'

В результате парсинга мы хотим получить словарь:

{ 'import': [ 'matplotlib', 'pyplot' ], 'as': 'plt' }

Сначала необходимо импортировать Pyparsing. Запустите например Python IDLE и введите:

from pyparsing import *

Звёздочка * выше означает импорт всех имён из pyparsing. В результате это может нарушить рабочее пространство имён, что приведёт к ошибкам в работе программы. В нашем случае * используется временно, потому что мы пока не знаем, какие классы из Pyparsing мы будем использовать. После того, как мы напишем парсер, мы заменим * на названия использованных нами классов.

При использовании pyparsing, парсер вначале пишется для отдельных ключевых слов, символов, коротких фраз, а потом из отдельных частей получается парсер для всего текста.

Начнём с того, что у нас в строке есть название модуля. Формальная грамматика: в общем случае название модуля — это слово, состоящее из букв и символа нижнего подчёркивания. На pyparsing:

module_name = Word(alphas + '_')

Word — это слово, alphas — буквы. Word(alphas + '_') — слово, состоящее из букв и нижнего подчёркивания. module_name переводится как название модуля. Теперь читаем всё вместе: название модуля — это слово, состоящее из букв и символа нижнего подчёркивания. Таким образом, запись на Pyparsing очень близка к естественному языку.

Полное имя модуля — это название модуля, потом точка, потом название другого модуля, потом снова точка, потом название третьего модуля и так далее, пока по цепочке не дойдём до искомого модуля. Полное имя модуля может состоять из имени одного модуля и не иметь точек. На pyparsing:

full_module_name = module_name + ZeroOrMore('.' + module_name)

ZeroOrMore дословно переводится как «ноль или более», а это означает, что содержимое в скобках может повторяться несколько раз или отсутствовать. В итоге читаем полностью вторую строчку парсера: полное имя модуля — это название модуля, после которого ноль и более раз идут точка и название модуля.

После полного названия модуля идёт необязательная часть 'as plt'. Она представляет собой ключевое слово 'as', после которого идёт имя, которое мы сами дали импортируемому модулю. На pyparsing:

import_as = Optional('as' + module_name)

Optional дословно переводится как «необязательный», а это означает, что содержимое в скобках может быть, а может отсутствовать. В сумме получаем: «необязательное выражение, состоящее из слова 'as' и названия модуля.

Полная инструкция импорта состоит из ключевого слова import, после которого идёт полное имя модуля, потом необязательная конструкция 'as plt'. На pyparsing:

parse_module = 'import' + full_module_name + import_as

В итоге имеем наш первый парсер:

module_name = Word(alphas + '_')
full_module_name = module_name + ZeroOrMore('.' + module_name)
import_as = Optional('as' + module_name)
parse_module = 'import' + full_module_name + import_as

Теперь надо распарсить строку s:

parse_module.parseString(s)

Мы получим:

(['import', 'matplotlib', '.', 'pyplot', 'as', 'plt'], {})

Вывод можно улучшить, преобразовав результат в список:

parse_module.parseString(s).asList()

Получим:

['import', 'matplotlib', '.', 'pyplot', 'as', 'plt']

Теперь будем совершенствовать парсер. Прежде всего, мы бы не хотели видеть в выводе парсера слово import и точку между названиями модулей. Для подавления вывода используется Suppress(). С учётом этого наш парсер выглядит так:

module_name = Word(alphas + '_')
full_module_name = module_name + ZeroOrMore(Suppress('.') + module_name)
import_as = Optional(Suppress('as') + module_name)
parse_module = Suppress('import') + full_module_name + import_as

Выполнив parse_module.parseString(s).asList(), получим:

['matplotlib', 'pyplot', 'plt']

Давайте теперь сделаем так, чтобы парсер сразу возвращал нам словарь вида {'import':[модуль1, модуль2, ...], 'as':модуль}. Прежде чем сделать это, вначале нужно отдельно получить доступ к списку импортируемых модулей (full_module_name) и к нашему собственному названию модуля (import_as). Для этого pyparsing позволяет назначать имена результатам парсинга. Давайте дадим списку импортируемых модулей имя 'modules', а тому, как мы сами назвали модуль — имя 'import as':

full_module_name = (module_name + ZeroOrMore(Suppress('.') + module_name))('modules')
import_as = (Optional(Suppress('as') + module_name))('import_as')

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

res = parse_module.parseString(s)
print(res.modules.asList())
print(res.import_as.asList())

Получим:

['matplotlib', 'pyplot']
['plt']

Теперь мы можем отдельно извлекать цепочку модулей для импорта искомого и наше название для него. Осталось сделать так, чтобы парсер возвращал словарь. Для этого используется так называемое ParseAction — действие в процессе парсинга:

parse_module = (Suppress('import') + full_module_name).setParseAction(lambda t: {'import': t.modules.asList(), 'as': t.import_as.asList()[0]})

lambda — это анонимная функция в Python, t — аргумент этой функции. Потом идёт двоеточие и выражение словаря Python, в который мы подставляем нужные нам данные. Когда мы вызываем asList(), мы получаем список. Имя модуля после as всегда одно, и список t.import_as.asList() всегда будет содержать только одно значение. Поэтому мы берём единственный элемент списка (он имеет индекс ноль) и пишем asList()[0].

Проверим парсер. Выполним parse_module.parseString(s).asList() и получим:

[{ 'import': [ 'matplotlib', 'pyplot' ], 'as': 'plt' }]

Мы почти достигли цели. Так как у полученного списка единственный аргумент, добавим [0] в конце строки для парсинга текста: parse_module.parseString(s).asList()[0]


В итоге:

{ 'import': [ 'matplotlib', 'pyplot' ], 'as': 'plt' }

Мы получили то, что хотели.

Достигнув цели, необходимо вернуться к 'from pyparsing import *' и поменять звёздочку на те классы, которые нам пригодились:

from pyparsing import Word, alphas, ZeroOrMore, Suppress, Optional

В итоге наш код имеет следующий вид:

from pyparsing import Word, alphas, ZeroOrMore, Suppress, Optional
module_name = Word(alphas + "_")
full_module_name = (module_name + ZeroOrMore(Suppress('.') + module_name))('modules')
import_as = (Optional(Suppress('as') + module_name))('import_as')
parse_module = (Suppress('import') + full_module_name + import_as).setParseAction(lambda t: {'import': t.modules.asList(), 'as': t.import_as.asList()[0]})

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

В заключение пару слов о себе. Я аспирант и ассистент МГТУ им. Баумана (кафедра МТ-1 „Металлорежущие станки“). Увлекаюсь Python, Linux, HTML, CSS и JS. Моё хобби — автоматизация инженерной деятельности и инженерных расчётов. Считаю, что могу быть полезным Хабру, делясь своими знаниями о работе в Pyparsing, Sage и некоторыми особенностями автоматизации инженерных расчётов. Также знаю среду SageMathCloud, которая является мощной альтернативой Wolfram Alpha. SageMathCloud заточена на проведение расчётов на Python в облаке. При этом Вам доступна консоль (Ubuntu под капотом), Sage, IPython и LaTeX. Есть возможность совместной работы. Помимо кода на Python SageMathCloud поддерживает html, css, js, coffescript, go, fortran, scilab и многое другое. В настоящее время среда бесплатна (достаточно стабильная бета-версия), потом будет будет работать по системе Freemium. На текущий момент времени эта среда не освещена на Хабре, и я хотел бы восполнить этот пробел.

Благодарю Дарью Фролову и Никиту Коновалова за помощь в редактировании статьи.
Андрей Ширшов @AndreWin
карма
25,0
рейтинг 0,0
Автоматизация технических расчётов
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (12)

  • +7
    Интересная вводная статья в Pyparsing. Но, конечно же, жду продолжения по
    создание рекурсивных выражений, обработка таблиц, поиск по тексту с оптимизацией, резко ускоряющей сам поиск, и многое другое.
  • +7
    Вредный совет про звёздочку.
    После того, как мы напишем парсер, мы заменим * на названия использованных нами классов.
    Читай как «Забудем заменить»

    А статья замечательная, спасибо.
  • 0
    Немного трогал pyparsing, даже делал небольшой рассказ про него (мало пояснений): nbviewer.ipython.org/urls/gist.githubusercontent.com/tbicr/cd584138ce183839946f/raw/e0c335bd57103e200279302eff3c667d5dd470b1/Pyparsion.ipynb. Если котортко дополнить, то мне сильно понравилось наличие setParseAction, addParseAction, которыми можно задать дополнительное поведение, как возвращая изменненное значение, так кидая ParseException, еще понарвилось что эскейпинг легко делать.
  • +2
    Для парсинга как правило используют регулярные выражения.

    /зануда моде он
    Вообще-то регулярные выражения не могут применяться для парсинга, поскольку не являются NP-полным языком.
    Впрочем, введение рекурсивности в них снимает это ограничение.
    /зануда моде офф
    А статья неплохая, спасибо.
    • 0
      Насколько я помню, регулярные выражения, соответствующие регулярным грамматикам, ограничены в возможностях не из-за NP-неполноты (которая вообще тут ни при чём), а из-за того, что это самый простой и ограниченный тип грамматик по классификации Хомского.
  • +1
    На примере разбора одной строки как-то сложно оценить работу с этим модулем. В следующей статье по этой теме, если можно, хотелось бы увидеть что-то поувесистее… Например, парсер крошечного подмножества SQL — скажем, только SELECT и UPDATE, только из одной таблицы с опциональным WHERE, в котором может быть ровно один предикат с равенством.
    • 0
      К сожалению я не владею SQL запросами. Напишите пожалуйста пример текстовой строки — SQL запроса и что должно получиться на выходе.
      Я обязательно разберу этот пример в одной из следующих статей.
      В следующей статье я планирую написать про парсинг единиц измерения, затронув такие моменты, как:
      1. Создание рекурсивных выражений
      2. Парсинг русских букв на pyparsing
      3. Учёт пробелов в pyparsing
      • 0
        Типовые запросы на SQL:

        • SELECT * FROM `t`
        • SELECT `c1`, `c2`, `c3` FROM `t` WHERE `c2` = 5
        • UPDATE `t` SET `c1` = 5
        • UPDATE `t` SET `c2` = `c1` + `c3` WHERE `c1` = 7

        SELECT, UPDATE, FROM, WHERE, SET — ключевые слова. В кавычках — идентификаторы. Звездочка — то же, что и перечисление всех колонок в таблице. Скажем, для самого простого подмножества запрсов SELECT описание синтаксиса будет примерно такое:

        SELECT * | `<имя колонки>` [, `<имя колонки>` ...]
        FROM   `<имя таблицы>`
        [WHERE `<имя колонки>` = <константа> | `<имя колонки>`]

        Для UPDATE:

        UPDATE `<имя таблицы>`
        SET    `<имя колонки>` = <константа>
             | `<имя колонки>` = `<имя колонки>`
             | `<имя колонки>` = `<имя колонки>` <арифметическая операция: + - * /> `<имя колонки>`
        [WHERE `<имя колонки>` = <константа> | `<имя колонки>`]
    • +1
      Во. А мне еще представляется, что осознать код, идущий после слов «В итоге наш код имеет следующий вид:», не сильно проще, чем базовые регулярные выражения.
      При этом знание элементарных основ регулярок даёт непропорционально бОльшие возможности с инструментами, их поддерживающими. imho, конечно. Раз модуль есть, значит кому-то с ним удобнее, почему нет.
  • 0
    промазал мимо ветки, удалено
  • +1
    А можно сравнение с ply?
    • 0
      К сожалению не доводилось работать с этим инструментом. Однако я нашёл эту статью на хабре про ply. Я постараюсь в октябре-ноябре выпустить статью, разбирающую тот же самый пример на pyparsing. Соответственно будет возможность сравнить эти два инструмента.

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