Привет, Хабр!
Если ты подросток и начинаешь свой путь в программировании, или просто хочешь понять, как программы анализируют текст и превращают его в структуру, эта статья для тебя. Сегодня поговорим о том, что такое парсер, зачем он нужен и как с помощью библиотеки 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-разработчик для подростков». Оставьте заявку на странице курса и получите бесплатное индивидуальное занятие-диагностику.
