1、介绍正则表达式
计算机科学里曾经有个笑话:“如果你有一个问题打算用正则表达式(regular expression)来解决,那么就是两个问题了。”
不幸的是,正则表达式(通常简写 regex)经常被嘲笑是一堆随机符号的混和物,看着毫无意义。这种印象让人对其避而远之,然后费尽心思写一堆没必要又复杂的查找和过滤函数,其实他们真正需要的就是一行正则表达式。其实正则表达式上手一点儿也不难,而且运行很快,通过一些简单的例子就可以轻松地学会。
之所以叫正则表达式,是因为它们可以识别正则字符串(regular string);也就是说,它们可以这么定义:“如果你给我的字符串符合规则,我就返回它”,或者是“如果字符串不符合规则,我就忽略它”。这在要求快速浏览大文档,以查找像电话号码和邮箱地址之类的字符串时是非常方便的。
注意这里我用了一个词组正则字符串。什么是正则字符串?其实就是任意可以用一系列线性规则构成的字符串 3,就像:
(1) 字母“a”至少出现一次;
(2) 后面跟着字母“b”重复 5 次;
(3) 后面再跟字母“c”重复任意偶数次;
(4) 最后一位是字母“d”,也可以没有。
满足上面规则的字符串有:“aaaabbbbbccccd”“aabbbbbcc”等(有无穷多种变化)。
正则表达式就是表达这组规则的缩写。这组规则的正则表达式如下所示:
aabbbbb(cc)(d | )
第一次看这个字符串会觉得有点儿奇葩,但是当我们把它分解之后就会很清楚了。
• aa*
a 后面跟着的 a(读作 a 星)表示“重复任意次 a,包括 0 次”。这样就可以保证字母 a至少出现一次。
• bbbbb
这没有什么特别的——就是 5 次 b。
• (cc)
任意偶数个字符都可以编组,这个规则是用括号两个 c,然后后面跟一个星号,表示有任意次两个 c(也可以是 0 次)。
• (d|)
增加一个竖线(|)在表达式里表示“这个或那个”。本例是表示“增加一个后面跟着空
格的 d,或者只有一个空格”。这样我们可以保证字符串的结尾最多是一个后面跟着空格的 d。
2、尝试正则表达式
在学习书写正则表达式的时候,做一些实验感受一下它们如何工作,这是至
关重要的。
如果你不想打开代码编辑器,写完再运行程序检查正则表达式的运行是否符
合预期,那么你可以去 RegexPal(http://regexpal.com/)这类网站上在线测试
正则表达式。
正则表达式在实际中的一个经典应用是识别邮箱地址。虽然不同邮箱服务器的邮箱地址的
具体规则不尽相同,但是我们还是可以创建几条通用规则。每条规则对应的正则表达式如
下表第 2 列所示。
把上面的规则连接起来,就获得了完整的正则表达式:
[A-Za-z0-9._+]+@[A-Za-z]+.(com|org|edu|net)
当我们动手开始写正则表达式的时候,最好先写一个步骤列表描述出你的目标字符串结构。
还要注意一些细节的处理。比如,当你识别电话号码的时候,会考虑国家代码和分机号吗?
表 2-1 用简单的说明和例子列举了正则表达式的一些常用符号。这个列表并不是全部符
号,另外就像之前所说的,可能在不同编程语言中会遇到一些变化。但是,这 12 个符号
是 Python 的正则表达式中最常用的,可以用来查找和收集绝大多数数据类型
3、正则表达式: 并非处处正则!
正则表达式的标准版(本书使用的版本,用于 Python 和 BeautifulSoup)是基
于 Perl 语法演变而来的。绝大多数主流编程语言都使用与之相同或近似的版
本。但是,在其他语言中使用这些正则表达式时需要当心,否则可能会出问
题。有些语言,比如 Java,其正则表达式就和 Python 不太一样。总之,遇
到问题时看文档!
4、正则表达式和BeautifulSoup
如果你觉得前面介绍的正则表达式内容与本书的主题有点儿脱节,那么这里就把它们连接
起来。在抓取网页的时候, BeautifulSoup 和正则表达式总是配合使用的。其实,大多数支
持字符串参数的函数(比如, find(id="aTagIdHere"))都可以用正则表达式实现。
让我们看几个例子,待抓取的网页是 http://www.pythonscraping.com/pages/page3.html。
注意观察网页上有几个商品图片——它们的源代码形式如下:
<img src="../img/gifts/img3.jpg">
如果我们想抓取所有图片的 URL 链接,非常直接的做法就是用 findAll("img") 抓取所有
图片,对吗?但是,有个问题。除了那些明显“多余的”图片(比如, LOGO)之外,新
式的网站里都有一些隐藏图片,用于网页布局留白和元素对齐的空白图片,以及一些不容
易察觉到的图片标签。总之,你不能仅用商品图片来统计网页上所有的图片。
而且网页的布局也可能会变化,或者,因为某些原因,我们不想通过图片在网页中的位置
来查找标签。那么当你想抓取随机分布在网站里的某个元素或数据时,就会出现问题。例
如,一些网页的最上面可能有一张商品图片,但是在另一些网页上没有。
解决这类问题的办法,就是直接定位那些标签来查找信息。在本例中,我们直接通过商品
图片的文件路径来查找:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)
images = bsObj.findAll("img",{"src":re.compile("\.\.\/img\/gifts\/img.*\.jpg")})
for image in images:
print(image["src"])
这段代码会打印出图片的相对路径,都是以 ../img/gifts/img 开头,以 .jpg 结尾,其结果如
下所示:
../img/gifts/img1.jpg
../img/gifts/img2.jpg
../img/gifts/img3.jpg
../img/gifts/img4.jpg
../img/gifts/img6.jpg
正则表达式可以作为 BeautifulSoup 语句的任意一个参数,让你的目标元素查找工作极具灵
活性。
5、获取属性
到目前为止,我们已经介绍过如何获取和过滤标签,以及获取标签里的内容。但是,在网
络数据采集时你经常不需要查找标签的内容,而是需要查找标签属性。比如标签 <a> 指向
的 URL 链接包含在 href 属性中,或者 <img> 标签的图片文件包含在 src 属性中,这时获
取标签属性就变得非常有用了。
对于一个标签对象,可以用下面的代码获取它的全部属性:
myTag.attrs
要注意这行代码返回的是一个 Python 字典对象,可以获取和操作这些属性。比如要获取图
片的资源位置 src,可以用下面这行代码:
myImgTag.attrs["src"]
6、 Lambda表达式
如果在学校读的是计算机科学专业,那么可能学过 Lambda 表达式,不过可能从来没有用过它。如果你不是计算机科学专业,它们看着可能有点儿陌生(或者只是“曾经学习过的东西”)。在这一节里,虽然我们不打算深入学习这个相当实用的函数,但是会用几个例子来演示它们是如何用在网络数据采集中的。
Lambda 表达式本质上就是一个函数,可以作为其他函数的变量使用;也就是说,一个函数不是定义成 f(x, y),而是定义成 f(g(x), y),或 f(g(x), h(x)) 的形式。
BeautifulSoup 允许我们把特定函数类型当作 findAll 函数的参数。唯一的限制条件是这些函数必须把一个标签作为参数且返回结果是布尔类型。 BeautifulSoup 用这个函数来评估它遇到的每个标签对象,最后把评估结果为“真”的标签保留,把其他标签剔除。
例如,下面的代码就是获取有两个属性的标签:
soup.findAll(lambda tag: len(tag.attrs) == 2)
这行代码会找出下面的标签:
<div class="body" id="content"></div>
<span style="color:red" class="title"></span>
如果你愿意多写一点儿代码,那么在 BeautifulSoup 里用 Lambda 表达式选择标签,将是正
则表达式的完美替代方案。
7、超越BeautifulSoup
虽然本书全部用 BeautifulSoup(也是 Python 里最受欢迎的 HTML 解析库之一),但它并不
是你唯一的选择。如果 BeautifulSoup 不能满足你的需求,你可以看看其他的库。
• lxml
这个库(http://lxml.de/)可以用来解析 HTML 和 XML 文档,以非常底层的实现而闻名
于世,大部分源代码是用 C 语言写的。虽然学习它需要花一些时间(其实学习曲线越
陡峭,表明你可以越快地学会它),但它在处理绝大多数 HTML 文档时速度都非常快。
• HTML parser
这是 Python 自带的解析库(https://docs.python.org/3/library/html.parser.html)。因为它不
用安装(只要装了 Python 就有),所以可以很方便地使用。
来源:Python网络爬虫权威指南
下面再次剖析正则表达式的各部分用途:
8、re 模块
不同的语言均有使用正则表达式的方法,但各不相同,Python 是使用 re 模块来实现的,因为这一部分比较难,但又重要,所以一定要学。举例:
>>> import re
>>> re.search(r"面朝大海,春暖花开!", "如果我是大海")
>>> re.search(r"大海", "面朝大海,春暖花开!")
<re.Match object; span=(2, 4), match='大海'>
>>>
(1)search()方法:
从上面可以看出,如果词“大海”在后面的内容中能搜寻到就会运行,否则不执行。
<re.Match object; span=(2, 4), match='大海'>
这里主要使用到了search()方法,该方法的原理是:
re.search 扫描整个字符串并返回正则表达式模式第一次成功匹配的位置。
(2)语法结构:
re.search(pattern, string, flags=0)
(3)函数参数说明:
pattern:匹配的正则表达式
string:要匹配的字符串。
flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。
(4)匹配返回
匹配成功re.search方法返回一个匹配的对象,否则返回None。
(5)find() 方法
使用 find() 方法也可以实现上面的功能,如下:
>>> "I love Python".find('Python')
7
>>> "I love Python".find('Python'), "I love Python".find('Python')
(7, 7)
>>> "I love Python".find('Python') + len('Python')
13
>>> ("I love Python".find('Python'), "I love Python".find('Python') + len('Python'))
(7, 13)
>>> ("面朝大海,春暖花开!".find('大海'), "面朝大海,春暖花开!".find('大海') + len('大海'))
(2, 4)
也有下面的情况,下面的例子使用 find() 方法没办法实现的:
9、通配符
通配符 (就是我们实际中经常使用的 " 星号* 和 问号?" 这一类可以表示任何字符的符号),例如我们想找到 word 类型的文件的时候,我们就会搜索 *.docx。
正则表达式也有所谓的通配符,这里是使用点号(.),它可以匹配除了 换行符 以外的任何字符。
>>> re.search(r'.', '面朝大海,春暖花开!')
<re.Match object; span=(0, 1), match='面'>
>>> re.search(r'.', 'I love Python')
<re.Match object; span=(0, 1), match='I'>
这里它就匹配到了字符串中的 ‘面’和‘I’
>>> re.search(r'Pytho.', 'I love Python')
<re.Match object; span=(7, 13), match='Python'>
>>> re.search(r'大海.', '面朝大海,春暖花开!')
<re.Match object; span=(2, 5), match='大海,'>
>>> re.search(r'大.', '面朝大海,春暖花开!')
<re.Match object; span=(2, 4), match='大海'>
这里 点号(.) 就匹配到了‘c’,然后正则表达式就匹配到了 ‘Python’。
每日三道题, 笔试不吃亏:
题目4:输入某年某月某日,判断这一天是这一年的第几天?
程序分析:以9月23日为例,应该先把前八个月的加起来,然后再加上23天即本年的第几天,特殊情况,闰年且输入月份大于2时需考虑多加一天:
程序源代码:
year = int(input('请输入年份year:\n'))
month = int(input('请输入月份month:\n'))
day = int(input('请输入日期day:\n'))
months = (0,31,59,90,120,151,181,212,243,273,304,334)
if 0 < month <= 12:
sum = months[month - 1]
else:
print ('data error')
sum += day
leap = 0
if (year % 400 == 0) or ((year % 4 == 0) and (year % 100 != 0)):
leap = 1
if (leap == 1) and (month > 2):
sum += 1
print('it is the %dth day.' % sum)
print('今天是这一年的第 %d 天.' % sum)
运行结果:
请输入年份year:
2019
请输入月份month:
9
请输入日期day:
23
it is the 266th day.
今天是这一年的第 266 天.
题目5:输入三个整数x,y,z,请把这三个数由小到大输出。
程序分析:我们想办法把最小的数放到x上,先将x与y进行比较,如果x>y则将x与y的值进行交换,然后再用x与z进行比较,如果x>z则将x与z的值进行交换,这样能使x最小。
a = []
for i in range(3):
x = int(input('请输入一个数integer:\n'))
a.append(x)
a.sort()
print("三个数的大小顺序为",a)
运行结果:
请输入一个数integer:
34
请输入一个数integer:
66
请输入一个数integer:
15
三个数的大小顺序为 [15, 34, 66]
题目6:斐波那契数列。
程序分析:斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……。
在数学上,费波那契数列是以递归的方法来定义:
F0 = 0 (n=0)
F1 = 1 (n=1)
Fn = F[n-1]+ Fn-2
def fib(n):
a,b = 1,1
for i in range(n-1):
a,b = b,a+b
return a
# 输出了第20个斐波那契数列
print("第20个斐波那契数列为",fib(20))
运行结果:
第20个斐波那契数列为 6765
网友评论