Как стать автором
Обновить
522.67
OTUS
Цифровые навыки от ведущих экспертов

Парсер для подростков с помощью pyparsing

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров6.3K

Привет, Хабр!

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

Теги:
Хабы:
Всего голосов 8: ↑5 и ↓3+3
Комментарии3

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS