Как лучше разобрать простую грамматику?

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

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

Из текста хотелось бы сгенерировать график конечно предпосылочных отношений. (Эта часть будет легкой после того, как я проанализирую данные.)

Некоторые примеры входных и выходных данных:

"CS 2110" => ("CS", 2110) # 0

"CS 2110 and INFO 3300" => [("CS", 2110), ("INFO", 3300)] # 1
"CS 2110, INFO 3300" => [("CS", 2110), ("INFO", 3300)] # 1
"CS 2110, 3300, 3140" => [("CS", 2110), ("CS", 3300), ("CS", 3140)] # 1

"CS 2110 or INFO 3300" => [[("CS", 2110)], [("INFO", 3300)]] # 2

"MATH 2210, 2230, 2310, or 2940" => [[("MATH", 2210), ("MATH", 2230), ("MATH", 2310)], [("MATH", 2940)]] # 3  
  1. Если все описание представляет собой только курс, оно выводится напрямую.

  2. Если курсы объединены ("и"), все они выводятся в одном списке.

  3. Если курсы разъединены ("или"), они находятся в отдельных списках

  4. Здесь есть и «и», и «или».

Одно предостережение, которое упрощает задачу: кажется, что вложенность фраз "и"/"или" никогда не бывает больше, чем показано в примере 3.

Как лучше всего это сделать? Я начал с PLY, но не мог понять, как разрешить конфликты уменьшения/уменьшения. Преимущество PLY в том, что легко манипулировать тем, что генерирует каждое правило синтаксического анализа:

def p_course(p):
 'course : DEPT_CODE COURSE_NUMBER'
 p[0] = (p[1], int(p[2]))

С PyParse менее понятно, как изменить вывод parseString(). Я рассматривал возможность использования идеи @Alex Martelli о сохранении состояния в объекте и создании из него вывода, но я не уверен, как именно это лучше всего сделать.

 def addCourse(self, str, location, tokens):
  self.result.append((tokens[0][0], tokens[0][1]))

 def makeCourseList(self, str, location, tokens):

  dept = tokens[0][0]
  new_tokens = [(dept, tokens[0][1])]
  new_tokens.extend((dept, tok) for tok in tokens[1:])

  self.result.append(new_tokens)

Например, для обработки случаев «или»:

    def __init__(self):
            self.result = []
            # ...
  self.statement = (course_data + Optional(OR_CONJ + course_data)).setParseAction(self.disjunctionCourses)



 def disjunctionCourses(self, str, location, tokens):
  if len(tokens) == 1:
   return tokens

  print "disjunction tokens: %s" % tokens

Как disjunctionCourses() узнает, какие более мелкие фразы нужно отделить? Все, что она получает, — это токены, но то, что было проанализировано до сих пор, хранится в result, так как же функция может определить, какие данные в result соответствуют каким элементам token? Думаю, я мог бы просмотреть токены, а затем найти элемент result с теми же данными, но это кажется запутанным...

Кроме того, существует множество описаний, содержащих разный текст, например:

"CS 2110 or permission of instructor"
"INFO 3140 or equivalent experience"
"PYSCH 2210 and sophomore standing"

Но не критично, что я разбираю этот текст.

Как лучше решить эту проблему?


person Nick Heiner    schedule 31.05.2010    source источник
comment
Нумерация в ваших примерах входных и выходных данных не соответствует нумерации в вашем их обсуждении.   -  person Tommy Herbert    schedule 01.06.2010


Ответы (5)


def parse(astr):
    astr=astr.replace(',','')
    astr=astr.replace('and','')    
    tokens=astr.split()
    dept=None
    number=None
    result=[]
    option=[]
    for tok in tokens:
        if tok=='or':
            result.append(option)
            option=[]
            continue
        if tok.isalpha():
            dept=tok
            number=None
        else:
            number=int(tok)
        if dept and number:
            option.append((dept,number))
    else:
        if option:
            result.append(option)
    return result

if __name__=='__main__':
    tests=[ ("CS 2110" , [[("CS", 2110)]]),
            ("CS 2110 and INFO 3300" , [[("CS", 2110), ("INFO", 3300)]]),
            ("CS 2110, INFO 3300" , [[("CS", 2110), ("INFO", 3300)]]),
            ("CS 2110, 3300, 3140", [[("CS", 2110), ("CS", 3300), ("CS", 3140)]]),
            ("CS 2110 or INFO 3300", [[("CS", 2110)], [("INFO", 3300)]]),
            ("MATH 2210, 2230, 2310, or 2940", [[("MATH", 2210), ("MATH", 2230), ("MATH", 2310)], [("MATH", 2940)]])]

    for test,answer in tests:
        result=parse(test)
        if result==answer:
            print('GOOD: {0} => {1}'.format(test,answer))
        else:
            print('ERROR: {0} => {1} != {2}'.format(test,result,answer))
            break

урожаи

GOOD: CS 2110 => [[('CS', 2110)]]
GOOD: CS 2110 and INFO 3300 => [[('CS', 2110), ('INFO', 3300)]]
GOOD: CS 2110, INFO 3300 => [[('CS', 2110), ('INFO', 3300)]]
GOOD: CS 2110, 3300, 3140 => [[('CS', 2110), ('CS', 3300), ('CS', 3140)]]
GOOD: CS 2110 or INFO 3300 => [[('CS', 2110)], [('INFO', 3300)]]
GOOD: MATH 2210, 2230, 2310, or 2940 => [[('MATH', 2210), ('MATH', 2230), ('MATH', 2310)], [('MATH', 2940)]]
person unutbu    schedule 31.05.2010
comment
Ничего себе, это намного проще, чем другие попытки, которые я сделал. Как вы это придумали? - person Nick Heiner; 01.06.2010
comment
@Rosarch: я уверен, что есть способы улучшить то, что я написал, но я думаю, что ключевая идея заключается в том, что вы можете читать токены слева направо и создавать результат, отслеживая свое состояние. После того, как вы нашли такой отдел, как CS, все последующие числа относятся к CS, пока вы не найдете другой отдел... Сначала я написал тестовый код, а затем много итераций выполнял функцию синтаксического анализа, чтобы пройти тесты. В моем первом проходе по этой задаче я проигнорировал и и или. Затем во втором проходе я понял, что это неважно, но или требует использования второго списка, option. Надеюсь это поможет. - person unutbu; 01.06.2010

Для простых грамматик мне очень нравятся грамматики синтаксического анализа (PEG), которые представляют собой дисциплинированный, структурированный способ написания синтаксического анализатора рекурсивного спуска. В динамически типизированном языке, таком как Python, вы можете делать полезные вещи, не имея отдельного «генератора синтаксического анализатора». Это означает, что никакой ерунды с конфликтами уменьшения-уменьшения или другими секретами разбора LR.

Я немного поискал, и pyPEG оказался хорошей библиотекой для Python.

person Norman Ramsey    schedule 31.05.2010
comment
Исходный код pyPEG выглядит кратким и хорошо написанным. Потрясающий. - person minghua; 27.08.2018
comment
Добавили ответ с парсером PEG. - person Jan; 03.03.2019

Я знаю, что этому вопросу около десяти лет, и ответ на него уже есть. В основном я публикую этот ответ, чтобы доказать себе, что я наконец понял PEG парсеры. Я использую фантастический модуль parsimonious здесь.
Это как говорится, вы можете придумать грамматику синтаксического анализа, построить ast и посетить это, чтобы получить желаемую структуру:

from parsimonious.nodes import NodeVisitor
from parsimonious.grammar import Grammar
from itertools import groupby

grammar = Grammar(
    r"""
    term            = course (operator course)*
    course          = coursename? ws coursenumber
    coursename      = ~"[A-Z]+"
    coursenumber    = ~"\d+"
    operator        = ws (and / or / comma) ws
    and             = "and"
    or              = (comma ws)? "or"
    comma           = ","
    ws              = ~"\s*"
    """
)

class CourseVisitor(NodeVisitor):
    def __init__(self):
        self.current = None
        self.courses = []
        self.listnum = 1

    def generic_visit(self, node, children):
        pass

    def visit_coursename(self, node, children):
        if node.text:
            self.current = node.text

    def visit_coursenumber(self, node, children):
        course = (self.current, int(node.text), self.listnum)
        self.courses.append(course)

    def visit_or(self, node, children):
        self.listnum += 1

courses = ["CS 2110", "CS 2110 and INFO 3300",
           "CS 2110, INFO 3300", "CS 2110, 3300, 3140",
           "CS 2110 or INFO 3300", "MATH 2210, 2230, 2310, or 2940"]

for course in courses:
    tree = grammar.parse(course)
    cv = CourseVisitor()
    cv.visit(tree)
    courses = [list(v) for _, v in groupby(cv.courses, lambda x: x[2])]
    print(courses)

Здесь мы идем снизу вверх, начиная с кирпичиков, таких как пробелы, операторы or, and и ,, которые в конечном итоге приведут к курсу, и, наконец, term. Класс посетителя строит нужную (ну вроде как надо избавиться от последнего элемента кортежа) структуру.

person Jan    schedule 03.03.2019

Если вы получаете конфликты уменьшения/уменьшения, вам необходимо указать приоритет «или» и «и». Я предполагаю, что «и» связывает сильнее всего, что означает «CS 101 и CS 102 или CS 201» означает [[CS 101, CS 102] [CS 201]].

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

PS. Похоже, язык обычный, можно подумать о DFA.

person Community    schedule 31.05.2010

Я не претендую на то, чтобы много знать о разборе грамматики, и для вашего случая решение unutbu - это все, что вам нужно. Но я немного узнал об анализе от Эрика Липперта в его недавней серии сообщений в блоге.

http://blogs.msdn.com/b/ericlippert/archive/2010/04/26/every-program-there-is-part-one.aspx

Это серия из 7 частей, посвященных созданию и разбору грамматики, а затем оптимизации грамматики для упрощения и повышения производительности синтаксического анализа. Он создает код C# для генерации всех комбинаций определенных грамматик, но не должно быть слишком много усилий, чтобы преобразовать его в python для разбора довольно простой собственной грамматики.

person Josh Smeaton    schedule 31.05.2010
comment
Обратите внимание, что существует огромная разница между использованием грамматики в качестве генератора строк в языке и использованием грамматики в качестве распознавателя строк в язык. Первая проблема очень проста; как вы видели, я сделал это в нескольких десятках строк кода. Последнее довольно сложно, особенно если грамматика сложная. - person Eric Lippert; 04.06.2010
comment
@eric достаточно честно. После того, как я написал этот ответ, я предпринял короткую попытку сделать это сам и обнаружил, что это совсем другое, и намного сложнее для того, кто пытается найти свой путь. - person Josh Smeaton; 06.06.2010