Python :: PyParsing


Pyparsing — модуль синтаксического анализа для языка Python

Для языка 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’] "

Грамматика CLData-файла

Ниже приведен пример описания 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
...

Дополнительная информация