[Из песочницы] Парсим на 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 Выполнив 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. На текущий момент времени эта среда не освещена на Хабре, и я хотел бы восполнить этот пробел.
Благодарю Дарью Фролову и Никиту Коновалова за помощь в редактировании статьи.