Вглубь Pyparsing: парсим единицы измерения на Python

Вглубь Pyparsing: парсим единицы измерения на Python

В прошлой статье мы познакомились с удобной библиотекой синтаксического анализа Pyparsing и написали парсер для выражения 'import matplotlib.pyplot as plt' .

В этой статье мы начнём погружение в Pyparsing на примере задачи парсинга единиц измерения. Шаг за шагом мы создадим рекурсивный парсер, который умеет искать символы на русском языке, проверять допустимость названия единицы измерения, а также группировать те из них, которые пользователь заключил в скобки. Примечание: Код этой статьи протестирован и выложен на Sagemathclod. Если у Вас вдруг что-то не работает (скорее всего из-за кодировки текста), обязательно сообщите мне об этом в личку, в комментариях или напишите мне на почту или в ВК.

Начало работы. Исходные данные и задача.

В качестве примера будем парсить выражение:

Эта единица измерения была взята из головы с целью получить строку, анализ которой задействовал бы все возможности нашего парсера. Нам нужно получить:

Заменив в строке s деление умножением, раскрыв скобки и явно проставив степени у единиц измерения, получим: Н*м^2/(кг*с^2) = Н^1 * м^2 * кг^-1 * с^-2.

Таким образом, каждый кортеж в переменной res содержит название единицы измерения и степень, в которую её необходимо возвести. Между кортежами можно мысленно поставить знаки умножения.

Перед тем, как использовать pyparsing, его необходимо импортировать:

Когда мы напишем парсер, мы заменим * на использованные нами классы.

Методика написания парсера на Pyparsing

  1. Сначала из текстовой строки выделяются ключевые слова или отдельные важные символы, которые являются «кирпичиками» для построения конечной строки.
  2. Пишем отдельные парсеры для «кирпичиков».
  3. «Собираем» парсер для конечной строки.

Написание парсера для единицы измерения. Парсинг русских букв.

Единица измерения — это слово, которое начинается с буквы и состоит из букв и точек (например мм.рт.ст.). В pyparsing мы можем записать:

Обратите внимание, что у класса Word теперь 2 аргумента. Первый аргумент отвечает за то, что должно быть первым символом у слова, второй аргумент — за то, какими могут быть остальные символы слова. Единица измерения обязательно начинается с буквы, поэтому мы поставили первым аргументом alphas . Помимо букв единица измерения может содержать точку (например, мм.рт.ст), поэтому второй аргумент у Word – alphas + '.' .

К сожалению, если мы попробуем распарсить любую единицу измерения, мы обнаружим, что парсер работает только для единиц измерения на английском языке. Это потому, что alphas подразумевает не просто буквы, а буквы английского алфавита.

Данная проблема обходится очень легко. Сначала создадим строку, перечисляющую все буквы на русском:

И код парсера для отдельной единицы измерения следует изменить на:

Теперь наш парсер понимает единицы измерения на русском и английском языках. Для других языков код парсера пишется аналогично.

Коррекция кодировки результата работы парсера.

При тестировании парсера для единицы измерения Вы можете получить результат, в котором русские символы заменены их кодовым обозначением. Например, на Sage:

Если Вы получили такой же результат, значит, всё работает правильно, но нужно поправить кодировку. В моём случае (sage) работает использование «самодельной» функции bprint (better print):

Используя эту функцию, мы получим вывод в Sage в правильной кодировке:

Написание парсера для степени. Парсинг произвольного числа.

Научимся парсить степень. Обычно степень — это целое число. Однако в редких случаях степень может содержать дробную часть или быть записанной в экспоненциальной нотации. Поэтому мы напишем парсер для обычного числа, например, такого:

«Кирпичиком» произвольного числа является натуральное число, которое состоит из цифр:

Перед числом может стоять знак плюс или минус. При этом знак плюс выводить в результат не надо (используем Suppress() ).

Вертикальная черта означает «или» (плюс или минус). Literal() означает точное соответствие текстовой строке. Таким образом, выражение для pm_sign означает, что надо найти в тексте необязательный символ +, который не надо выводить в результат парсинга, или необязательный символ минус.

Теперь мы можем написать парсер для всего числа. Число начинается с необязательного знака плюс или минус, потом идут цифры, потом необязательная точка — разделитель дробной части, потом цифры, потом может идти символ e, после которого — снова число: необязательный плюс-минус и цифры. У числа после e дробной части уже нет. На pyparsing:

У нас теперь есть парсер для числа. Посмотрим, как работает парсер:

Как мы видим, число разбито на отдельные составляющие. Нам это ни к чему, и мы бы хотели «собрать» число обратно. Это делается при помощи Combine() :

Отлично! Но… На выходе по-прежнему строка, а нам нужно число. Добавим преобразование строки в число, используя ParseAction() :

Мы используем анонимную функцию lambda , аргументом которой является t . Сначала мы получаем результат в виде списка (t.asList()) . Т.к. полученный список имеет только один элемент, его сразу можно извлечь: t.asList()[0] . Функция float() преобразует текст в число с плавающей точкой. Если вы работаете в Sage, можете заменить float на RR — конструктор класса вещественных чисел Sage.

Парсинг единицы измерения со степенью.

Отдельная единица измерения — это название единицы измерения, после которой может идти знак степени ^ и число — степень, в которую необходимо возвести. На pyparsing:

Сразу усовершенствуем вывод. Нам не нужно видеть ^ в результате парсинга, и мы хотим видеть результат в виде кортежа (см. переменную res в начале этой статьи). Для подавления вывода используем Suppress() , для преобразования списка в кортеж — ParseAction() :

Парсинг единиц измерения, обрамлённых скобками. Реализация рекурсии.

Мы подошли к интересному месту — описанию реализации рекурсии. При написании единицы измерения пользователь может обрамить скобками одну или несколько единиц измерения, между которыми стоят знаки умножения и деления. Выражение в скобках может содержать другое, вложенное выражение, обрамлённое скобками (например "(м^2/ (с^2 * кг))" ). Возможность вложения одних выражений со скобками в другие и есть источник рекурсии. Перейдём к Pyparsing.

Вначале напишем выражение, не обращая внимание, что у нас есть рекурсия:

Optional содержит ту часть строки, которая может присутствовать, а может отсутствовать. OneOrMore (переводится как «один или больше») содержит ту часть строки, которая должна встретиться в тексте не менее одного раза. OneOrMore содержит два «слагаемых»: сначала мы ищем знак умножения и деления, потом единицу измерения или вложенное выражение.

В том виде, как сейчас, оставлять unit_expr нельзя: слева и справа от знака равенства есть unit_expr , что однозначно свидетельствует о рекурсии. Решается эта проблема очень просто: надо поменять знак присваивания на <<, а в строке перед unit_expr добавить присваивание специального класса Forward() :

Таким образом, при написании парсера нет необходимости заранее предвидеть рекурсию. Сначала пишите выражение так, как будто в нём не будет рекурсии, а когда увидите, что она появилась, просто замените знак = на << и строкой выше добавьте присваивание класса Forward() .

Парсинг общего выражения для единицы измерения.

У нас остался последний шаг: общее выражение для единицы измерения. На pyparsing:

Обратите внимание, что выражение имеет вид (a | b) + (c | d) . Скобки здесь обязательны и имеют ту же роль, что и в математике. Используя скобки, мы хотим указать, что вначале надо проверить, что первое слагаемое — unit_expr или single_unit , а второе слагаемое — необязательное выражение. Если скобки убрать, то получится, что parse_unit – это unit_expr или single_unit + необязательное выражение, что не совсем то, что мы задумывали. Те же рассуждения применимы и к выражению внутри Optional() .

Черновой вариант парсера. Коррекция кодировки результата.

Итак, мы написали черновой вариант парсера:

Группировка единиц измерения, обрамлённых скобками.

Мы уже близко к тому результату, который хотим получить. Первое, что нам нужно реализовать — группировка тех единиц измерения, которых пользователь обрамил скобками. Для этого в Pyparsing используется Group() , который мы применим к unit_expr :

Посмотрим, что изменилось:

Ставим степень 1 в тех кортежах, где степень отсутствует.

В некоторых кортежах после запятой ничего не стоит. Напомню, что кортеж соответствует единице измерения и имеет вид (единица измерения, степень). Вспомним, что мы можем давать имена определённым кусочкам результата работы парсера (описано в прошлой статье). В частности, назовём найденную единицу измерения как 'unit_name' , а её степень как 'unit_degree' . В setParseAction() напишем анонимную функцию lambda() , которая будет ставить 1 там, где пользователь не указал степень единицы измерения). На pyparsing:

Теперь весь наш парсер выдаёт следующий результат:

В коде выше вместо float(1) можно было бы написать просто 1.0 , но в Sage в таком случае получится не тип float , а собственный тип Sage для вещественных чисел.

Убираем из результата парсера знаки * и /, раскрываем скобки.

Всё, что нам осталось сделать — это убрать в результате парсера знаки * и /, а также вложенные квадратные скобки. Если перед вложенным списком (т. е. перед [) стоит деление, знак степени у единиц измерения во вложенном списке надо поменять на противоположный. Для этого напишем отдельную функцию transform_unit() , которую будем использовать в setParseAction() для parse_unit :

После этого наш парсер возвращает единицу измерения в нужном формате:

Обратите внимание, что функция transform_unit() убирает вложенность. В процессе преобразования все скобки раскрываются. Если перед скобкой стоит знак деления, знак степени единиц измерения в скобках меняется на противоположный.

Реализация проверки единиц измерения непосредственно в процессе парсинга.

Последнее, что было обещано сделать — внедрить раннюю проверку единиц измерения. Другими словами, как только парсер найдёт единицу измерения, он сразу проверит её по нашей базе данных.

В качестве базы данных будем использовать словарь Python:

Чтобы быстро проверить единицу измерения, хорошо было бы создать множество Python, поместив в него единицы измерения:

Напишем функцию check_unit , которая будет проверять единицу измерения, и вставим её в setParseAction для ph_unit :

Вывод парсера не изменится, но, если попадётся единица измерения, которая отсутствует в базе данных или в науке, то пользователь получит сообщение об ошибке. Пример:

Последняя строчка и есть наше сообщение пользователю об ошибке.

Полный код парсера. Заключение.

В заключение приведу полный код парсера. Не забудьте в строке импорта "from pyparsing import *" заменить * на использованные классы.

Благодарю вас за терпение, с которым вы прочитали мою статью. Напомню, что код, представленный в этой статье, выложен на Sagemathcloud. Если вы не зарегистрированы на Хабре, вы можете прислать мне вопрос на почту или написать в ВК. В следующей статье я хочу познакомить вас с Sagemathcloud, показать, насколько сильно он может упростить вашу работу на Python. После этого я вернусь к теме парсинга на Pyparsing на качественно новом уровне.

Благодарю Дарью Фролову и Никиту Коновалова за помощь в проверке статьи перед её публикацией.

📎📎📎📎📎📎📎📎📎📎