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