Привет, Хабр!
Если ты подросток и начинаешь свой путь в программировании, или просто хочешь понять, как программы анализируют текст и превращают его в структуру, эта статья для тебя. Сегодня поговорим о том, что такое парсер, зачем он нужен и как с помощью библиотеки pyparsing создать свой собственный парсер — основу для мини‑языка. Разберём, как создать парсер для арифметических выражений, добавить поддержку скобок, встроить вычисление выражений, работать с переменными и обрабатывать ошибки.
Парсер — это программа, которая читает входной текст, анализирует его по заданным правилам и строит внутреннее представление, например, дерево разбора. В нашем случае создадим простой парсер, который сможет обрабатывать арифметические выражения, а затем расширим его функциональность, чтобы поддерживать переменные и присваивание. Это очень хороший способ научиться работать с текстовыми данными и создать базовый интерпретатор.
Установим библиотеку:
pip install pyparsing
Эта команда установит последнюю версию библиотеки pyparsing.
Базовый арифметический парсер
Начнём с примера, который разбирает выражения вида «2 + 3 * 4 — 1». Сначала импортируем необходимые классы из pyparsing. Здесь используем класс Word для задания токенов, константу nums — набор цифр, а также функцию infixNotation, которая автоматически создаёт парсер с учетом приоритета операторов.
from pyparsing import Word, nums, infixNotation, opAssoc, oneOf
# Определяем правило для числа: последовательность цифр, преобразуем в int
number = Word(nums).setParseAction(lambda t: int(t[0]))
# Создаем парсер для арифметических выражений, указывая операторы и их приоритет
expr = infixNotation(number,
[
(oneOf('* /'), 2, opAssoc.LEFT), # умножение и деление
(oneOf('+ ‑'), 2, opAssoc.LEFT), # сложение и вычитание
]
)
# Тестовое выражение
test_expr = «2 + 3 * 4 — 1»
result = expr.parseString(test_expr)
print(«Parsed expression:», result.asList())
Cначала задаём правило для числа: Word(nums) ищет последовательность цифр, а setParseAction(lambda t: int(t[0]))
сразу преобразует её в число. Далее функция infixNotation
автоматически создает структуру разбора, учитывая, что умножение и деление имеют более высокий приоритет, чем сложение и вычитание. Если запустить код, то на экран будет выведено нечто вроде:
Parsed expression: [2, '+', [3, '*', 4], '‑', 1]
Это означает, что сначала будет выполнено умножение (3 * 4), а затем — сложение и вычитание.
Создание собственной грамматики с поддержкой скобок
Чтобы сделать парсер более гибким, добавим поддержку скобок. Здесь понадобится класс Forward
, который позволяет определять рекурсивные правила, и функция Suppress
, чтобы убрать лишние символы из итогового результата.
from pyparsing import Forward, Group, Literal, Suppress, oneOf
# Объявляем рекурсивное выражение
expr = Forward()
# Определяем символы скобок; Suppress убирает скобки из финального результата
lpar = Suppress(«(«)
rpar = Suppress(„)“)
# Правило „atom“: число или выражение в скобках (собирается в отдельный список)
atom = number | Group(lpar + expr + rpar)
# Определяем оператор
operator = oneOf(„+ — /“)
# Задаем грамматику: сначала атом, затем может следовать оператор и еще выражение
expr <<= atom + (operator + expr)[...]
# Тестовое выражение со скобками
test_expr = „2 + (3 (4 — 1))“
parsed = expr.parseString(test_expr)
print(„Custom parsed:“, parsed.asList())
Здесь переменная expr
объявляется как Forward()
, что позволяет использовать её до окончательного определения. Правило atom говорит, что допустимым элементом может быть либо число, либо группа, заключенная в скобки. Если запустить этот код, результатом будет:
Custom parsed: [2, '+', [3, '*', [4, '‑', 1]]]
Так отражается вложенность операций, где скобки изменяют порядок вычислений.
Встраивание действий
Теперь мы сделаем парсер умнее — встроим вычисление выражений прямо в процесс разбора. Для этого напишем функцию, которая будет рекурсивно обрабатывать полученную структуру и вычислять значение.
def evalBinaryExpression(tokens):
«„“Рекурсивно вычисляет значение арифметического выражения.»»»
# Если токен — число, возвращаем его
if isinstance(tokens, int):
return tokens
# tokens — вложенный список, извлекаем первый элемент
tokens = tokens[0]
if len(tokens) == 1:
return tokens[0]
# Предполагаем, что список выглядит как: [число, оператор, число,...]
result = tokens[0]
for i in range(1, len(tokens), 2):
op = tokens[i]
operand = tokens[i+1]
if op == '+':
result += operand
elif op == '‑':
result ‑= operand
elif op == '*':
result = operand
elif op == '/':
result /= operand
return result
# Создаем парсер для арифметических выражений, как раньше
number = Word(nums).setParseAction(lambda t: int(t[0]))
expr = infixNotation(number,
[
(oneOf(' /'), 2, opAssoc.LEFT),
(oneOf('+ ‑'), 2, opAssoc.LEFT),
]
)
# Встраиваем вычисление: результатом разбора сразу становится число
expr.setParseAction(lambda t: evalBinaryExpression(t.asList()))
test_expr = «2 + 3 * 4 — 1»
value = expr.parseString(test_expr)[0]
print(«Computed value:», value)
В функции evalBinaryExpression
обрабатываем список токенов, представляющий выражение. Если элемент — число, просто возвращаем его; иначе, последовательно применяем операторы к первому числу. После установки этого действия с помощью setParseAction при разборе строки «2 + 3 4 — 1» сразу получится итоговое значение 13, так как сначала вычисляется умножение (3 4 = 12), потом сложение (2 + 12 = 14) и вычитание (14 — 1 = 13).
Создание мини-языка
Теперь добавим поддержку переменных, чтобы можно было писать такие конструкции, как let x = 10 и затем использовать x в выражениях. Для этого определим правило для присваивания, а также обработку идентификаторов.
from pyparsing import Keyword, Suppress, alphas, alphanums, Group
# Определяем ключевое слово для присваивания и идентификатор
LET = Keyword(«let»)
identifier = Word(alphas, alphanums + «_»)
equals = Suppress(«=»)
# Правило для присваивания: let <идентификатор> = <выражение>
assignment = Group(LET + identifier(«var») + equals + expr(«value»))
# Словарь для хранения переменных
variables = {}
def assignAction(tokens):
var_name = tokens.var
value = tokens.value
variables[var_name] = value
return (var_name, value)
assignment.setParseAction(assignAction)
# Правило для обращения к переменной (идентификатор)
var_ref = identifier.copy()
def varAction(tokens):
var_name = tokens[0]
if var_name in variables:
return variables[var_name]
raise Exception(f»Переменная {var_name} не определена»)
var_ref.setParseAction(varAction)
# Обновляем правило для операндов: число, переменная или выражение в скобках
from pyparsing import Forward, Literal
lpar = Suppress(«(«)
rpar = Suppress(„)“)
operand = number | var_ref | Group(lpar + expr + rpar)
operator = oneOf(„+ — /“)
expr <<= operand + (operator + expr)[...]
# Тест присваивания
test_assign = „let x = 10“
res_assign = assignment.parseString(test_assign)[0]
print(„Assignment:“, res_assign)
print(„Current variables:“, variables)
# Тест выражения с использованием переменной
test_expr = „x 2 + 3“
result_expr = expr.parseString(test_expr)[0]
print(„Expression with variable:“, result_expr)
Конструкция let x = 10 сохраняет значение переменной x
в словаре variables
с помощью функции assignAction
. Правило var_ref
позволяет при встрече идентификатора заменить его значением из словаря. Таким образом, если выполнить строку let x = 10, переменная x получит значение 10, а выражение «x 2 + 3» будет вычислено как 10 2 + 3 = 23.
Обработка ошибок и прочие возможности
Любой парсер должен обрабатывать ошибки, чтобы пользователь мог понять, что именно не так в его вводе. Pyparsing позволяет установить обработчик ошибок, который будет выводить понятное сообщение, если входной текст не соответствует грамматике.
def errorHandler(s, loc, expr, err):
msg = f»Ошибка в позиции {loc}: ожидалось {expr}»
raise Exception(msg)
# Применяем обработчик ошибок к выражению
expr.setFailAction(errorHandler)
# Пример некорректного выражения для демонстрации
try:
bad_expr = «2 + * 3»
expr.parseString(bad_expr)
except Exception as e:
print(«Caught error:», e)
В данном примере функция errorHandler
вызывается, если парсер не может корректно разобрать строку. Она сообщает позицию ошибки и что ожидалось, что помогает быстро найти и исправить проблему. При разборе строки «2 + * 3» будет выброшено исключение с сообщением об ошибке, что показывает, как работает обработка ошибок.
Заключение
Теперь вы немного знаете о технической стороне создания собственного парсера. Какие идеи для собственного парсера или мини‑языка возникли у вас? Вдохновиться можно на GitHub, один пользователь собрал большой список примеров парсеров: https://github.com/pyparsing/pyparsing/tree/master/examples
Какие бы идеи вы ни выбрали, парсер — это серьезный инструмент для реализации самых смелых проектов. Делитесь своими мыслями в комментариях.
Материал подготовлен в рамках нового курса Otus «Python-разработчик для подростков». Оставьте заявку на странице курса и получите бесплатное индивидуальное занятие-диагностику.