pyparsing
Материал из Xgu.ru
Автор: Владимир Кореньков
Содержание |
[править] Pyparsing — модуль синтаксического анализа для языка Python
Для языка Python создано достаточно большое количество синтаксических анализаторов. В данной статье будет рассмотрен один из них — pyparsing.
Подобно регулярным выражениям, последовательность работы с данной библиотекой содержит три этапа:
- определение грамматики исходного текста;
- применение данной грамматики к исходному тексту (как правило это вызов функции parseString или scanString);
- анализ полученных в виде списка результатов.
[править] Постановка задачи
Задача формулируется следующим образом. Практически все CAM-системы результат генерирования траекторий перемещения инструмента, а также некоторую технологическую информацию, сохраняют в промежуточный формат, который получил обобщенное название CLData.
В принципе существует его описание[1], однако каждая система в этот формат привносит свои особенности, поэтому CLData нельзя назвать полностью "стандартным".
Между CLData и конкретной стойкой ЧПУ находится программа, под общим названием "постпроцессор" (применительно к вычислительной технике, эту программу можно было бы назвать драйвером). Постпроцессор должен решать две глобальные задачи (мелочь -- пока не в счет):
- преобразование форматов (каждая стойка ЧПУ понимает только свойственный ей формат команд, так называемый G-код);
- математические преобразования (см. python/CAM/postporcessor).
Решение же данных задач невозможно без разбора исходного CLData-файла. Собственно этим мы и займемся в данной статье.
[править] Исходные данные
Исходный файл, с которым мы будем работать дальше, имеет следующий вид:
$$ Manufacturing Program.2 $$ 1.00000 0.00000 0.00000 -60.49063 $$ 0.00000 1.00000 0.00000 119.91763 $$ 0.00000 0.00000 1.00000 -21.00000 MACH_AXIS/Manufacturing Program.2,1,$ 1.00000, 0.00000, 0.00000,$ 0.00000, 1.00000, 0.00000,$ 0.00000, 0.00000, 1.00000,$ -60.49063, 119.91763, -21.00000 $$ PP-TABLE : CPOST_MILL V1R8 PARTNO Manufacturing Program.2 PARTNO Part Operation.1 PPRINT 3-axis Machine.1 INIT TLAXIS/ 0.000000, 0.000000, 1.000000 $$ TOOLCHANGEBEGINNING CUTTER/ 18.000000, 0.000000, 9.000000, 0.000000, 0.000000,$ 0.000000, 50.000000 TOOLNO/1,MILL, 18.000000, 0.000000,,$ 100.000000, 60.000000,, 50.000000,4,$ 96.000000,MMPM, 1200.000000,RPM,CLW,ON,$ AUTO, 0.000000,NOTE TPRINT/D18,,D18 PRE_LOADTL/1 LOADTL/1 POST_LOADTL/1 $$ TOOLCHANGEEND PPRINT MACHINE OPERATION = ZLevel PPRINT OPERATION NAME = ZLevel.4 PPRINT TOOL ASSEMBLY = D18 MO_INIT/ZLevel,ZLevel.4,D18 REGLTL/1,1, SPINDL/ 1200.0000,RPM,CLW RAPID GOTO / 52.40160, -133.06004, 47.65000 RAPID GOTO / 52.40160, -133.06004, 45.85000 FEDRAT/ 100.0000,MMPM GOTO / 52.40160, -133.06004, 44.85000 GOTO / 53.34099, -132.71719, 44.85000 INTOL / 0.03000 OUTTOL/ 0.00000 AUTOPS INDIRV/ 0.42181, 0.90668, 0.00000 TLON,GOFWD/ (CIRCLE/ 44.27417, -128.49905, 44.85000,$ 10.00000),ON,(LINE/ 44.27417, -128.49905, 44.85000,$ 48.49231, -119.43222, 44.85000) FEDRAT/ 96.0000,MMPM GOTO / 47.30959, -118.88199, 44.85000 GOTO / 45.15931, -117.74927, 44.85000 GOTO / 42.47095, -115.96498, 44.85000 RAPID GOTO / 35.52745, -127.15614, 47.65000 LIST NCDOC FINI
Файл code.cldata представляет собой последовательность команд, состоящих в общем случае из пары: Имя — аргументы (могут быть необязательными), разделенных разного рода разделителями. Например:
GOTO / x, y, z [i, j, k]
Если команда достаточно длинная, то она может быть разбита символом $ на несколько строк.
[править] Функции обработки входного текста
Для того чтобы не только правильно описать грамматику, а и вообще понять ЧТО мы хотим описать, сначала следует выяснить как будет обрабатываться текст.
Допустим имеется некая переменная pattern -- шаблон (описание грамматики), на соответствие которому проверяется текст. Проверка выполняется методом searchString (для простоты изложения вопроса, пока не будем рассматривать все разнообразие методов):
result = pattern.searchString(text)
Отсюда вытекают два возможных варианта:
- переменная text представляет собой одну строку, в которую помещено все содержимое исходного файла
- переменной text в процессе чтения файла (в цикле) присваивается по отдельной строке данного файла
#!/usr/bin/python # -*- coding: utf-8 -*- from pyparsing import * ... # описываем грамматику pattern = .... # читаем файл: lines -- список, каждый элемент которого содержит отдельную строку файла lines = file('code.cldata','r').readlines() # объединяем все строки в одну text = ''.join(lines) # выполняем поиск for tokens in pattern.searchString(text): print tokens
#!/usr/bin/python # -*- coding: utf-8 -*- from pyparsing import * ... # описываем грамматику pattern = .... # читаем файл: lines -- список, каждый элемент которого содержит отдельную строку файла lines = file('code.cldata','r').readlines() # проверяем каждую строку на соответствие шаблону for i in lines: # если строка i не соответствует шаблону, то pypаrsing # генерирует исключение, которое здесь и обрабатывается try: result = pattern.parseString(i) print result except: pass
Выбранный вариант будет предопределять содержимое переменной pattern.
Применительно к рассматриваемому примеру, второй вариант будет требовать обязательной предварительной обработки текста -- приведения файла к виду, одна строка -- одна команда. В противном случае будет путаница, поскольку заранее неизвестно в каком именно месте САМ-система поставит разрыв CLData-строки. Для первого способа эта предобработка является необязательной, но желаемой, т.к. значительно упрощает описание pattern.
Таким образом, для дальнейшего рассмотрения примем первый вариант обработки текста.
[править] Описание грамматики
Немного терминологии: понятие "грамматика" — это способ описания формального языка, который в данном случае представлен набором управляющих команд файла code.cldata. Последовательности допустимых символов языка, несущих некоторою смысловую нагрузку, принято называть "лексемами". В свою очередь, лексемы формируются из отдельных групп символов — "токенов".
Таким образом, процесс описания грамматики следует начинать с формирования "кирпичиков".
[править] Синтаксис
Согласно официальной документации, токены можно сформировать с помощью следующих функций.
Literal() | поиск точного совпадения строки (допускается писать строку просто в кавычках -- см. примеры) |
CaselessLiteral() | создаётся искомой строкой, но без проверки регистра; результаты всегда превращаются в определяющий литерал, а не остаются такими, как они записаны во входной строке |
Keyword() | похоже на Literal, но обязательно должен сопровождаться пробельным символом, символом пунктуации или другим не ключевым словом; защищает от неправильного распознавания неключевых слов, которые начинаются ключевым словом |
CaselessKeyword() | аналогично Keyword, только без учёта регистра |
Word() | строка, не содержащая символа пробела и/или табуляции; формируется из букв, цифр и пр. (например, 'Hello', 'user_name', '$a' и т.д.) |
CharsNotIn() | Похоже на Word_, but matches characters not in the given constructor string (accepts only one string for both initial and body characters); also supports min, max, and exact optional parameters. |
Regex() | полноценные конструкции, содержащие регулярные выражения; принимают необязательные параметры флагов аналогичных модулю re; если выражение включает именованные поля, то они будут возвращены в ParseResults |
QuotedString() | определяет различные разделители (в дополнение к dblQuotedString и sglQuotedString) |
SkipTo() | позволяет при поиске пропускать несовпадающие фрагменты (выполняется предпросмотр) |
White() | аналогично Word, но включает проверку с символами пробела (по умолчанию pyparsing игнорирует пробелы, поэтому данная функция может быть полезна при разборе текста, строки которого содержит ведущие символы табуляции/пробелов) |
Empty() | выражение, не содержащее символов — null |
NoMatch() | противоположно Empty |
Функции в качестве аргументов, помимо обычных символов, могут принимать константы или их комбинации:
alphas | строка, состоящая из букв алфавита |
nums | строка, состоящая из цифр |
alphanums | строка, состоящая из букв и цифр |
alphas8bit | строка, состоящая из 8-битных символов |
printables | все печатаемые символы, за исключением пробела (' ') |
restOfLine() | все печатаемые символы, от места обявления до конца строки |
empty | соответствует глобальному Empty() |
sglQuotedString | строка символов, заключенная в кавычки " ' ", может содержать пробелы, но не знак конца строки |
dblQuotedString | ??? (согласно документации, определение аналогично sglQuotedString) |
quotedString | комбинация предыдущих двух: 'sglQuotedString | dblQuotedString' |
cStyleComment() | блок текста (комментариев), помещенный между ' /* ' и ' */ '; может занимать несколько строк, но не поддерживает вложенность комментариев |
htmlComment() | блок текста (комментариев), помещенный между ' <!-- ' и ' --> '; может занимать несколько строк, но не поддерживает вложенность комментариев |
commaSeparatedList() | аналогично delimitedList, однако список выражений может принимать любые значения |
Длина символов или констант ограничивается параметрами:
min | ограничение минимальной длины строки |
max | ограничение максимальной длины строки |
exact | указание точной длины строки |
srange | задание диапазона символов |
Комбинирование функции или константы выполняется с помощью выражений:
And() | логическое И (может быть заменено символом '+') |
Or() | логическое ИЛИ (может быть заменено символом '^') |
MatchFirst() | каждое соответствие фрагмента текста шаблону рассматривается слева направо (может быть заменено символом '|') |
Each() | аналогичен оператору And, с той особенностью, что каждое соответствие фрагмента текста шаблону рассматривается не последовательно, а в произвольном порядке (может быть заменено '&') |
Optional() | поиск фрагмента текста, который может встречаться или не встречаться (например, необязательный параметр функции) |
ZeroOrMore() | фрагмент текста, соответствующий аргумент (шаблону) может встречаться ноль или более раз |
OneOrMore() | фрагмент текста, соответствующий аргумент (шаблону) может встречаться один или более раз |
FollowedBy() | предпросмотр совпадения шаблона (функция всегда возвращает NULL и лишь подтверждает совпадение) |
NotAny() | отрицание выражения заданного шаблоном (может быть заменено '~') |
[править] Примеры составления шаблонов
Word(nums + ".-") | число, определенное как слово, которое может состоять из цифр и/или точки и/или знака минуса (например, "10" или "10.5", или "-10.5") |
Literal("#") + restOfLine | любой текст от символа "#" включительно до символа конца строки (чаще всего применяется для поиска закомментированного текста) |
Word( "xyz", max=1 ) + Literal("=").suppress() | одна из букв x, y или z и стоящий за ней знак равенства (последний в список результатов поиска не будет включен) |
"[" + Word(alphas.upper()) + "]" | любые буквы верхнего регистра, заключенные в скобки, например "[ABC]" (метод upper() и lower() могу применяться к константам, функциям или группам); скобки, заключенные в кавычки, раскрываются как Literal |
Word( srange("[A-N]"), exact=6) | слово, состоящее из шести строчных букв из диапазона A-N, например "DEBIAN" |
Regex(r"(?P<user>[A-Za-z0-9._%+-]+)@(?P<hostname>[A-Za-z0-9.-]+)\.(?P<domain>[A-Za-z]{2,4})") | шаблон e-mail (используются именованные регулярные выражения для выделения составляющих: user, hostname и domain |
Word( nums ) + Optional( Word(alphas), default = 'AAА' ) | слово, состоящее из цифр и букв (например '123ABC'); если исходный текст состоит только из одних цифр, например '123', то будет возвращена строка с значением по умолчанию: " ['123', 'AAA'] " |
[править] Грамматика CLData-файла
Ниже приведен пример описания 3-х функций. В действительности их больше сотни, поэтому написать весь перечень по образцу будет несложно.
#!/usr/bin/python # -*- coding: utf-8 -*- # читаем файл: lines -- список, каждый элемент которого содержит отдельную строку файла lines = file('code.cldata','r').readlines() # объединяем все строки в одну text = ''.join(lines) # удаляем из строки text все последовательности символов разрыва команд: "$\n или $\r\n" text = re.sub(r'\$.?\n', '', text) # описываем числа, разделители и комментарии Number = Word(nums + ".-") slashdot = Literal("/").suppress() comma = Literal(",").suppress() comments = "$$" + restOfLine # описываем численные значения параметров команд x = Number y = Number z = Number i = Number j = Number k = Number f = Number # координата точки формируется с помощью логического И Point = x + comma + y + comma + z # направляющие косинусы вектора нормали также формируются с помощью логического И # (в учебных целях использованая альтернативная форма записи) Vector = And( [i, comma, j, comma, k] ) # шаблон для поиска команды линейной интерполяции # (например, "GOTO / 81.85738, -34.36788, 22.85000" или "GOTO / 81.85738, -34.36788, 22.85000, 0, 0, 1") Linear = Literal("GOTO").suppress() + slashdot + Point + Optional(comma + Vector) # аналогично записываем шаблон поиска команды быстрых перемещений (просто строка со словом "RAPID") Rapid = Literal("RAPID") # шаблон поиска команд задания подачи (например, "FEDRAT/ 1000.0000,MMPM") Feed = Literal("FEDRAT").suppress() + slashdot + f + comma + Word(alphas) # формируем общий шаблон: так как количество и последовательность ранее # определенных "подшаблонов" неизвестны, то объединяем их логическим ИЛИ pattern = Linear ^ Rapid ^ Feed pattern.ignore(Comments)
[править] Анализ результатов
Собственно приведенный выше листинг программы бесполезен, поскольку результатом его работы будет список с постоянно изменяющимся количеством элементов. Идентификация значений этого списка в принципе возможна, но этот процесс будет достаточно утомительным. Поэтому желательно на этапе формирования шаблона присвоить токенам имена.
Для этих целей служить функция setResultsName, которая может быть применена как к отдельным токенам, так и к сгруппированным функцией Group.
#!/usr/bin/python # -*- coding: utf-8 -*- lines = file("code.cldata", 'r').readlines() text = ''.join(lines) text = re.sub(r'\$.?\n', '', text) Number = Word(nums + ".-") slashdot = Literal("/").suppress() comma = Literal(",").suppress() comments = "$$" + restOfLine # описываем именованные токены x = Number.setResultsName("x") y = Number.setResultsName("y") z = Number.setResultsName("z") i = Number.setResultsName("i") j = Number.setResultsName("j") k = Number.setResultsName("k") f = Number.setResultsName("f") Point = x + comma + y + comma + z Vector = And( [i, comma, j, comma, k] ) # создаем группу, присвоив ей имя Linear Linear = Group(Literal("GOTO").suppress() + slashdot + Point + Optional(comma + Vector)).setResultsName("Linear") # альтернативная setResultsName форма записи Rapid = Literal("RAPID")("Rapid") Feed = Group(Literal("FEDRAT").suppress() + slashdot + f + comma + Word( alphas )).setResultsName("Feed") pattern = Feed ^ Rapid ^ Linear pattern.ignore(comments) for tokens in pattern.searchString(text): # если найдена группа Linear, то выводим на печать соответствующие координаты if tokens.Linear: print tokens.Linear.x, tokens.Linear.y, tokens.Linear.z, tokens.Linear.i, tokens.Linear.j, tokens.Linear.k # если найдена группа Rapid, то выводим на печать слово "RAPID" if tokens.Rapid: print "RAPID" # если найдена группа Feed, то выводим на печать значение подачи if tokens.Feed: print "F = " + tokens.Feed.f
Через несколько секунд (к слову сказать, библиотека pyparsing работает достаточно медленно) на экране получим следующий результат:
... RAPID 52.40160 -133.06004 47.65000 RAPID 52.40160 -133.06004 45.85000 F = 100.0000 52.40160 -133.06004 44.85000 53.34099 -132.71719 44.85000 F = 96.0000 81.46980 -34.02249 22.85000 81.85738 -34.36788 22.85000 92.24972 -91.51438 22.85000 F = 1000.0000 35.52745 -127.15614 22.85000 35.52745 -127.15614 22.85000 35.52745 -127.15614 23.85000 35.52745 -127.15614 22.85000 RAPID 35.52745 -127.15614 47.65000 ...
[править] Дополнительная информация
- HowTo Use Pyparsing (англ.)
- Examples (англ.) — Примеры использования pyparsing
- Pyparsing quick reference: A Python text processing tool (англ.)
- Построение парсеров с рекурсивным спуском на Python (рус.)
- Необыкновенно лёгкий парсинг в Python (рус.)
- Рисуем диаграммы реляционной базы (рус.)
- Regexp и Python: извлечение токенов из текста (рус.)
- Pyparsing introduction: BNF to code (англ.) — написание кода PyParsing на основе BNF