Аналитика, редактирование текста. Почти такие же сложные: CFG, BNF, LL(K), LR(K), PEG и другие страшные слова.
Наверное, каждый разработчик сталкивается с задачей типа "прочитать что-то, выполнить какие-то операции" - JSON, NGINX, CFG, SQL, YAML, CSV и т.д. Было бы хорошо, если бы можно было использовать библиотеки, но по ряду причин это не всегда возможно. Затем возникает проблема создания аналитики для конкретной формы. Как говорят англичане, это часто оказывается pita. В данной статье сделана попытка устранить эту боль. Приглашаются все заинтересованные стороны.
Введение.
Этот вопрос - именно то, о чем эта статья. Здесь я постараюсь помочь. читателю Наименьшая потеря для выхода из ситуации, когда вам нужно проанализировать какой-то текст, но вы не можете воспользоваться библиотекой. То есть, решить конкретный проект полностью общими средствами.
Следует отметить, что сама эта проблема не только очень сложна, но и очень обширна, что делает невозможным охватить все в одной статье. Она начинается с общих принципов и переходит к специализированным областям, а не излагает инструментарий данной статьи (который можно использовать для решения вполне конкретных задач анализа). Возможно, если читатели если они заинтересованы, то эта статья может быть преобразована в серию, в которой будут проанализированы некоторые конкретные темы.
Я решил написать ее, потому что вся имеющаяся информация по этому вопросу разрозненна и часто неполна, а русских источников мало, но имеющиеся источники показывают довольно приличное знакомство. читателя Они оснащены очень специфической математикой. Таким образом, чтобы избежать мук познания читатель Ему не больно от ощущения своей (предполагаемой) неполноценности. Я решил попытаться объяснить эту тему самым простым способом.
Подождите, это большая статья, и некоторые моменты не так интересны, но без них вы можете не понять.
Основные понятия
Прежде чем говорить об этом, стоит определить некоторые основные понятия, чтобы не возникло путаницы. Это глоссарий для данной статьи. Она может совпадать с общепринятой терминологией, но обычно это не обязательно, поскольку показывает картину, сложившуюся в сознании автора. Хорошо:.
Входной символ (далее входной ток или ток) - поток символов, подлежащих анализу, подпитываемый входными данными аналитика
Парсер - программа, которая принимает входной поток и преобразует его в AST и/или позволяет привязать исполняемые функции к элементам грамматики.
AST (Abstract Syntax Tree) (структура выходных данных) - объектная структура, представляющая иерархию нетерминальных сущностей в разобранном потоке грамматики и составляющие их термины. Например, алгебраический поток (1 + 2) + 3 можно представить в виде ВЫЧИСЛИТЬ(ВЫЧИСЛИТЬ(ЧИСЛО(1) ЧИСЛО(+) ЧИСЛО(2) ЧИСЛО(+) ЧИСЛО(3)). Как правило, это дерево обрабатывается следующим образом. Клиент парсера выдает результат каким-либо способом (например, измеряет ответ определенного выражения).
CFG (Context Free Grammar) - наиболее часто используемый тип грамматики. для описания Входящий поток символов парсера (не только его, конечно). Он характеризуется тем, что использование правил не зависит от контекста (это не исключает придания контекста в той или иной форме, например, правило для вызова функции не зависит от контекста). Например, правило для вызова функции не имеет значения, находится ли она в пределах секции потока. описывается правилом аннотации). Он состоит из производственных правил, определенных для терминальных и нетерминальных символов.
Терминал - для некоторых языков синтаксического анализа, набор всех (отдельных) символов, которые появляются во входном потоке.
Нетерминальные символы - набор всех символов, которые не могут быть обнаружены во входном потоке, но участвуют в грамматических правилах конкретного языка синтаксического анализа.
Язык синтаксического анализа (в большинстве случаев это CLL ("контекстно-свободный язык")) - набор всех терминальных и нетерминальных символов, а также CRC конкретного входного потока; например, в естественных языках терминальные символы - это все символы, цифры и знаки препинания, используемые в языке, а нетерминальные символы - это слова и предложения. (и другие структуры, такие как субъекты, предикаты, глаголы и наречия), а грамматика - это грамматика самого языка.
BNF (backus-nur form, backus normal form)/ bnf (backus-nur form) - это форма, в которой некоторые редакционные категории определяются последовательно через другие редакционные формы представления CRC. Это часто используется непосредственно для определения входа аналитика. Он характеризуется тем, что идентифицируемые символы всегда находятся в нетерминальной форме. Классической формой символизма является:: = |||||. ||ABNF (AugmentedBNF), EBNF (ExtendedBNF) и многие другие разновидности. В целом, эти формы несколько расширяют синтаксис обычных BNF-вкладов.
LL(K), LR(K), SLR. - Типы алгоритмов анализа. В данной статье они подробно не рассматриваются. Если вы заинтересовались, ниже приведены ссылки на материалы, где вы можете узнать о них. Однако давайте остановимся на другом аспекте - грамматике аналитиков: грамматика LL/LR-аналитиков - это BNF, это правда. Но также верно, что не существует грамматики BNF LL(K) или LR(K). Действительно, что означает буква K для LL/LR (k)? Это означает, что для анализа грамматики конечная буква по нити должна сначала посмотреть на k. Поэтому для анализа (0) необходимо знать только текущую букву. (1) - необходимо знать текущий и один следующий символ. (2) - Давайте поговорим немного больше о выборе/создании BNF некоторых аналитиков ниже, включая текущий и два следующих символа.
PEG (Parsing Expression Grammar) - это грамматика, предназначенная для распознавания строк на одном языке. Примеры этой грамматики алгебраического поведения +, -, *, / / неотрицательных чисел: [0-9] + / '(' expr ')' '' продукт ← цена ('*' / '' / '' / '' (' expr ')' '' цена) * sum← продукт ('+' / ' - ') продукт) * expr← sum
Разновидности анализа
Когда перед вами стоит задача создания аналитики, решение обычно сводится к четырем основным вариантам.
Решить проблему в лоб, т.е. проанализировать входящий поток символ за символом и создать ASD, используя правила грамматики, или сразу выполнить необходимые операции над требуемыми компонентами. Плюс в том, что этот вариант самый простой с точки зрения алгоритмов и математики. Недостаток - нет формальных критериев, позволяющих определить, все ли правила грамматики были учтены при построении синтаксического анализатора, поэтому вероятность случайных ошибок практически максимальна. Это отнимает много времени. В целом, изменения не являются простыми или гибкими, особенно если сборка ASD не реализована. Невозможно запустить анализатор на длительное время, чтобы убедиться, что он работает полностью правильно. Плюсы и минусы. В этой вариации все зависит от постановки рук. Детали этой вариации не объясняются.
Давайте использовать регулярные выражения! Я не хочу шутить по поводу количества проблем или регулярных выражений, но в целом этот метод, хотя и доступен, не очень хорош. Для сложных грамматик вы можете попасть в ад с регулярными выражениями, особенно если попытаетесь оптимизировать правила, чтобы сделать их быстрее. Удачи, если вы выберете этот путь. Регулярные выражения не предназначены для разбора! В противном случае не позволяйте никому пытаться убедить вас в обратном. Они предназначены для поиска и замены. Любая попытка использовать их для чего-то другого неизбежно окажется напрасной. Их использование либо значительно замедлит ваш анализ, если вы будете слишком часто пересекать линию, либо вы потеряете клетки мозга, пытаясь понять, как удалить гланды из ануса. Возможно, попытка скрестить этот метод с предыдущим немного улучшит ситуацию. Может и нет. В целом, преимущества во многом совпадают с предыдущим вариантом. Однако необходимо знание регулярных выражений. Желательно не только знать, как использовать регулярные выражения, но и как быстро работают используемые вами опции. Недостатки почти те же, что и у предыдущей версии, но менее трудоемкие.
Множество инструментов разрешения BNF для использования! Это более интересно. Во-первых, существуют такие варианты, как lex-yacc и flex-bison, а во-вторых, во многих языках можно найти библиотеки, специфичные для разрешения БНФ. Ключевыми словами для поиска являются LL, LR и BNF. Дело в том, что все получают на вход некий вариант BNF, в то время как LL, LR, SLR и т.д. являются специфическими алгоритмами, используемыми аналитиками. В большинстве случаев конечного пользователя не особенно интересует, какой алгоритм используется, но существуют некоторые ограничения на грамматический анализ (больше относительно меньше) и время выполнения может варьироваться (большинство утверждает, что o(l), в то время как L есть L. (длина потока символов). Хорошо, что есть стабильный инструментарий, понятная форма символики (BNF), достаточные оценки времени выполнения и записи BNF для большинства современных языков (вы можете найти записи для SQL, Python, JSON, CFG, YAML, HTML, CSV и многих других). людей по мере необходимости). Среди недостатков - не всегда очевидный и удобный интерфейс. Возможно, вам придется написать что-то на незнакомом языке.
Используйте инструмент PEG-анализа! Существуют также интересные альтернативы и даже более богатые библиотеки, но они, как правило, относятся к другой эпохе (PEG был предложен Брайаном Фордом в 2004 году, а BNF уходит корнями в 1980-е годы). Тем не менее, они довольно молоды, не очень хорошо сохраняются и в основном находятся на GitHub. Хорошо то, что это быстро, просто и часто присуще. Недостатком является то, что она в значительной степени зависит от приложения; пессимистическая оценка спецификации PEG, похоже, составляет O(exp(l)) (другое дело, что для создания такой грамматики нужно потрудиться). Она сильно зависит от наличия/отсутствия библиотек. По какой-то причине многие библиотеки PEG считают достаточными функции маркировки и поиска/обмена, даже при наличии AST-обязательств и возможностей для элементов грамматики. В целом, однако, проблема весьма перспективна.
Решение аналитических задач.
Пропустите варианты перебора и regexp и переходите по одному.
БНФ
Вот и настало время чуть подробнее остановиться на алгоритмах разборщика, вернее на используемых ими грамматиках. Итак, наиболее часто встречаются GLR (до O(L^3) или до O(L^4), как говорят некоторые источники (antlr)), LR, LALR и LL — все в пределах O(L). Наибольшей “прожорливостью” обладает GLR — она способна лучше обрабатывать неоднозначности грамматики, но за счет этого и более медленна. То есть если рассматривать “размер” класса обрабатываемых парсером грамматик(назовем его мощностью парсера), то при прочих равных, мощности распределятся так GLR > LR > LALR > SLR >ll. соответственно, потребление ресурсов почти противоположное. Но вернемся к грамматике.
Обычно очень легко написать или найти грамматику для синтаксического анализатора LR, и весьма вероятно, что БНФ будет принята и обработана синтаксическим анализатором "на лету". Главное, чтобы грамматика была полной, т.е. описывала все возможные состояния входного потока. Кроме того, если вы знаете k последующих символов (в зависимости от выбранного разделителя LR), постарайтесь с первого взгляда понять, можно ли явно определить, какие правила применяются.
В случае парсеров LR могут возникнуть следующие конфликты
Перенос-свертка (shift-reduce): NT ::= x NT | x . Где длина x >k. Это разрешает: NT ::= xK- K ::= K |e
Свертка-свертка (reduce-reduce): NT :: = e A ::= NT B ::= NT D ::= B u v | A u w , где длина u >R ::= Au J ::= Bu D ::= Rw | k. Это решает следующее: jv
Левая рекурсия: NT ::= NT x |y , где x, y могут быть любой конечной/неконечной строкой, но y не начинается с NT
Пример: E ::= E + T |T. Можно перефразировать как
E ::= T Z Z ::= '+' TZ |TZ.
Левая факторизация: NT ::= ax | ay , где a — строка длины >k нашего аналитика. еще проще: NT ::= aC; C = x|y
Так что же нужно решить?
Допустим, это простой калькулятор.
OPERATOR ::= "+ | "-" | "*" | "/" STMT ::= "(" STMT ")" | STMT OPERATOR STMT | FLOAT | INT FLOAT ::= INT "". INT INT INT :: = (POSITIVE_DIGIT INT | DIGIT ) | DIGIT POSITIVE_DIGIT ::= "1"|"2 |"3 |"4 |"5 |"6 |"7 |"8 |"9" DIGIT ::= POSITIVE_DIGIT |"0"
Если читатель Поиск грамматик калькуляторов в Интернете показывает, что сложение/вычитание и умножение/деление часто обрабатываются с помощью разных грамматических структур. Это сделано намеренно, чтобы проиллюстрировать (и прояснить грамматические неясности), например, старшинство глаголов в грамматике. Это будет сделано далее в статье.
При попытке найти решение на базе Python можно найти множество решений.
Парглар. Это библиотека/cli инструмент Python, реализующий парсер LR/GLR с очень широким спектром возможностей (встроенные функции, иерархии, деревья, прямой грамматический анализ, визуализация CA через обработку грамматики).
pip installation parglare
Переформатируйте компьютер в соответствии с требованиями parglare
Операторы: "+"|"-" |"*" |"/" |= STMT : "(" STMT ")" |STMT operator STMT |Float |INT FLOAT : INT "". INT INT : (POSITIVE_DIGIT INT |DIGIT) |DIGIT POSITIVE_DIGIT : "1"|"2 |"3 |"4 |"5 |"6 |"7 |"8 |"9" DIGIT : POSITIVE_DIGIT |"0"
Достаточно ли этого? Сохраните его в calc. pg и используйте cli-tool.
pglr --debug check calc.pg Error in file "/home/survivor/tests/calc.pg" at position 1,42 =>"" |"/" |"*=
STMT ". Assumed: Name или StrTerm или RegExTerm
Упс! Выглядит как дополнительный. Бинго!!! Я поставлю его|= он почему-то стоит после '/' (нет, я знаю, почему он там стоит (но это не имеет отношения к теме статьи)). Главное, что наша программа это наглядно продемонстрировала. После исправления программа снова жалуется на его отсутствие? нетерминальные символы и заключающие круглые скобки в правиле INT. Отредактированная версия будет выглядеть следующим образом
STMT : "(" STMT ")" |STMT operator STMT |FLOAT |INT? operator: "+"|"-" |"*"|"/"? FLOAT : INT "." INT? INT : POSITIVE_DIGIT INT |POSITIVE_DIGIT Число |Digit?POSITIVE_DIGIT : "1"|"2 |"3 |"4 |"5 |"6 |"7 |"8 |"9"; DIGIT : POSITIVE_DIGIT |"0",
Наконец, pglr нравится все это и говорит
Грамматика в порядке!
Существует четыре конфликта Shift/Reduce.' Используйте функцию анализа 'prefer_shifts' или попытайтесь решить ее вручную, или используйте GLR-анализ Имеется семь конфликтов Reduce/Reduce. Используйте функцию анализа 'prefer_shifts' или решайте вручную, или используйте GLR-анализ. описание Теперь, в отладочном выводе выше, вы можете прочитать приятное (и ясное)
. Теперь давайте подумаем. Прежде всего, не надо умничать и отбрасывать положительные цифры. Если кто-то пишет 0034, то, во-первых, он злой идиот, потому что не является пользователем Python, а во-вторых, он не является пользователем Python. Во-вторых, большинство языков, включая Python, ничего не скажут вам при преобразовании в число, они просто вернут 34. Во-вторых, удалите деление int/float из этого. Для простоты мы предполагаем, что все числа являются числами с плавающей запятой. Кроме того, pglr понимает регулярные выражения BNF, поэтому давайте использовать их. Вот следующие.
STMT : "(" STMT ")"|STMT operator STMT |float ? Оператор: "+"|"-"|"*"|"/"? FLOAT : /\d+(\. \d+)? /,
Существует четыре конфликта Shift/Reduce.' Используйте функцию анализа 'prefer_shifts', решайте вручную или используйте GLR-анализ.
В любом случае, грамматика такова.
*** Грамматика *** Терминал: + EOF ) ( FLOAT STOP * / - \d+(\. \d+)? EMPTY Нетерминал: OPERATOR S' STMT: 0: S' = STMT STOP 1: STMT = STMT OPERATOR STMT 2: STMT = ( STMT ) 3: STMT = FLOAT 4: operator = + 5: operator = - 6: operator = * 7: operator = /
Давайте проанализируем кое-что.
from parglare import Grammar from parglare import Parser grammar = Grammar. from_file('calc. pg') parser = Parser(grammar, build_tree=True, prefer_ shifts=True) result = parser. parse('1 + 2 / (3 - 1 + 5)')
Результат.
Результаты доступны по адресу. Под елкой с ребенком. Мы также можем вычислить конечное выражение, но здесь мы этого делать не будем. Важно то, что мы имеем доступ к дереву объектов, терминальному символу и "значению", и можем делать с ними все, что захотим.
Если вы хотите устранить конфликт, как еще можно разрешить грамматический конфликт?
Существует несколько вариантов:.
Например, вы можете сбросить грамматику, чтобы решить проблему
stmt: term|stmt audop term? термин: factor|factor marop factor? Фактор: "(" stmt ")" | число?ADDOP: "+" | "-"?mulop: "*" | "/"; число: / кнопка
В этом случае решение более частное, но в некоторых случаях более удобное; Parglare предоставляет отличный набор инструментов для иерархий правил. Например, в случае с арифметическими выражениями можно определить приоритет действий и соотношения (с какой стороны выполняется действие - слева направо или справа налево), чем выше приоритет и чем раньше выполняется действие (по сравнению с другими), тем быстрее выполняется действие. Например, наша грамматика этой нотации может выглядеть следующим образом
stmt: stmt audop stmt |stmt mulop stmt |"(" stmt ")" число; ADDOP: "+" |" - "? mulop: "*" | "/"; число: / кнопка
Проанализировано, но не функционирует должным образом. Например.
У нас есть (кроме неорешений).
['1', u '+', ['2', u '/', [u '(', ['3', u'- ', ['1', u'+', ' 5 '], u ')].
Это явно не соответствует ожидаемому результату.
['1', u '+', ['2', u '/', [u '(', ['3', u'- ', ' 1 '], u'+', ' 5 ']], u ')']].
По мнению автора этой ошибки, это происходит потому, что синтаксис аналитика на алгоритмическом уровне (слева) отдает предпочтение редукции, а не смещению. Поэтому на самом деле лучше "сократить" правило или продолжать его, если есть возможность. Поскольку анализ проводится слева направо, это подразумевает левые ассоциации. Причина ошибки в том, что правила внутри Addop/Multi не являются иерархическими, что разрушает все правило.
Однако даже если вы установите приоритеты (например.
ADDOP: "+" | "-") по-прежнему не работает, но уже из-за другой ошибки.
from parglare import Parser, Grammar grammar = r""" E: E '+' E | E '-' E | E '*' E | E '/' E | E '^' E | '(' E ')' | number; number: /\d+(\.\d+)?/; """ # Define inline functions bound to grammar rule actions = g = Grammar.from_string(grammar) parser = Parser(g, debug=True, actions=actions) result = parser.parse("34 + 4.6 / 2 * 4^2^2 + 78") print("Result bash">Наконец, пример встроенной функции, напрямую связанной с правилом, из документации Parglare.
$ cd/usr/local/lib $ cur l-o http: //www. antlr. org/download/antlr-4. 7. 1-Complete. jar $ classpath = ".". : /usr/local/lib/antlr- 4. 7. 1-complete. jar: $ classpath "$ alias antlr4 = 'jav a-xmx500 m-cp"/usr/local/lib/antlr-4. 7. 1-complete. jar: $ classpath "org. antlr. v4. tool' $ alias gun = 'java org. antlr. v4. gui. testrig '
Для работы с Python2 вам необходимо установить
PIP install ANTLR4-PYTHON2-RUNTIME
Теперь давайте попробуем сделать грамматику по этому поводу. Поскольку у нас немного другая форма, давайте заменим двойные кавычки на одинарные, перепишем обычное выражение и добавим нотацию WS, определенную в предыдущем инструменте по умолчанию. Левое заднее колебание в нашем случае может быть просто устранено правым знаком.
grammar calc; stmt : term | term addop stmt ; term : factor | factor mulop factor ; factor : '(' stmt ')' | NUMBER; addop : '+' | '-'; mulop : '*'|'/'; NUMBER: [0-9]+|[0-9]+.[0-9]+; WS : [ \t\r\n]+ ->Важный момент! ANTLR имеет аналитику и правила грамматики. Это правила анализа, которые ведут к узлам результирующего AST. Кроме того, существует некоторое различие в том, что присутствует/не присутствует в правилах грамматики/производства. В частности, в правилах анализа нет нормальных выражений. Аналитик также должен получить правило входа, которое является первым нетерминалом (в общем случае все немного сложнее, но в нашем случае этого достаточно). В целом, стоит отметить, что ANTLR имеет довольно сильный синтаксис, поддерживающий встроенные функции (хотя и в несколько иной форме), неестественные нетерминалы и т.д. В результате наш синтаксис выглядит следующим образом
Скип,.
Файл называется calc.g4 (это важно! Имя файла должно совпадать с именем грамматики, которую он содержит).
antlr4 calc. g4 javac calc*. java grun calc stm t-gui
Затем приведите ему пример (например, 1 + 2 / (3-1 + 5)) и нажмите кнопку онлайн (ctrl-d в *nix, ctrl-z в windows), чтобы увидеть результат. Представлено красивое дерево анализа. Это также интерактивно. Вы можете видеть, крутить и думать узлы и экспортировать их в виде изображений.
antlr 4-dlanguage = python2 calc. g4
Затем вы можете поместить слушателя в грамматику и наслаждаться.<>Импорт из ANTLR4 import from calc_baklexer calc_baklistener from calc_bakxelser calc_bakliser calc_baklisner<>self. result = 0 def exitstmt (self, ctx): print ("oh, a stmt!")
". format (ctx. getText()) def main (argv): input = fileStream (argv [1]) print (input) lexer = calc_baklexer (input) stream = commontokenstream (lexer) parser = calc_bakparser (stream) tree = parser . stmt () printer = stmtprinter () walker = parsetreewalker () walker. walk (printer, tree) if __name__ == '__main__': main (sys. argv)
... Наслаждайтесь программой, которая не функционирует должным образом. Скорее, правильно, но неожиданно.
python . /calc_antlr_min. py . /пример. antlr 1 + 2/(3-1 + 5) ah, stmt!5 ah, stmt!1+5 ah, stmt!3-1+5 ah, stmt!2/(3-1+5) ah, stmt!1+2/(3-1+5)
Как видите, ассоциация здесь "правильная". А поведение аддитивного исключения и мультипликативного деления является левосторонним. Я честно пытался несколько дней (sic!) найти решение (конечно, у меня не всегда получалось, но в общей сложности убил 12-15 часов). Совместные показатели, учебники и переработка грамматики: .... Это не сработало. Я убежден, что решение есть. Кроме того, вот пример компьютерной грамматики. Однако, если это возможно, я хотел найти решение в терминах общей грамматики. В конце концов, целью было не решение проблемы, а ее исследование.
И я признаю свою неудачу. Да, проблема решена. Но при поиске общей проблемы, используя только документацию, я не смог ее решить. Причина... Во-первых, помимо книг по ANTLR, источники, доступные в Интернете, не очень подробны и выразительны. Во-вторых, в интернете есть много материала по различным (несовместимым) версиям ANTLR. Нет, я все понимаю, рост и т.д. Но в каком-то смысле я был настолько знаком с этим инструментом, что мне показалось, что автор даже не слышал о концепции задней совместимости. В общем, это печально.
Обновление.
Как вы правильно заметили, голова - это хорошо, но две - лучше. Перестроить грамматику с правой ссылки на левую можно буквально "одним щелчком пальца" (пожалуйста, предоставьте информацию valentin nechayev netCh80) - нужно просто заменить
stmt: term|term adp stmt,.
stmt: term|stmt audop term,.
В этом случае ANTLR снова обработает левую часть без проблем (возможно, благодаря оптимизации). Простой выход слушателя в этом случае
python . /calc_antlr_min. py . /пример. antlr 1 + 2/(3-1 + 5) ah, stmt!1 ah, stmt!3 ah, stmt!3-1 ah, stmt!3-1+5 ah, stmt!1+2/(3-1+5)
Пег. , читаемые По сути, это упрощенная форма БНФ, но программистам не нужно знать ее в целом; в отличие от БНФ, она изначально похожа на регулярное выражение. Фактически, можно сказать, что "в принципе" регулярные выражения могут использоваться в БНФ (надеюсь, не только в пределах нетерминальных строк, но и в некоторой степени в самом выражении (PEG поддерживает структуры типа A). = B ( X C)* или, например, Z = R (K)?
(например, "A состоит из B и любого количества последовательных повторений X и C", или "Z состоит из P с последующим нулевым или одним повторением K"). Однако, на мой взгляд, это еще не главный вопрос. Дело в том, что PEG изначально разрабатывался как грамматика синтаксического анализатора. И с какими основными проблемами сталкиваются синтаксические анализаторы, включая BNF? Неоднозначный выбор! Для решения этой проблемы в PEG есть отличное окончание или оператор "/". В отличие от оператора "|", который представляет эквивалентный выбор, "/" четко указывает порядок, в котором решаются правила. Например, A / B / C / D указывает, что если не совпадает, то сравнивается с A, если не совпадает, то сравнивается с B, сравнивается с C и т.д. По этой причине следует соблюдать осторожность при использовании синтаксиса PEG. Также рекомендация - если вам не важен порядок, в котором выполняются сравнения, рекомендуется писать '/'. Это уменьшает количество двусмысленностей аналитиков. Однако, как и с любым другим инструментом, с ним следует обращаться осторожно. Также обратите внимание: разработчики PEG-парсеров еще более склонны понимать стандарт по своему усмотрению, поэтому словарный запас может существенно отличаться от реализации к реализации.
Теперь приступим к выполнению упражнения.
Давайте попробуем использовать арпеджио от авторов parglare :
pip install Arpeggio
Далее, давайте посмотрим в документации, что рекомендуемым шаблоном для манипулирования AST в этой библиотеке является Visitor. Это очень похоже на рекомендуемый слушатель для ANTLR. К счастью, весь эксперимент занял час и был не очень сложным.
from arpeggio. cleanpeg import ParserPEG from arpeggio import PTNodeVisitor, EOF, visit_parse_tree # Простая настройка грамматики calc_grammar = """ number = r'\d+(\. \d+)?"" factor = number / "(" stmt ")" term = factor (mulop factor)* stmt = term (addop term)* addop = "+" / "-" mulop = "*" / "/" calc = stmt EOF " " # Создаем класс посетителя для вычисления результата do class CalcVisitor(PTNodeVisitor): def visit_number(self, node, children): return float(node. value) def visit_factor(self, node, children): # children - это список структурного типа с результатами посещения узла VISITED # посещения осуществляются с глубины вверх return children[0] def visit_term(self, node, children): # такие правила включают, например, следующее children["factor"]. Обратите внимание, что children["factor"] - это список всех # элементов термина if len(children) == 1: return children[0] else: res = children[0] for i in xrange(len(children ) / 2): if children[2 * i + 1] == '*': res *= children[2 * (i + 1)] else: res /= children[2 * (i + 1)] return res def visit_stmt (self, node, children): if len(children) == 1: return children[0] else: res = children[0] for i in xrange(len(children) / 2): if children[2 * i + 1] == '+': res += children [2 * (i + 1)] else: res -= children[2 * (i + 1)] return res # Запомните корневое правило парсера, поскольку оно генерируется следующим образом # результат разбора parser = ParserPEG( calc_grammar, "calc") input_expr = "(4-1)*5+(2+4. 67)+5. 89 /(1. 2+7)" print(input_expr) parse_tree = parser. parse(input_expr) result = visit_parse_tree(parse_tree, CalcVisitor ( # debug=True )) print(result) input_expr = "1 + 2 / (3 - 1 + 5)" print(input_expr) parse_tree = parser. parse(input_expr) result = visit_parse_tree(parse_tree, CalcVisitor( # debug= True )) print (result)
При запуске печатается следующий вывод
python . /calc_arpeggio. py (4-1)*5+(2+4. 67)+5. 89/(1. 2+7) 22. 3882926829 1 + 2 / (3 - 1 + 5) 1. 28571428571
Если вы хотите увидеть, как посетители обходят дерево, вы можете отменить отладку
Как вы видите, в синтаксис были внесены небольшие, но важные изменения. В частности, если вы посмотрите на правила stmt и term, то увидите, что существует произвольное количество элементов. Поэтому мы сами решаем, что делать со связностью математических операций. Несмотря на эти изменения, программа остается простой и понятной.
Общие выводы
На самом деле, можно сделать несколько выводов.
Дьявол не так плох, как вы думаете. Обычно можно создать синтаксический анализатор с помощью инструментов. Достаточно изучить общие принципы и потратить полдня на изучение конкретного инструмента. Тогда все становится намного проще. Однако нет необходимости изобретать колесо заново. Особенно когда скорость анализа и оптимизации не так важна.
Грамматика имеет свою ценность. Если перед вами грамматика, то синтаксический анализатор, написанный в соответствии с ней, значительно облегчает оценку ошибок.
Инструменты всегда можно найти. Возможно, это не тот язык, с которым вы знакомы, но он есть у большинства людей. Если вам не повезло и у вас нет такой программы, вы можете приобрести ту, которая проста в использовании (js, python, lua или что-то на языке ruby - все, что вам больше нравится). Да, он будет "почти автономным в проекте", но в большинстве случаев этого достаточно.
Все инструменты (немного) отличаются - в BNF это иногда ":" вместо "=", а иногда различия более обширны. Нет причин пугаться этого. Чтобы изменить грамматику для другого инструмента, требуется не более 20 минут, поэтому если вы можете взять грамматику откуда-то, это лучше, чем писать ее самому. Однако в любом случае рекомендуется проверить его перед использованием. Мы все люди и совершаем ошибки...
При прочих равных условиях рекомендуется использовать более "интерактивный" инструмент. Это поможет вам избежать грамматических ошибок и понять, что и как происходит.
Если скорость анализа важна в первую очередь, вам, вероятно, придется использовать инструменты на языке Си (например, Bison) или решать проблему в лоб. Также стоит рассмотреть вопрос о необходимости специального разрешения (которое стоит рассмотреть в любом случае, но особенно в случае ограничений скорости). Токенизация, т.е. разбиение строки на подстроки с помощью определенного разделителя или набора разделителей, особенно подходит для многих задач. Возможно, это относится и к вам.
Заключение.
В заключение я хотел бы сказать, что это было интересное исследование. Я постарался сделать свои выводы как можно более простыми и понятными. Я надеюсь, что мне удалось написать эту статью так, что даже разработчики, далекие от математики, смогут понять тему, хотя бы в общих чертах, достаточно, чтобы использовать существующие инструменты.
Если детали актуальны для многих людей, может быть полезно написать другие статьи.
Как и было обещано, несколько полезных ссылок по теме:
Википедия, на английском языке (на русском языке статей значительно меньше): cfg bnf ll lr peg
Если требуется более серьезное чтение для тех, у кого "нет математического образования", я могу отослать вас к переведенным и отредактированным книгам А. Ахо и Й. Ульмана. Например, их можно найти здесь. При необходимости вы можете легко найти их в русском переводе на сайте. описание .! ГАЙД # eveoline
Ева онлайн для начинающих, регистрация. Пути образовательной миссии. Читать. ив онлайн Дорогие друзья 1 миллион SP ссылка доступна только на YouTube! Первый видеогайд игры. онлайн Помогает новичкам освоиться в игре. Поддержите канал: https: /www. donationalerts. com/r/serg. _________________________________________________ Всем добра / космические игры. См.
Сергей Вереск #eveonline #eveonline
Внешний вид.
Комментарии