使用词法分析或正则表达式将非结构化文本解析为结构化形式

0 投票
1 回答
580 浏览
提问于 2025-04-16 00:38

我想写一些代码,让它像谷歌日历的快速添加功能一样工作。你知道的,就是那种你可以输入以下内容的功能:

1) 2010年9月24日,约翰的生日
2) 约翰的生日,24/9/10
3) 2010年9月24日,约翰·多的生日
4) 24-9-2010:约翰·多的生日
5) 约翰·多的生日,2010年9月24日

然后它能识别出我们想要在2010年9月24日创建一个事件,其他的内容就是事件的描述。

我想用Python来实现这个功能。

我在考虑设计一个方案,使用正则表达式来匹配上面列出的所有情况,并提取出日期。不过我觉得可能有更聪明的方法来解决这个问题。因为我显然没有接受过词法分析或各种解析器风格的训练。我在寻找一个好的方法来处理这个问题。

1 个回答

2

注意:这里的Python代码并不正确!它只是一个大概的伪代码,展示可能的样子。

正则表达式非常擅长从固定格式的文本中查找和提取数据,比如日期格式(例如DD/MM/YYYY)。

词法分析器和解析器组合在处理结构化但稍微灵活的数据格式时效果很好。词法分析器会把文本分割成一个个小单元,这些小单元叫做“标记”。标记是特定类型的信息单位(比如数字、字符串等)。解析器则会根据这些标记的顺序来做不同的处理。

从数据来看,你可以看到基本的(主语、动词、宾语)结构,它们以不同的组合表示关系(人、'生日'、日期):

我会把29/9/10和24-9-2010当作一个标记来处理,使用正则表达式,把它们返回为日期类型。你可能也可以对其他日期做同样的处理,使用一个映射来把“September”和“sep”都转换成9。

然后你可以把其他的内容作为字符串返回(用空格分隔)。

接下来你会得到:

  1. 日期 ',' 字符串 '生日'
  2. 字符串 '生日' ',' 日期
  3. 日期 '生日' '的' 字符串 字符串
  4. 日期 ':' 字符串 字符串 '生日'
  5. 字符串 字符串 '生日' 日期

注意:这里的'生日'、','、':'和'的'是关键词,所以:

class Lexer:
    DATE = 1
    STRING = 2
    COMMA = 3
    COLON = 4
    BIRTHDAY = 5
    OF = 6

    keywords = { 'birthday': BIRTHDAY, 'of': OF, ',': COMMA, ':', COLON }

    def next_token():
        if have_saved_token:
            have_saved_token = False
            return saved_type, saved_value
        if date_re.match(): return DATE, date
        str = read_word()
        if str in keywords.keys(): return keywords[str], str
        return STRING, str

    def keep(type, value):
        have_saved_token = True
        saved_type = type
        saved_value = value

除了第3种情况,其他情况都使用了人的所有格形式(如果最后一个字符是辅音就用's,如果是元音就用s)。这可能有点棘手,因为'Alexis'可能是'Alexi'的复数形式,但由于你限制了复数形式的使用范围,所以很容易检测到:

def parseNameInPluralForm():
    name = parseName()
    if name.ends_with("'s"): name.remove_from_end("'s")
    elif name.ends_with("s"): name.remove_from_end("s")
    return name

现在,名字可以是名字或者名字 姓氏(是的,我知道日本的顺序是反的,但从处理的角度来看,上述问题并不需要区分名字和姓氏)。以下内容将处理这两种形式:

def parseName():
    type, firstName = Lexer.next_token()
    if type != Lexer.STRING: raise ParseError()
    type, lastName = Lexer.next_token()
    if type == Lexer.STRING: # first-name last-name
        return firstName + ' ' + lastName
    else:
        Lexer.keep(type, lastName)
        return firstName

最后,你可以用类似这样的方式处理1-5种形式:

def parseBirthday():
    type, data = Lexer.next_token()
    if type == Lexer.DATE: # 1, 3 & 4
        date = data
        type, data = Lexer.next_token()
        if type == Lexer.COLON or type == Lexer.COMMA: # 1 & 4
            person = parsePersonInPluralForm()
            type, data = Lexer.next_token()
            if type != Lexer.BIRTHDAY: raise ParseError()
        elif type == Lexer.BIRTHDAY: # 3
            type, data = Lexer.next_token()
            if type != Lexer.OF: raise ParseError()
            person = parsePerson()
    elif type == Lexer.STRING: # 2 & 5
        Lexer.keep(type, data)
        person = parsePersonInPluralForm()
        type, data = Lexer.next_token()
        if type != Lexer.BIRTHDAY: raise ParseError()
        type, data = Lexer.next_token()
        if type == Lexer.COMMA: # 2
            type, data = Lexer.next_token()
        if type != Lexer.DATE: raise ParseError()
        date = data
    else:
        raise ParseError()
    return person, date

撰写回答