Python中的不规则字符串解析

2 投票
3 回答
504 浏览
提问于 2025-04-17 03:56

我刚开始学习python和django,现在想从我的爬虫程序中提取更多有效的信息。目前,这个爬虫会把漫画书的标题列表分成三部分,分别是出版日期、原始日期和标题,并正确地保存到一个CSV文件里。接着,我会把当前日期和标题传递到数据库的不同部分,这个过程是在我的加载脚本中完成的(把mm/dd/yy格式转换成yyyy-mm-dd格式,保存到“pub_date”这一列,标题则放到“title”这一列)。

一个常见的字符串可能看起来像这样:

10/12/11|10/12/11|Stan Lee's Traveler #12 (10 Copy Incentive Cover)

我成功地抓取到了日期,但标题就比较棘手了。在这个例子中,我希望在第二个“|”之后的信息填入三个不同的列。标题应该放到“title”这一列,它是一个字符字段;数字12(在“#”后面)应该放到“issue_num”这一列,它是一个小数字段;而括号中的内容应该放到“Special”这一字符字段里。我不太确定怎么进行这种严格的解析。

有时候,字符串中会有多个“#”(有一本漫画特别提到是一个捆绑包,“包含第#90到#95期”),还有一些字符串中有多个括号组(比如,“猩球崛起的背叛 #1 (共4期)(25本赠品封面)”)。

那么,我应该从哪个方向入手来解决这个问题呢?我对if/else语句的理解在处理更复杂的行时很快就崩溃了。我该如何高效地(如果可能的话,还要符合python的风格)解析这些行,并将它们细分,以便后续能正确地放入我的数据库中呢?

3 个回答

1

解析标题是比较难的部分,听起来你可以自己处理日期等其他内容。问题在于,没有一种规则可以解析所有的标题,而是有很多规则,你只能猜测哪一条适合特定的标题。

我通常通过创建一系列规则来处理这个问题,从最具体的到最一般的,然后逐个尝试,直到找到匹配的。

要编写这些规则,你可以使用re模块,甚至可以使用pyparsing

大致的思路是这样的:

class CantParse(Exception): 
    pass

# one rule to parse one kind of title
import re
def title_with_special( title ):
    """ accepts only a title of the form
     <text> #<issue> (<special>) """
    m = re.match(r"[^#]*#(\d+) \(([^)]+)\)", title)
    if m:
        return m.group(1), m.group(2)
    else:
        raise CantParse(title)


def parse_extra(title, rules):
    """ tries to parse extra information from a title using the rules """
    for rule in rules:
        try:
            return rule(title)
        except CantParse:
            pass

    # nothing matched
    raise CantParse(title)


# lets try this out
rules = [title_with_special] # list of rules to apply, add more functions here
titles = ["Stan Lee's Traveler #12 (10 Copy Incentive Cover)",
          "Betrayal Of The Planet Of The Apes #1 (Of 4)(25 Copy Incentive Cover) )"]

for title in titles:
    try:
        issue, special = parse_extra(title, rules)
        print "Parsed", title, "to issue=%s special='%s'" % (issue, special)
    except CantParse:
        print "No matching rule for", title

如你所见,第一个标题解析得很正确,但第二个就不行。你需要写一堆规则来考虑你数据中每种可能的标题格式。

1

使用 正则表达式 模块 re。比如,如果你有一个变量 s,里面存的是你样本记录的第三个用 | 分隔的字段,你可以这样做:

match = re.match(r"^(?P<title>[^#]*) #(?P<num>[0-9]+) \((?P<special>.*)\)$", s)
title = match.groups('title')
issue = match.groups('num')
special = match.groups('special')

在最后三行,你可能会遇到 IndexError 错误,因为某个字段缺失。你需要调整正则表达式,直到它能正确解析你想要的所有内容。

1

正则表达式是解决问题的好方法。不过,如果你觉得写正则表达式不太舒服,可以试试我写的一个小工具(https://github.com/hgrecco/stringparser)。这个工具可以把一种字符串格式(PEP 3101)转换成正则表达式。在你的情况下,你可以这样做:

>>> from stringparser import Parser
>>> p = Parser(r"{date:s}\|{date2:s}\|{title:s}#{issue:d} \({special:s}\)")
>>> x = p("10/12/11|10/12/11|Stan Lee's Traveler #12 (10 Copy Incentive Cover)")
OrderedDict([('date', '10/12/11'), ('date2', '10/12/11'), ('title', "Stan Lee's Traveler "), ('issue', 12), ('special', '10 Copy Incentive Cover')])
>>> x.issue
12

这个输出结果是一个(有序的)字典。这种方法适用于一些简单的情况,你也可以稍微调整一下,以便处理多个问题或者多个括号。

还有一点要注意:在当前版本中,你需要手动转义正则表达式中的特殊字符(比如,如果你想找到 |,你需要输入 \|)。我计划很快会对此进行改进。

撰写回答