Python 正则表达式与 IMDB 前250名单
我刚开始学习Python,正在尝试用这段有问题的代码来匹配IMDB上前250部电影:
import urllib2
import re
def main():
response = urllib2.urlopen('http://www.imdb.com/chart/top')
html = response.read()
entries = re.findall("/title/.*</font>", html) #Wrong regex
print entries
if __name__ == "__main__":
main()
我的想法是想要匹配所有在/title/
和</font>
之间的内容,所以我用了中间的.*
,但显然这样做不对,因为它只是匹配了整个列表,而不是每一条电影的信息。我对网上的正则表达式教程感到很困惑……谁能帮帮我?
5 个回答
试试这个
def main(s):
response = urllib2.urlopen('http://www.imdb.com/chart/top')
html = response.read()
entries = re.findall("<a.*?/title/(.*?)/\">(.*?)</a>", html) #Wrong regex
return entries
它使用了分组来提取电影的IMDB编号和标题。结果会是一个包含多个元组的列表。
用正则表达式来解析 HTML
是个不太好的做法,因为处理这些内容的 html 解析器
已经被专门开发出来了。在 Python 中,有很多选择,比如 Beautiful Soup
和 lxml
等等。
我将展示如何使用 lxml
和 XPath 表达式
来获取前 250 个标题。
import lxml
from lxml import etree
import urllib2
response = urllib2.urlopen('http://www.imdb.com/chart/top')
html = response.read()
imdb = etree.HTML(html)
titles = imdb.xpath('//div[@id="main"]/table//tr//a/text()')
如果你输入 print titles[0]
,它会输出 '肖申克的救赎'
。
要使用 XPath,可以用 Firefox 的 firebug
插件,或者安装 firepath
。
这是一个比较长的回答;我希望它能帮到你,同时也能让你更清楚正则表达式的奥秘。
处理这个问题的方法是先选择一个完整的条目,然后用可以匹配的模式替换你想要概括的部分。
比如,你提到的页面中的第一个条目看起来是这样的。
<tr bgcolor="#e5e5e5" valign="top"><td align="right"><font face="Arial, Helvetica, sans-serif" size="-1"><b>1.</b></font></td><td align="center"><font face="Arial, Helvetica, sans-serif" size="-1">9.2</font></td><td><font face="Arial, Helvetica, sans-serif" size="-1"><a href="/title/tt0111161/">The Shawshank Redemption</a> (1994)</font></td><td align="right"><font face="Arial, Helvetica, sans-serif" size="-1">689,815</font></td></tr>
所有条目都长得差不多,但每个条目之间有一些不同的地方。我会逐一讲解。
背景颜色,如你在页面上看到的,交替为灰色(#e5e5e5
)和白色(#ffffff
)。你需要能够匹配这两种颜色。为此,你可以使用|
运算符,它允许你指定多个模式,任意一个都可以接受。这个条目的那部分将被替换为:
bgcolor="#(?:e5e5e5|ffffff)"
括号的作用是让|
运算符只分隔表达式中的那一部分,而不是全部。开头的?:
可以防止这个括号表达式被当作一个组捕获,我们不想这样,因为我们不关心某部电影的具体颜色。稍后会详细讲。
接下来,每个条目中变化的部分是条目的编号。我们将用这个来替换条目的那部分:
(\d{1,3})\.
每当你在正则表达式中看到反斜杠,它就有特殊的作用;具体作用取决于后面的字符。\d
可以匹配任何数字。{1,3}
表示它期望有一个到三个数字,因为所有条目编号都是一到三位数。
反斜杠的另一个主要用途是字面表示一些特殊字符。点号就是这样一个字符;它可以匹配任何字符。但我们想要的是字面上的.
。为了得到这个,我们使用\.
,它匹配.
。需要这样处理的字符叫做元字符,包括这些:.^$*+?|{}[]()\
最后,我们在数字周围加了括号。除了细分正则表达式,括号还形成一个组;也就是说,它们表示你关心正则表达式的这一部分。当你使用re.findall
时,只有在组中的匹配部分会被保存;其他部分都会被丢弃。(上面提到的?:
是为了在你不关心正则表达式的某部分,但又需要用括号来细分时使用的。)我假设表格中的所有信息,包括条目编号,都是重要的,所以我把条目编号(但不包括后面的句号,因为我们不关心它)放在了括号里。如果你不关心条目编号,可以去掉括号。
接下来是电影的评分,我们用类似的方法处理:
(\d\.\d)
接下来是电影的链接。这里唯一变化的是链接中的数字,始终是七位数。所以我们将条目的那部分替换为:
<a href="/title/tt(\d{7})/">
现在我们要处理电影标题。处理这个有几种不同的方法;最重要的是避免模糊不清。你必须明确标题后面的<
不是标题的一部分(这就是你第一次尝试没有成功的原因)。一种方法是声明电影标题只能包含字母、数字、空格和某些标点符号。为此,你可以这样说:
([A-Za-z0-9 ,.:'-]+)
方括号表示它将匹配括号内的任何字符。这些可以是字面字符(如标点符号和空格)或字符范围(例如,A-Z
匹配所有大写字母)。范围用连字符表示,放在起始和结束字符之间。结尾的连字符是字面上的连字符;它放在最后是为了不让正则表达式解析器误认为它是范围的一部分。注意,大多数元字符在字符组内不需要用反斜杠转义。
同时,+
表示重复前面的字符一次或多次。综合起来,每个电影标题由我选择的字符集中的一个或多个字符组成。由于<
不在这个字符集中,所以标题到此为止没有疑问。
这个模式看起来适合标题,但它有一个问题。页面上有两个电影标题(WALL·E和8½)包含一些特殊字符,而这个模式无法匹配。与其试图列出所有可能出现在电影标题中的字符,不如使用一个简单得多的模式:
([^<]+)
字符集开头的^
表示它应该匹配所有不在该集合中的字符(不包括^
本身)。这样,电影标题可以包含任何字符(所以我们不必猜测哪些是有效的),唯一的例外是<
(因为,如前所述,我们不能允许这种模糊性)。幸运的是,即使有电影标题中包含<
,HTML的规则规定它必须被转义为<
,所以它仍然会匹配。这个集合并不理想,因为它不够具体,但它简单,而且没有更好的选择。
接下来是电影的年份:
\((\d{4})\)
这有点混淆;外面的括号用反斜杠转义,因为它们表示字面括号,但里面的括号不需要,因为它们是用来形成一个组的(因为我们关心电影的年份)。
最后是投票数。这是一个用逗号分组的正整数,所以我们将这样匹配:
(\d{1,3}(?:,\d{3})*)
这又有点复杂。大致意思是这个数字将由一个或多个用逗号分隔的组组成。第一个组可以有一到三个数字;其余的组都必须有三个数字。*
与+
类似,但它匹配零个或多个重复,而不是一个或多个。内部的括号是必要的,以便*
匹配其中的所有内容,而不仅仅是前面的字符。它有一个?:
,因为我们关心的是整个数字,而不是其中的单个组。
现在是时候把所有内容组合在一起了。在这样做时,我们需要考虑最后一点:确保正则表达式解析器能看到所有这些反斜杠,以便它们能正常工作。我们需要担心这个的原因是,Python本身使用反斜杠作为转义字符,如果它在正则表达式解析器之前处理了这些反斜杠,就会出问题。为了防止这种情况,正则表达式几乎总是用原始字符串编写的,也就是在开头的引号前加一个r
。这告诉Python不要处理反斜杠。此外,由于正则表达式中有很多双引号,我们希望将其放在单引号字符串中。
综上所述,代码在你的程序中看起来是这样的:
entries = re.findall(r'<tr bgcolor="#(?:e5e5e5|ffffff)" valign="top"><td align="right"><font face="Arial, Helvetica, sans-serif" size="-1"><b>(\d{1,3})\.</b></font></td><td align="center"><font face="Arial, Helvetica, sans-serif" size="-1">(\d\.\d)</font></td><td><font face="Arial, Helvetica, sans-serif" size="-1"><a href="/title/tt(\d{7})/">([^<]+)</a> \((\d{4})\)</font></td><td align="right"><font face="Arial, Helvetica, sans-serif" size="-1">(\d{1,3}(?:,\d{3})*)</font></td></tr>', html)
这将为你提供一个匹配列表。每个匹配将是一个元组,包含所有组(编号、评分、链接编号、标题、年份、投票数),按顺序排列。请注意,所有这些,即使是表示数字的部分,都是字符串。
如果你想了解更多关于正则表达式的内容,我推荐这个网站。还有值得阅读的是Python的re
模块文档。