Для языка Python создано достаточно большое количество синтаксических анализаторов. В данной статье будет рассмотрен один из них — pyparsing.
Подобно регулярным выражениям, последовательность работы с данной библиотекой содержит три этапа:
Задача формулируется следующим образом. Практически все CAM-системы результат генерирования траекторий перемещения инструмента, а также некоторую технологическую информацию, сохраняют в промежуточный формат, который получил обобщенное название CLData.
В принципе существует его описание The Official Generative Machining ASCII CLDATA Output Specification, однако каждая система в этот формат привносит свои особенности, поэтому CLData нельзя назвать полностью “стандартным”.
Между CLData и конкретной стойкой ЧПУ находится программа, под общим названием “постпроцессор” (применительно к вычислительной технике, эту программу можно было бы назвать драйвером). Постпроцессор должен решать две глобальные задачи (мелочь – пока не в счет):
Решение же данных задач невозможно без разбора исходного CLData-файла. Собственно этим мы и займемся в данной статье.
Исходный файл, с которым мы будем работать дальше, имеет следующий вид:
# Исходный файл code.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)
Отсюда вытекают два возможных варианта:
# Пример обработки текста по первому варианту
from pyparsing import *
...
# описываем грамматику
pattern = ....
# читаем файл: lines -- список, каждый элемент которого содержит отдельную строку файла
lines = file('code.cldata','r').readlines()
# объединяем все строки в одну
text = ''.join(lines)
# выполняем поиск
for tokens in pattern.searchString(text):
print tokens
# Пример обработки текста по второму варианту
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. Последовательности допустимых символов языка, несущих некоторою смысловую нагрузку, принято называть “лексемами”. В свою очередь, лексемы формируются из отдельных групп символов — “токенов”.
Таким образом, процесс описания грамматики следует начинать с формирования “кирпичиков”.
Согласно официальной документации, токены можно сформировать с помощью следующих функций.
Syntax | Description |
---|---|
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 |
Функции в качестве аргументов, помимо обычных символов, могут принимать константы или их комбинации:
Syntax | Description |
---|---|
alphas | строка, состоящая из букв алфавита |
nums | строка, состоящая из цифр |
alphanums | строка, состоящая из букв и цифр |
alphas8bit | строка, состоящая из 8-битных символов |
printables | все печатаемые символы, за исключением пробела ’ ‘ |
restOfLine() | все печатаемые символы, от места обявления до конца строки |
empty | соответствует глобальному Empty() |
sglQuotedString | строка символов, заключенная в кавычки ’, может содержать пробелы, но не знак конца строки |
dblQuotedString | ??? (согласно документации, определение аналогично sglQuotedString) |
quotedString | комбинация предыдущих двух: sglQuotedString / dblQuotedString |
cStyleComment() | блок текста (комментариев), помещенный между /* и */; может занимать несколько строк, но не поддерживает вложенность комментариев |
htmlComment() | блок текста (комментариев), помещенный между <! – и – >; может занимать несколько строк, но не поддерживает вложенность комментариев |
commaSeparatedList() | аналогично delimitedList, однако список выражений может принимать любые значения |
Длина символов или констант ограничивается параметрами:
Syntax | Description |
---|---|
min | ограничение минимальной длины строки |
max | ограничение максимальной длины строки |
exact | указание точной длины строки |
srange | задание диапазона символов |
Комбинирование функции или константы выполняется с помощью выражений:
Syntax | Description |
---|---|
And() | логическое И (может быть заменено символом ‘+’) |
Or() | логическое ИЛИ (может быть заменено символом ‘^’) |
MatchFirst() | каждое соответствие фрагмента текста шаблону рассматривается слева направо (может быть заменено символом ' |
Each() | аналогичен оператору And, с той особенностью, что каждое соответствие фрагмента текста шаблону рассматривается не последовательно, а в произвольном порядке (может быть заменено ‘&’) |
Optional() | поиск фрагмента текста, который может встречаться или не встречаться (например, необязательный параметр функции) |
ZeroOrMore() | фрагмент текста, соответствующий аргумент (шаблону) может встречаться ноль или более раз |
OneOrMore() | фрагмент текста, соответствующий аргумент (шаблону) может встречаться один или более раз |
FollowedBy() | предпросмотр совпадения шаблона (функция всегда возвращает NULL и лишь подтверждает совпадение) |
NotAny() | отрицание выражения заданного шаблоном (может быть заменено ‘~’) |
Syntax | Description |
---|---|
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[A-Za-z0-9._%+-]+)@(?P[A-Za-z0-9.-]+).(?P[A-Za-z]{2,4})") | шаблон e-mail (используются именованные регулярные выражения для выделения составляющих: user, hostname и domain |
Word( nums ) + Optional( Word(alphas), default = ‘AAА’ ) | слово, состоящее из цифр и букв (например ‘123ABC’); если исходный текст состоит только из одних цифр, например ‘123’, то будет возвращена строка с значением по умолчанию: " [‘123’, ‘AAA’] " |
Ниже приведен пример описания 3-х функций. В действительности их больше сотни, поэтому написать весь перечень по образцу будет несложно.
# Файл main.py
# читаем файл: 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.
# Файл main.py
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 работает достаточно медленно) на экране получим следующий результат:
# результат выполнения файла main.py
...
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
...