Советы по анализу пользовательского формата файла

Извините за расплывчатое название, но я действительно не знаю, как кратко описать эту проблему.

Я создал (более или менее) простой язык для предметной области, который я использовать, чтобы указать, какие правила проверки должны применяться к различным объектам (обычно формы, отправленные с веб-страницы). Внизу этого поста я включил образец того, как выглядит язык.

Моя проблема в том, что я понятия не имею, как начать синтаксический анализ этого языка в форме, которую я могу использовать (я буду использовать Python для синтаксического анализа). Моя цель — получить список правил/фильтров (в виде строк, включая аргументы, например, 'cocoa(99)'), которые следует применять (по порядку) к каждому объекту/сущности (также к строке, например, 'chocolate', 'chocolate.lindt' и т. д.).

Я не уверен, какую технику использовать для начала, или даже какие методы существуют для таких проблем. Как вы думаете, что является лучшим способом сделать это? Я не ищу полного решения, просто общий толчок в правильном направлении.

Спасибо.

Пример файла языка:

# Comments start with the '#' character and last until the end of the line
# Indentation is significant (as in Python)


constant NINETY_NINE = 99       # Defines the constant `NINETY_NINE` to have the value `99`


*:      # Applies to all data
    isYummy             # Everything must be yummy

chocolate:              # To validate, say `validate("chocolate", object)`
    sweet               # chocolate must be sweet (but not necessarily chocolate.*)

    lindt:              # To validate, say `validate("chocolate.lindt", object)`
        tasty           # Applies only to chocolate.lindt (and not to chocolate.lindt.dark, for e.g.)

        *:              # Applies to all data under chocolate.lindt
            smooth      # Could also be written smooth()
            creamy(1)   # Level 1 creamy
        dark:           # dark has no special validation rules
            extraDark:
                melt            # Filter that modifies the object being examined
                c:bitter        # Must be bitter, but only validated on client
                s:cocoa(NINETY_NINE)    # Must contain 99% cocoa, but only validated on server. Note constant
        milk:
            creamy(2)   # Level 2 creamy, overrides creamy(1) of chocolate.lindt.* for chocolate.lindt.milk
            creamy(3)   # Overrides creamy(2) of previous line (all but the last specification of a given rule are ignored)



ruleset food:       # To define a chunk of validation rules that can be expanded from the placeholder `food` (think macro)
    caloriesWithin(10, 2000)        # Unlimited parameters allowed
    edible
    leftovers:      # Nested rules allowed in rulesets
        stale

# Rulesets may be nested and/or include other rulesets in their definition



chocolate:              # Previously defined groups can be re-opened and expanded later
    ferrero:
        hasHazelnut



cake:
    tasty               # Same rule used for different data (see chocolate.lindt)
    isLie
    ruleset food        # Substitutes with rules defined for food; cake.leftovers must now be stale


pasta:
    ruleset food        # pasta.leftovers must also be stale




# Sample use (in JavaScript):

# var choc = {
#   lindt: {
#       cocoa: {
#           percent: 67,
#           mass:    '27g'
#       }
#   }
#   // Objects/groups that are ommitted (e.g. ferrro in this example) are not validated and raise no errors
#   // Objects that are not defined in the validation rules do not raise any errors (e.g. cocoa in this example)
# };
# validate('chocolate', choc);

# `validate` called isYummy(choc), sweet(choc), isYummy(choc.lindt), smooth(choc.lindt), creamy(choc.lindt, 1), and tasty(choc.lindt) in that order
# `validate` returned an array of any validation errors that were found

# Order of rule validation for objects:
# The current object is initially the object passed in to the validation function (second argument).
# The entry point in the rule group hierarchy is given by the first argument to the validation function.
# 1. First all rules that apply to all objects (defined using '*') are applied to the current object,
#    starting with the most global rules and ending with the most local ones.
# 2. Then all specific rules for the current object are applied.
# 3. Then a depth-first traversal of the current object is done, repeating steps 1 and 2 with each object found as the current object
# When two rules have equal priority, they are applied in the order they were defined in the file.



# No need to end on blank line

person Cameron    schedule 10.01.2010    source источник


Ответы (7)


Во-первых, если вы хотите научиться синтаксическому анализу, напишите свой собственный синтаксический анализатор рекурсивного спуска. Язык, который вы определили, требует лишь нескольких производств. Я предлагаю использовать библиотеку Python tokenize, чтобы избавить себя от скучной задачи преобразования потока байтов в поток токенов.

Для практических вариантов разбора читайте дальше...

Быстрое и грязное решение - использовать сам python:

NINETY_NINE = 99       # Defines the constant `NINETY_NINE` to have the value `99`

rules = {
  '*': {     # Applies to all data
    'isYummy': {},      # Everything must be yummy

    'chocolate': {        # To validate, say `validate("chocolate", object)`
      'sweet': {},        # chocolate must be sweet (but not necessarily chocolate.*)

      'lindt': {          # To validate, say `validate("chocolate.lindt", object)`
        'tasty':{}        # Applies only to chocolate.lindt (and not to chocolate.lindt.dark, for e.g.)

        '*': {            # Applies to all data under chocolate.lindt
          'smooth': {}  # Could also be written smooth()
          'creamy': 1   # Level 1 creamy
        },
# ...
    }
  }
}

Есть несколько способов реализовать этот трюк, например, вот более чистый (хотя и несколько необычный) подход с использованием классов:

class _:
    class isYummy: pass

    class chocolate:
        class sweet: pass

        class lindt:
            class tasty: pass

            class _:
                class smooth: pass
                class creamy: level = 1
# ...

В качестве промежуточного шага к полному синтаксическому анализатору вы можете использовать синтаксический анализатор Python с включенными батареями, который анализирует синтаксис Python и возвращает AST. AST очень глубокий с большим количеством (IMO) ненужных уровней. Вы можете отфильтровать их до гораздо более простой структуры, отбрасывая любые узлы, у которых есть только один дочерний элемент. При таком подходе вы можете сделать что-то вроде этого:

import parser, token, symbol, pprint

_map = dict(token.tok_name.items() + symbol.sym_name.items())

def clean_ast(ast):
    if not isinstance(ast, list):
        return ast
    elif len(ast) == 2: # Elide single-child nodes.
        return clean_ast(ast[1])
    else:
        return [_map[ast[0]]] + [clean_ast(a) for a in ast[1:]]

ast = parser.expr('''{

'*': {     # Applies to all data
  isYummy: _,    # Everything must be yummy

  chocolate: {        # To validate, say `validate("chocolate", object)`
    sweet: _,        # chocolate must be sweet (but not necessarily chocolate.*)

    lindt: {          # To validate, say `validate("chocolate.lindt", object)`
      tasty: _,        # Applies only to chocolate.lindt (and not to chocolate.lindt.dark, for e.g.)

      '*': {            # Applies to all data under chocolate.lindt
        smooth: _,  # Could also be written smooth()
        creamy: 1   # Level 1 creamy
      }
# ...
    }
  }
}

}''').tolist()
pprint.pprint(clean_ast(ast))

Этот подход имеет свои ограничения. Окончательный AST все еще немного зашумлен, и определяемый вами язык должен интерпретироваться как допустимый код Python. Например, вы не могли поддержать это...

*:
    isYummy

... потому что этот синтаксис не анализируется как код Python. Однако его большим преимуществом является то, что вы контролируете преобразование AST, поэтому невозможно внедрить произвольный код Python.

person Marcelo Cantos    schedule 10.01.2010
comment
Хм, интересно. Это сделало бы синтаксический анализ очень простым, но я бы очень хотел научиться делать синтаксический анализ самостоятельно. Кроме того, этот файл можно редактировать через веб-интерфейс, поэтому я бы предпочел не позволять вводить произвольный код Python, даже если он доступен для редактирования только администраторам ;-) - person Cameron; 10.01.2010
comment
+1, да, dict здесь кажется оптимальным решением, и его можно легко использовать на стороне javascript и передавать как json - person Anurag Uniyal; 10.01.2010
comment
Да, одна из причин, по которой я изобрел этот псевдоязык, заключалась в том, чтобы иметь собственный формат, который можно было бы скомпилировать как в объекты Python, так и в объекты JavaScript. - person Cameron; 10.01.2010
comment
Спасибо всем за комментарии. Я добавил раздел в начало, посвященный обучению анализу, и один в конец для более чистого (и в некоторых отношениях более грязного) практического решения. - person Marcelo Cantos; 10.01.2010
comment
+1: не изобретайте DSL. Просто используйте Python. Это намного проще и уже работает. Когда у вас есть проблема, которая решается с помощью DSL, у вас теперь есть две проблемы. Первоначальная проблема плюс поддержка DSL. - person S.Lott; 10.01.2010
comment
Я думаю, что я пойду с парсером рекурсивного спуска. @S.Lott: Lol, ты прав, теперь у меня есть две проблемы, но я бы предпочел иметь формат, независимый от языка программирования, чем тот, который привязан к Python. Ответ Даррена указывает, что уже существуют некоторые такие форматы (например, XML) с множеством парсеров на выбор (на любом количестве языков программирования), но я хотел бы иметь свой собственный, который я мог бы научиться анализировать (плюс, ИМО, в XML слишком много мусора для чего-то, что можно представить так просто) - person Cameron; 11.01.2010
comment
Хороший ход, @Cameron. Я бы все же посоветовал хотя бы начать с Python tokenize, так как он справится со всем беспорядком отступов за вас. - person Marcelo Cantos; 12.01.2010

Опять же, я не буду учить вас синтаксическому анализу, но ваш формат настолько близок к легальному YAML, что вы, возможно, захотите просто переопределить свой язык как подмножество YAML и использовать стандартный синтаксический анализатор YAML.

person Beni Cherniavsky-Paskin    schedule 01.03.2010
comment
+1! Я понял это сам некоторое время назад и начал реализовывать это таким образом. Это сработало отлично, хотя я мало что узнал о разборе. С тех пор я пошел в другом направлении, где не используется DSL, но спасибо за ответ! - person Cameron; 02.03.2010

Если ваша цель — научиться синтаксическому анализу, я настоятельно рекомендую библиотеку стиля объектно-ориентированного программирования, например PyParsing. Они не такие быстрые, как более сложные варианты antler, lex, yac, но вы сразу же приступаете к парсингу.

person Shane Holloway    schedule 10.01.2010
comment
На PyCon 2010 Эндрю Далке показал, что, хотя PyParsing может быть медленным, PLY на самом деле является довольно быстрой альтернативой. Его слайды находятся по адресу us.pycon.org/2010/conference/schedule/. event/139 и видео его выступления должны появиться в ближайшее время. Его записи в блоге также нельзя пропустить, если вы занимаетесь разбором. dalkescientific.com/writings/diary - person gotgenes; 02.03.2010
comment
@gotgenes Я бы хотел присутствовать на этом выступлении! - person Shane Holloway; 02.03.2010

Как предложил «Марсело Кантос», вы можете использовать python dict, преимущество в том, что вам не нужно ничего анализировать, вы можете использовать те же правила на стороне сервера, что и python dict, и на стороне клиента, используя объекты javascript, и можете передавать их с сервера на клиент или наоборот как JSON.

Если вы действительно хотите выполнить синтаксический анализ самостоятельно, см. это http://nedbatchelder.com/text/python-parsers.html

но я не уверен, что вы сможете легко разобрать язык с отступом.

person Anurag Uniyal    schedule 10.01.2010
comment
Спасибо за ссылку. Я делаю этот проект больше для того, чтобы учиться, а не для того, чтобы что-то делать (хотя это тоже было бы неплохо) - person Cameron; 10.01.2010

Язык, для которого вы показали пример, вероятно, слишком сложен, чтобы написать для него простую (и безошибочную) функцию синтаксического анализа. Я бы посоветовал прочитать о методах синтаксического анализа, таких как рекурсивный спуск или синтаксический анализ на основе таблиц, таких как LL (1), LL (k) и т. Д.

Но это может быть слишком общим и/или сложным. Возможно, было бы проще упростить язык правил до чего-то простого, например, текста с разделителями.

Например, что-то вроде

шоколад:сладкий
шоколад.lindt:вкусный
шоколад.lindt.*:гладкий,кремовый(1)

Это было бы легче анализировать и можно было бы сделать без формальных парсеров.

person Sam Post    schedule 10.01.2010

Существуют библиотеки и инструменты, облегчающие синтаксический анализ. Одним из наиболее известных является lex/yacc. Существует библиотека Python под названием «lex' и файл руководство по его использованию.

person SpliFF    schedule 10.01.2010

какова мотивация кастомизированной файловой структуры? Можно ли преобразовать ваши данные в более известную структуру, такую ​​как XML? Если это так, вы можете использовать один из множества для анализа вашего файла. Использование принятого инструмента синтаксического анализа может сэкономить вам много времени на отладку и может сделать ваш файл более читабельным, если это необходимо.

person D.C.    schedule 10.01.2010