Раз, два, три — парсим, как арабские математики XVIII века!
Именно введение арабских цифр и позиционной системы записи позволило Европе перейти от абака к алгоритмам, способным решать задачи в письменном виде. Сегодня мы проделаем аналогичный путь: избавимся от «абаковых» ручных вычислений и научимся парсить формулы арабской записи с помощью элегантных алгоритмов.
Что такое арабский парсинг?
Парсер — это программа, которая анализирует текстовые данные и извлекает структурированную информацию. В контексте «арабского парсинга» речь идёт о двух вещах:
- Обработка чисел в арабской системе счисления.
- Корректное отображение смешанного текста (арабско-латинского) с учётом направления письма справа налево (RTL).
Этапы парсинга формул
Лексический анализ (токенизация)
На этом этапе входная строка разбивается на лексемы, минимальные неделимые единицы:
Вход: "٣(١+٢)+√١٦"
Токены: [ '٣', '', '(', '١', '+', '٢', ')', '+', '√', '١٦' ]
Синтаксический анализ (построение дерева)
Используем LL(1) или LR(1) грамматику, описанную в BNF:
Expr ::= Term ('+'|'-') Expr | Term
Term ::= Factor ('*'|'/') Term | Factor
Factor::= NUMBER | '(' Expr ')' | '√' Factor
Учёт Unicode BiDi-алгоритма
Арабские цифры и математические знаки сохраняют нейтральное направление, но вставки английских переменных force-LTR требуют явного указания направления символом ‎ или U+202A.
Пример:
"النتيجة = ١٠ + 2x ‎ + ٣"
Алгоритм «Арабский парсер» на Python
Шаг 1. Импорт библиотек
from typing import List, Union
import unicodedata
Шаг 2. Лексер
def tokenize(expr: str) -> List[str]:
tokens = []
i = 0
while i < len(expr):
c = expr[i]
if c in "٠١٢٣٤٥٦٧٨٩0123456789":
num = c
while i+1 < len(expr) and expr[i+1] in "٠١٢٣٤٥٦٧٨٩0123456789":
i += 1
num += expr[i]
tokens.append(str(int(num.translate(str.maketrans("٠١٢٣٤٥٦٧٨٩", "0123456789")))))
elif c in "+-*/√":
tokens.append(c)
elif not c.isspace:
raise ValueError(f"Неизвестный символ {c}")
i += 1
return tokens
Шаг 3. Парсер-рекурсивный спуск
class ArabParser:
def __init__(self, tokens: List[str]):
self.tokens = tokens
self.pos = 0
def parse(self) -> float:
return self.expr
def expr(self):
val = self.term
while self.current in ('+', '-'):
op = self.consume
right = self.term
val = val + right if op == '+' else val ― right
return val
def term(self):
val = self.factor
while self.current in ('', '/'):
op = self.consume
right = self.factor
val = val right if op == '*' else val / right
return val
def factor(self):
if self.current == '(':
self.consume
val = self.expr
assert self.consume == ')'
return val
elif self.current == '√':
self.consume
return self.factor ** 0.5
else:
return float(self.consume)
def current(self):
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
def consume(self):
tok = self.current
self.pos += 1
return tok
Пробуем в деле
tokens = tokenize("√١٦ + ٣ * (١+٢)")
print(ArabParser(tokens).parse) # 11.0
Советы по RTL-вёрстке
- Используйте CSS
direction: rtl;для контейнера с арабским текстом. - Для математических формул применяйте flexbox с направлением
ltr, чтобы знаки «+» и «=» выглядели естественно. - Добавляйте невидимые маркеры направления Unicode (U+200E, U+202A) при смешанном контенте.
Как и алгоритмики средневековой Европы, мы победили «абак» ручного разбора формул, создав компактный арабский парсер. Он умеет:
- Понимать арабские и латинские цифры в одном выражении.
- Корректно обрабатывать смешанное направление текста.
- Выдавать результат в любом машинно-читаемом формате:
.txt,.json,.sql.
Устраивайтесь поудобнее, запускайте скрипт, и пусть ваши данные потекут, как арабские цифры в XVIII веке: быстро, точно и без абака!