В прошлой статье мы познакомились с удобной библиотекой синтаксического анализа Pyparsing и написали парсер для выражения
В этой статье мы начнём погружение в Pyparsing на примере задачи парсинга единиц измерения. Шаг за шагом мы создадим рекурсивный парсер, который умеет искать символы на русском языке, проверять допустимость названия единицы измерения, а также группировать те из них, которые пользователь заключил в скобки.
Примечание: Код этой статьи протестирован и выложен на Sagemathclod. Если у Вас вдруг что-то не работает (скорее всего из-за кодировки текста), обязательно сообщите мне об этом в личку, в комментариях или напишите мне на почту или в ВК.
В качестве примера будем парсить выражение:
Эта единица измерения была взята из головы с целью получить строку, анализ которой задействовал бы все возможности нашего парсера. Нам нужно получить:
Заменив в строке
Таким образом, каждый кортеж в переменной
Перед тем, как использовать pyparsing, его необходимо импортировать:
Когда мы напишем парсер, мы заменим * на использованные нами классы.
При использовании pyparsing следует придерживаться следующей методики написания парсера:
В нашем случае основными «кирпичиками» являются названия отдельных единиц измерения и их степени.
Единица измерения — это слово, которое начинается с буквы и состоит из букв и точек (например мм.рт.ст.). В pyparsing мы можем записать:
Обратите внимание, что у класса
К сожалению, если мы попробуем распарсить любую единицу измерения, мы обнаружим, что парсер работает только для единиц измерения на английском языке. Это потому, что
Данная проблема обходится очень легко. Сначала создадим строку, перечисляющую все буквы на русском:
И код парсера для отдельной единицы измерения следует изменить на:
Теперь наш парсер понимает единицы измерения на русском и английском языках. Для других языков код парсера пишется аналогично.
При тестировании парсера для единицы измерения Вы можете получить результат, в котором русские символы заменены их кодовым обозначением. Например, на Sage:
Если Вы получили такой же результат, значит, всё работает правильно, но нужно поправить кодировку. В моём случае (sage) работает использование «самодельной» функции
Используя эту функцию, мы получим вывод в Sage в правильной кодировке:
Научимся парсить степень. Обычно степень — это целое число. Однако в редких случаях степень может содержать дробную часть или быть записанной в экспоненциальной нотации. Поэтому мы напишем парсер для обычного числа, например, такого:
«Кирпичиком» произвольного числа является натуральное число, которое состоит из цифр:
Перед числом может стоять знак плюс или минус. При этом знак плюс выводить в результат не надо (используем
Вертикальная черта означает «или» (плюс или минус).
Теперь мы можем написать парсер для всего числа. Число начинается с необязательного знака плюс или минус, потом идут цифры, потом необязательная точка — разделитель дробной части, потом цифры, потом может идти символ e, после которого — снова число: необязательный плюс-минус и цифры. У числа после e дробной части уже нет. На pyparsing:
У нас теперь есть парсер для числа. Посмотрим, как работает парсер:
Как мы видим, число разбито на отдельные составляющие. Нам это ни к чему, и мы бы хотели «собрать» число обратно. Это делается при помощи
Проверим:
Отлично! Но… На выходе по-прежнему строка, а нам нужно число. Добавим преобразование строки в число, используя
Мы используем анонимную функцию
Отдельная единица измерения — это название единицы измерения, после которой может идти знак степени ^ и число — степень, в которую необходимо возвести. На pyparsing:
Протестируем:
Сразу усовершенствуем вывод. Нам не нужно видеть ^ в результате парсинга, и мы хотим видеть результат в виде кортежа (см. переменную res в начале этой статьи). Для подавления вывода используем
Проверим:
Мы подошли к интересному месту — описанию реализации рекурсии. При написании единицы измерения пользователь может обрамить скобками одну или несколько единиц измерения, между которыми стоят знаки умножения и деления. Выражение в скобках может содержать другое, вложенное выражение, обрамлённое скобками (например
Вначале напишем выражение, не обращая внимание, что у нас есть рекурсия:
В том виде, как сейчас, оставлять
Таким образом, при написании парсера нет необходимости заранее предвидеть рекурсию. Сначала пишите выражение так, как будто в нём не будет рекурсии, а когда увидите, что она появилась, просто замените знак = на << и строкой выше добавьте присваивание класса
Проверим:
У нас остался последний шаг: общее выражение для единицы измерения. На pyparsing:
Обратите внимание, что выражение имеет вид
Итак, мы написали черновой вариант парсера:
Проверим:
Мы уже близко к тому результату, который хотим получить. Первое, что нам нужно реализовать — группировка тех единиц измерения, которых пользователь обрамил скобками. Для этого в Pyparsing используется
Посмотрим, что изменилось:
В некоторых кортежах после запятой ничего не стоит. Напомню, что кортеж соответствует единице измерения и имеет вид (единица измерения, степень). Вспомним, что мы можем давать имена определённым кусочкам результата работы парсера (описано в прошлой статье). В частности, назовём найденную единицу измерения как
Теперь весь наш парсер выдаёт следующий результат:
В коде выше вместо
Всё, что нам осталось сделать — это убрать в результате парсера знаки * и /, а также вложенные квадратные скобки. Если перед вложенным списком (т. е. перед [) стоит деление, знак степени у единиц измерения во вложенном списке надо поменять на противоположный. Для этого напишем отдельную функцию
После этого наш парсер возвращает единицу измерения в нужном формате:
Обратите внимание, что функция
Последнее, что было обещано сделать — внедрить раннюю проверку единиц измерения. Другими словами, как только парсер найдёт единицу измерения, он сразу проверит её по нашей базе данных.
В качестве базы данных будем использовать словарь Python:
Чтобы быстро проверить единицу измерения, хорошо было бы создать множество Python, поместив в него единицы измерения:
Напишем функцию
Вывод парсера не изменится, но, если попадётся единица измерения, которая отсутствует в базе данных или в науке, то пользователь получит сообщение об ошибке. Пример:
Последняя строчка и есть наше сообщение пользователю об ошибке.
В заключение приведу полный код парсера. Не забудьте в строке импорта
Благодарю вас за терпение, с которым вы прочитали мою статью. Напомню, что код, представленный в этой статье, выложен на Sagemathcloud. Если вы не зарегистрированы на Хабре, вы можете прислать мне вопрос на почту или написать в ВК. В следующей статье я хочу познакомить вас с Sagemathcloud, показать, насколько сильно он может упростить вашу работу на Python. После этого я вернусь к теме парсинга на Pyparsing на качественно новом уровне.
Благодарю Дарью Фролову и Никиту Коновалова за помощь в проверке статьи перед её публикацией.
'import matplotlib.pyplot as plt'
.В этой статье мы начнём погружение в Pyparsing на примере задачи парсинга единиц измерения. Шаг за шагом мы создадим рекурсивный парсер, который умеет искать символы на русском языке, проверять допустимость названия единицы измерения, а также группировать те из них, которые пользователь заключил в скобки.
Примечание: Код этой статьи протестирован и выложен на Sagemathclod. Если у Вас вдруг что-то не работает (скорее всего из-за кодировки текста), обязательно сообщите мне об этом в личку, в комментариях или напишите мне на почту или в ВК.
Начало работы. Исходные данные и задача.
В качестве примера будем парсить выражение:
s = "Н*м^2/(кг*с^2)"
Эта единица измерения была взята из головы с целью получить строку, анализ которой задействовал бы все возможности нашего парсера. Нам нужно получить:
res = [('Н',1.0), ('м',2.0), ('кг',-1.0), ('с',-2.0)]
Заменив в строке
s
деление умножением, раскрыв скобки и явно проставив степени у единиц измерения, получим: Н*м^2/(кг*с^2) = Н^1 * м^2 * кг^-1 * с^-2.Таким образом, каждый кортеж в переменной
res
содержит название единицы измерения и степень, в которую её необходимо возвести. Между кортежами можно мысленно поставить знаки умножения.Перед тем, как использовать pyparsing, его необходимо импортировать:
from pyparsing import *
Когда мы напишем парсер, мы заменим * на использованные нами классы.
Методика написания парсера на Pyparsing
При использовании pyparsing следует придерживаться следующей методики написания парсера:
- Сначала из текстовой строки выделяются ключевые слова или отдельные важные символы, которые являются «кирпичиками» для построения конечной строки.
- Пишем отдельные парсеры для «кирпичиков».
- «Собираем» парсер для конечной строки.
В нашем случае основными «кирпичиками» являются названия отдельных единиц измерения и их степени.
Написание парсера для единицы измерения. Парсинг русских букв.
Единица измерения — это слово, которое начинается с буквы и состоит из букв и точек (например мм.рт.ст.). В pyparsing мы можем записать:
ph_unit = Word(alphas, alphas+'.')
Обратите внимание, что у класса
Word
теперь 2 аргумента. Первый аргумент отвечает за то, что должно быть первым символом у слова, второй аргумент — за то, какими могут быть остальные символы слова. Единица измерения обязательно начинается с буквы, поэтому мы поставили первым аргументом alphas
. Помимо букв единица измерения может содержать точку (например, мм.рт.ст), поэтому второй аргумент у Word
– alphas + '.'
.К сожалению, если мы попробуем распарсить любую единицу измерения, мы обнаружим, что парсер работает только для единиц измерения на английском языке. Это потому, что
alphas
подразумевает не просто буквы, а буквы английского алфавита.Данная проблема обходится очень легко. Сначала создадим строку, перечисляющую все буквы на русском:
rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ'
И код парсера для отдельной единицы измерения следует изменить на:
ph_unit = Word(alphas+rus_alphas, alphas+rus_alphas+'.')
Теперь наш парсер понимает единицы измерения на русском и английском языках. Для других языков код парсера пишется аналогично.
Коррекция кодировки результата работы парсера.
При тестировании парсера для единицы измерения Вы можете получить результат, в котором русские символы заменены их кодовым обозначением. Например, на Sage:
ph_unit.parseString("мм").asList()
# Получим: ['\xd0\xbc\xd0\xbc']
Если Вы получили такой же результат, значит, всё работает правильно, но нужно поправить кодировку. В моём случае (sage) работает использование «самодельной» функции
bprint
(better print):def bprint(obj):
print(obj.__repr__().decode('string_escape'))
Используя эту функцию, мы получим вывод в Sage в правильной кодировке:
bprint(ph_unit.parseString("мм").asList())
# Получим: ['мм']
Написание парсера для степени. Парсинг произвольного числа.
Научимся парсить степень. Обычно степень — это целое число. Однако в редких случаях степень может содержать дробную часть или быть записанной в экспоненциальной нотации. Поэтому мы напишем парсер для обычного числа, например, такого:
test_num = "-123.456e-3"
«Кирпичиком» произвольного числа является натуральное число, которое состоит из цифр:
int_num = Word(nums)
Перед числом может стоять знак плюс или минус. При этом знак плюс выводить в результат не надо (используем
Suppress()
).pm_sign = Optional(Suppress("+") | Literal("-"))
Вертикальная черта означает «или» (плюс или минус).
Literal()
означает точное соответствие текстовой строке. Таким образом, выражение для pm_sign
означает, что надо найти в тексте необязательный символ +, который не надо выводить в результат парсинга, или необязательный символ минус.Теперь мы можем написать парсер для всего числа. Число начинается с необязательного знака плюс или минус, потом идут цифры, потом необязательная точка — разделитель дробной части, потом цифры, потом может идти символ e, после которого — снова число: необязательный плюс-минус и цифры. У числа после e дробной части уже нет. На pyparsing:
float_num = pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)
У нас теперь есть парсер для числа. Посмотрим, как работает парсер:
float_num.parseString('-123.456e-3').asList()
# Получим ['-', '123', '.', '456', 'e', '-', '3']
Как мы видим, число разбито на отдельные составляющие. Нам это ни к чему, и мы бы хотели «собрать» число обратно. Это делается при помощи
Combine()
:float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num))
Проверим:
float_num.parseString('-123.456e-3').asList()
# Получим ['-123.456e-3']
Отлично! Но… На выходе по-прежнему строка, а нам нужно число. Добавим преобразование строки в число, используя
ParseAction()
:float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0]))
Мы используем анонимную функцию
lambda
, аргументом которой является t
. Сначала мы получаем результат в виде списка (t.asList())
. Т.к. полученный список имеет только один элемент, его сразу можно извлечь: t.asList()[0]
. Функция float()
преобразует текст в число с плавающей точкой. Если вы работаете в Sage, можете заменить float
на RR
— конструктор класса вещественных чисел Sage.Парсинг единицы измерения со степенью.
Отдельная единица измерения — это название единицы измерения, после которой может идти знак степени ^ и число — степень, в которую необходимо возвести. На pyparsing:
single_unit = ph_unit + Optional('^' + float_num)
Протестируем:
bprint(single_unit.parseString("м^2").asList())
# Получим: ['м', '^', 2.0]
Сразу усовершенствуем вывод. Нам не нужно видеть ^ в результате парсинга, и мы хотим видеть результат в виде кортежа (см. переменную res в начале этой статьи). Для подавления вывода используем
Suppress()
, для преобразования списка в кортеж — ParseAction()
:single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList()))
Проверим:
bprint(single_unit.parseString("м^2").asList())
# Получим: [('м', 2.0)]
Парсинг единиц измерения, обрамлённых скобками. Реализация рекурсии.
Мы подошли к интересному месту — описанию реализации рекурсии. При написании единицы измерения пользователь может обрамить скобками одну или несколько единиц измерения, между которыми стоят знаки умножения и деления. Выражение в скобках может содержать другое, вложенное выражение, обрамлённое скобками (например
"(м^2/ (с^2 * кг))"
). Возможность вложения одних выражений со скобками в другие и есть источник рекурсии. Перейдём к Pyparsing.Вначале напишем выражение, не обращая внимание, что у нас есть рекурсия:
unit_expr = Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
Optional
содержит ту часть строки, которая может присутствовать, а может отсутствовать. OneOrMore
(переводится как «один или больше») содержит ту часть строки, которая должна встретиться в тексте не менее одного раза. OneOrMore
содержит два «слагаемых»: сначала мы ищем знак умножения и деления, потом единицу измерения или вложенное выражение.В том виде, как сейчас, оставлять
unit_expr
нельзя: слева и справа от знака равенства есть unit_expr
, что однозначно свидетельствует о рекурсии. Решается эта проблема очень просто: надо поменять знак присваивания на <<, а в строке перед unit_expr
добавить присваивание специального класса Forward()
:unit_expr = Forward()
unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
Таким образом, при написании парсера нет необходимости заранее предвидеть рекурсию. Сначала пишите выражение так, как будто в нём не будет рекурсии, а когда увидите, что она появилась, просто замените знак = на << и строкой выше добавьте присваивание класса
Forward()
.Проверим:
bprint(unit_expr.parseString("(Н*м/с^2)").asList())
# Получим: [('Н',), '*', ('м',), '/', ('с', 2.0)]
Парсинг общего выражения для единицы измерения.
У нас остался последний шаг: общее выражение для единицы измерения. На pyparsing:
parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
Обратите внимание, что выражение имеет вид
(a | b) + (c | d)
. Скобки здесь обязательны и имеют ту же роль, что и в математике. Используя скобки, мы хотим указать, что вначале надо проверить, что первое слагаемое — unit_expr
или single_unit
, а второе слагаемое — необязательное выражение. Если скобки убрать, то получится, что parse_unit
– это unit_expr
или single_unit
+ необязательное выражение, что не совсем то, что мы задумывали. Те же рассуждения применимы и к выражению внутри Optional()
.Черновой вариант парсера. Коррекция кодировки результата.
Итак, мы написали черновой вариант парсера:
from pyparsing import *
rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ'
ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.')
int_num = Word(nums)
pm_sign = Optional(Suppress("+") | Literal("-"))
float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0]))
single_unit = (ph_unit + Optional(Suppress('^') + float_num)).setParseAction(lambda t: tuple(t.asList()))
unit_expr = Forward()
unit_expr << Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")")
parse_unit = (unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))
Проверим:
print(s) # s = "Н*м^2/(кг*с^2)" — см. начало статьи.
bprint(parse_unit.parseString(s).asList())
# Получим: [('Н',), '*', ('м', 2.0), '/', ('кг',), '*', ('с', 2.0)]
Группировка единиц измерения, обрамлённых скобками.
Мы уже близко к тому результату, который хотим получить. Первое, что нам нужно реализовать — группировка тех единиц измерения, которых пользователь обрамил скобками. Для этого в Pyparsing используется
Group()
, который мы применим к unit_expr
:unit_expr = Forward()
unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")"))
Посмотрим, что изменилось:
bprint(parse_unit.parseString(s).asList())
# Получим: [('Н',), '*', ('м', 2.0), '/', [('кг',), '*', ('с', 2.0)]]
Ставим степень 1 в тех кортежах, где степень отсутствует.
В некоторых кортежах после запятой ничего не стоит. Напомню, что кортеж соответствует единице измерения и имеет вид (единица измерения, степень). Вспомним, что мы можем давать имена определённым кусочкам результата работы парсера (описано в прошлой статье). В частности, назовём найденную единицу измерения как
'unit_name'
, а её степень как 'unit_degree'
. В setParseAction()
напишем анонимную функцию lambda()
, которая будет ставить 1 там, где пользователь не указал степень единицы измерения). На pyparsing:single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree))
Теперь весь наш парсер выдаёт следующий результат:
bprint(parse_unit.parseString(s).asList())
# Получим: [('Н', 1.0), '*', ('м', 2.0), '/', [('кг', 1.0), '*', ('с', 2.0)]]
В коде выше вместо
float(1)
можно было бы написать просто 1.0
, но в Sage в таком случае получится не тип float
, а собственный тип Sage для вещественных чисел.Убираем из результата парсера знаки * и /, раскрываем скобки.
Всё, что нам осталось сделать — это убрать в результате парсера знаки * и /, а также вложенные квадратные скобки. Если перед вложенным списком (т. е. перед [) стоит деление, знак степени у единиц измерения во вложенном списке надо поменять на противоположный. Для этого напишем отдельную функцию
transform_unit()
, которую будем использовать в setParseAction()
для parse_unit
:def transform_unit(unit_list, k=1):
res = []
for v in unit_list:
if isinstance(v, tuple):
res.append(tuple((v[0], v[1]*k)))
elif v == "/":
k = -k
elif isinstance(v, list):
res += transform_unit(v, k=k)
return(res)
parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList()))
После этого наш парсер возвращает единицу измерения в нужном формате:
bprint(transform_unit(parse_unit.parseString(s).asList()))
# Получим: [('Н', 1.0), ('м', 2.0), ('кг', -1.0), ('с', -2.0)]
Обратите внимание, что функция
transform_unit()
убирает вложенность. В процессе преобразования все скобки раскрываются. Если перед скобкой стоит знак деления, знак степени единиц измерения в скобках меняется на противоположный.Реализация проверки единиц измерения непосредственно в процессе парсинга.
Последнее, что было обещано сделать — внедрить раннюю проверку единиц измерения. Другими словами, как только парсер найдёт единицу измерения, он сразу проверит её по нашей базе данных.
В качестве базы данных будем использовать словарь Python:
unit_db = {'Длина':{'м':1, 'дм':1/10, 'см':1/100, 'мм':1/1000, 'км':1000, 'мкм':1/1000000}, 'Сила':{'Н':1}, 'Мощность':{'Вт':1, 'кВт':1000}, 'Время':{'с':1}, 'Масса':{'кг':1, 'г':0.001}}
Чтобы быстро проверить единицу измерения, хорошо было бы создать множество Python, поместив в него единицы измерения:
unit_set = set([t for vals in unit_db.values() for t in vals])
Напишем функцию
check_unit
, которая будет проверять единицу измерения, и вставим её в setParseAction
для ph_unit
:def check_unit(unit_name):
if not unit_name in unit_set:
raise ValueError("Единица измерения указана неверно или отсутствует в базе данных: " + unit_name)
return(unit_name)
ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0]))
Вывод парсера не изменится, но, если попадётся единица измерения, которая отсутствует в базе данных или в науке, то пользователь получит сообщение об ошибке. Пример:
ph_unit.parseString("дюйм")
# Получим сообщение об ошибке:
Error in lines 1-1
Traceback (most recent call last):
…
File "", line 1, in <lambda>
File "", line 3, in check_unit
ValueError: Единица измерения указана неверно или отсутствует в базе данных: дюйм
Последняя строчка и есть наше сообщение пользователю об ошибке.
Полный код парсера. Заключение.
В заключение приведу полный код парсера. Не забудьте в строке импорта
"from pyparsing import *"
заменить * на использованные классы.from pyparsing import nums, alphas, Word, Literal, Optional, Combine, Forward, Group, Suppress, OneOrMore
def bprint(obj):
print(obj.__repr__().decode('string_escape'))
# База данных единиц измерения
unit_db = {'Длина':{'м':1, 'дм':1/10, 'см':1/100, 'мм':1/1000, 'км':1000, 'мкм':1/1000000}, 'Сила':{'Н':1}, 'Мощность':{'Вт':1, 'кВт':1000}, 'Время':{'с':1}, 'Масса':{'кг':1, 'г':0.001}}
unit_set = set([t for vals in unit_db.values() for t in vals])
# Парсер для единицы измерения с проверкой её по базе данных
rus_alphas = 'йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ'
def check_unit(unit_name):
"""
Проверка единицы измерения по базе данных.
"""
if not unit_name in unit_set:
raise ValueError("Единица измерения указана неверно или отсутствует в базе данных: " + unit_name)
return(unit_name)
ph_unit = Word(rus_alphas+alphas, rus_alphas+alphas+'.').setParseAction(lambda t: check_unit(t.asList()[0]))
# Парсер для степени
int_num = Word(nums)
pm_sign = Optional(Suppress("+") | Literal("-"))
float_num = Combine(pm_sign + int_num + Optional('.' + int_num) + Optional('e' + pm_sign + int_num)).setParseAction(lambda t: float(t.asList()[0]))
# Парсер для единицы измерения со степенью
single_unit = (ph_unit('unit_name') + Optional(Suppress('^') + float_num('unit_degree'))).setParseAction(lambda t: (t.unit_name, float(1) if t.unit_degree == "" else t.unit_degree))
# Парсер для выражения в скобках
unit_expr = Forward()
unit_expr << Group(Suppress('(') + single_unit + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr))) + Suppress(")"))
# Парсер для общего выражения единицы измерения
def transform_unit(unit_list, k=1):
"""
Функция раскрывает скобки в результате, выданном парсером, корректирует знак степени и убирает знаки * и /
"""
res = []
for v in unit_list:
if isinstance(v, tuple):
res.append(tuple((v[0], v[1]*k)))
elif v == "/":
k = -k
elif isinstance(v, list):
res += transform_unit(v, k=k)
return(res)
parse_unit = ((unit_expr | single_unit) + Optional(OneOrMore((Literal("*") | Literal("/")) + (single_unit | unit_expr)))).setParseAction(lambda t: transform_unit(t.asList()))
#Проверка
s = "Н*м^2/(кг*с^2)"
bprint(parse_unit.parseString(s).asList())
Благодарю вас за терпение, с которым вы прочитали мою статью. Напомню, что код, представленный в этой статье, выложен на Sagemathcloud. Если вы не зарегистрированы на Хабре, вы можете прислать мне вопрос на почту или написать в ВК. В следующей статье я хочу познакомить вас с Sagemathcloud, показать, насколько сильно он может упростить вашу работу на Python. После этого я вернусь к теме парсинга на Pyparsing на качественно новом уровне.
Благодарю Дарью Фролову и Никиту Коновалова за помощь в проверке статьи перед её публикацией.