Парсинг (синтаксический анализ) представляет собой процесс сопоставления последовательности слов или символов — так называемой формальной грамматике. Например, для строчки кода:
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:Python27Scripts), после чего выполнить:
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. На текущий момент времени эта среда не освещена на Хабре, и я хотел бы восполнить этот пробел.
Благодарю Дарью Фролову и Никиту Коновалова за помощь в редактировании статьи.
Автор: AndreWin