如何解析简单语法?

37 投票
6 回答
42687 浏览
提问于 2025-04-15 23:23

好吧,我之前问过一些关于这个项目的小问题,但我对自己设计的方案还是没什么信心,所以我想问一个更广泛的问题。

我正在解析课程目录中的先修课程描述。这些描述几乎总是遵循某种特定的格式,这让我觉得我可以解析大部分内容。

我想从文本中生成一个课程先修关系的图表。(这部分在我解析完数据后会很简单。)

以下是一些示例输入和输出:

"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"

但我并不需要解析这些文本。

有没有更好的方法来处理这个问题?

6 个回答

7

我知道这个问题已经有十年了,肯定有人回答过了。我主要发这个回答是为了证明我终于理解了 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)

在这里,我们是从下往上走的,首先处理像空格这样的括号,然后是操作符 orand,,最后到达课程,最终得到 term。访问者类会构建出想要的(其实有点问题,需要去掉最后一个元组元素)结构。

7

对于简单的语法,我非常喜欢使用解析表达式语法(PEG),这是一种有条理、结构化的方式来编写递归下降解析器。在像Python这样的动态类型语言中,你可以在不需要单独的“解析器生成器”的情况下做一些有用的事情。这意味着你不需要担心一些复杂的情况,比如减少-减少冲突或其他LR解析的难题。

我稍微查了一下,发现pyPEG似乎是一个很不错的Python库。

28
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)]]

产生

撰写回答