爬虫学习笔记(三)
正则表达式
在上一小节我们拿到了豆瓣喜剧电影排行榜,但是数据非常杂乱,有很多我们不想要的内容。该怎么办呢。这里我们就要引用一个重要的内容,那就是正则表达式
正则表达式是什么
正则表达式,简称Regex,是一种强大的文本处理语言,用于描述字符串的模式。它能够帮助用户在文本中进行复杂的搜索、匹配、替换和提取操作。正则表达式的基本理念是用有限的符号来定义和匹配无限的字符串序列。
基本概念
正则表达式是一个字符串,其中包含普通字符(如字母和数字)和特殊字符(元字符),用于定义一个搜索模式。它广泛应用于各种编程语言、文本编辑器和命令行工具中,如JavaScript、Python、grep等。
主要用途
- 搜索:在大量文本中查找符合特定模式的字符串
- 验证:检查字符串是否符合特定格式,如邮箱地址、电话号码的格式验证。
- 替换:根据模式替换文本,例如批量修改文档中的某些部分。
- 提取:从文本中提取符合模式的信息,如从网页中抓取链接。
实例引入
接下来通过一个例子看一下正则表达式的用法
打开开源中国提供的正则表达式测试工具 http://tool.oschina.net/regex/ 输入待匹配的文本,然后选择常用的正则表达式,就可以得出相应的匹配结果了。例如,这里输入待匹配的文本如下:
Hello, my phone number is 010-86432100 and email is cqc@cuiqingcai.com, and my website is https://cuiqingcai.com.
尝试用正则表达式提取出这段文本中的电话,邮箱和网址
在网页右侧可以选择相应的正则表达式从文本中提取我们想要的内容
其实,这里就是用了正则表达式匹配,也就是用一定的规则将特定的文本提取出来。比如,电子邮件开头是一段字符串,然后是一个 @符号,最后是某个域名,这是有特定的组成格式的。另外,对于 URL,开头是协议类型,然后是冒号加双斜线,最后是域名加路径。
对于 URL 来说,可以用下面的正则表达式匹配:
[a-zA-z]+://[^\s]*
这个正则表达式看上去是乱糟糟的一团,其实不然,这里面都是有特定的语法规则的。比如,a-z 代表匹配任意的小写字母,\s 表示匹配任意的空白字符,* 就代表匹配前面的字符任意多个,这一长串的正则表达式就是这么多匹配规则的组合。
写好正则表达式后,就可以拿它去一个长字符串里匹配查找了。不论这个字符串里面有什么,只要符合我们写的规则,统统可以找出来。对于网页来说,如果想找出网页源代码里有多少 URL,用匹配 URL 的正则表达式去匹配即可。
下表列出了常用的匹配规则。
模式 | 描述 |
---|---|
\w | 匹配字母、数字及下划线 |
\W | 匹配不是字母、数字及下划线的字符 |
\s | 匹配任意空白字符,等价于 [\t\n\r\f] |
\S | 匹配任意非空字符 |
\d | 匹配任意数字,等价于 [0-9] |
\D | 匹配任意非数字的字符 |
\A | 匹配字符串开头 |
\Z | 匹配字符串结尾,如果存在换行,只匹配到换行前的结束字符串 |
\z | 匹配字符串结尾,如果存在换行,同时还会匹配换行符 |
\G | 匹配最后匹配完成的位置 |
\n | 匹配一个换行符 |
\t | 匹配一个制表符 |
^ | 匹配一行字符串的开头 |
$ | 匹配一行字符串的结尾 |
. | 匹配任意字符,除了换行符,当 re.DOTALL 标记被指定时,则可以匹配包括换行符的任意字符 |
[…] | 用来表示一组字符,单独列出,比如 [amk] 匹配 a、m 或 k |
不在 [] 中的字符,比如 匹配除了 a、b、c 之外的字符 | |
* | 匹配 0 个或多个表达式 |
+ | 匹配 1 个或多个表达式 |
? | 匹配 0 个或 1 个前面的正则表达式定义的片段,非贪婪方式 |
{n} | 精确匹配 n 个前面的表达式 |
{n, m} | 匹配 n 到 m 次由前面正则表达式定义的片段,贪婪方式 |
a | 匹配 a |
b | 匹配b |
( ) | 匹配括号内的表达式,也表示一个组 |
match
这里首先介绍第一个常用的匹配方法 —— match,向它传入要匹配的字符串以及正则表达式,就可以检测这个正则表达式是否匹配字符串。
match 方法会尝试从字符串的起始位置匹配正则表达式,如果匹配,就返回匹配成功的结果;如果不匹配,就返回 None。示例如下:
引入re模块
1 | import re |
通过match
方法匹配字符串
1 | content = 'Hello 123 4567 World_This is a Regex Demo' |
^Hello\s\d\d\d\s\d{4}\s\w{10}
开头的 ^ 是匹配字符串的开头,也就是以 Hello 开头;然后 \s 匹配空白字符,用来匹配目标字符串的空格;\d 匹配数字,3 个 \d 匹配 123;然后再写 1 个 \s 匹配空格;后面还有 4567,我们其实可以依然用 4 个 \d 来匹配,但是这么写比较烦琐,所以后面可以跟 {4} 以代表匹配前面的规则 4 次,也就是匹配 4 个数字;然后后面再紧接 1 个空白字符,最后 \w{10} 匹配 10 个字母及下划线。
打印结果
1 | print(result) |
我们可以看到,re.match
返回一个 SRE_Match 对象。该对象有两个方法:group 方法可以输出匹配到的内容,结果是 Hello 123 4567 World_This,这恰好是正则表达式规则所匹配的内容;span 方法可以输出匹配的范围,结果是 (0, 25),这就是匹配到的结果字符串在原字符串中的位置范围。
匹配目标
接下来我们看一下如何从字符串中提取出想要的内容
可以使用 () 括号将想提取的子字符串括起来。() 实际上标记了一个子表达式的开始和结束位置,被标记的每个子表达式会依次对应每一个分组,调用 group 方法传入分组的索引即可获取提取的结果。示例如下:
1 | import re |
可以看到,我们成功得到了 1234567。这里用的是 group(1),它与 group() 有所不同,后者会输出完整的匹配结果,而前者会输出第一个被 () 包围的匹配结果。假如正则表达式后面还有 () 包括的内容,那么可以依次用 group(2)、group(3) 等来获取。
同用匹配
刚才我们写的正则表达式其实比较复杂,出现空白字符我们就写 \s 匹配,出现数字我们就用 \d 匹配,这样的工作量非常大。其实完全没必要这么做,因为还有一个万能匹配可以用,那就是.(点星)。其中.(点)可以匹配任意字符(除换行符),(星)代表匹配前面的字符无限次,所以它们组合在一起就可以匹配任意字符了。有了它,我们就不用挨个字符地匹配了。
我们改写正则表达式
1 | import re |
这里我们将中间部分直接省略,全部用 .* 来代替,最后加一个结尾字符串就好了。运行结果如下:
可以看到,group 方法输出了匹配的全部字符串,也就是说我们写的正则表达式匹配到了目标字符串的全部内容;span 方法输出 (0, 41),这是整个字符串的长度。
因此,我们可以使用 .* 简化正则表达式的书写。
贪婪与非贪婪
我们先看一个例子:
1 | import re |
我们只得到了 7 这个数字,这里就涉及一个贪婪匹配与非贪婪匹配的问题了。
在贪婪匹配下,. 会匹配尽可能多的字符。正则表达式中. 后面是 \d+,也就是至少一个数字,并没有指定具体多少个数字,因此,.* 就尽可能匹配多的字符,这里就把 123456 匹配了,给 \d + 留下一个可满足条件的数字 7,最后得到的内容就只有数字 7 了。
所以这里需要非贪婪匹配,非贪婪匹配的写法是 .*?
1 | import re |
此时就可以成功获取 1234567 了。原因可想而知,贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。
修饰符
正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。
我们在字符串中增加换行符
1 | import re |
运行报错,也就是说正则表达式没有匹配到这个字符串。这是因为匹配的是除换行符之外的任意字符,当遇到换行符时,.*? 就不能匹配了,所以导致匹配失败。这里只需加一个修饰符 re.S
,即可修正这个错误:
1 | result = re.match('^He.*?(\d+).*?Demo$', content, re.S) |
这个修饰符的作用是匹配包括换行符在内的所有字符
下面列出所有的修饰符
修饰符 | 描述 |
---|---|
re.I | 使匹配对大小写不敏感 |
re.L | 做本地化识别(locale-aware)匹配 |
re.M | 多行匹配,影响 ^ 和 $ |
re.S | 匹配包括换行在内的所有字符 |
re.U | 根据 Unicode 字符集解析字符。这个标志影响 \w、\W、\b 和 \B |
re.X | 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解 |
在网页匹配中,较为常用的有 re.S 和 re.I。
转义匹配
举个例子,如果我们要匹配下面的文本,该怎么做
(百度) www.baidu.com
注意到该文本里面有().等特殊字符,如果直接匹配势必会出现错误。当遇到用于正则匹配模式的特殊字符时,在前面加反斜线转义一下即可。
1 | import re |
search
match 方法是从字符串的开头开始匹配的,一旦开头不匹配,那么整个匹配就失败了。
因为 match 方法在使用时需要考虑到开头的内容,这在做匹配时并不方便。它更适合用来检测某个字符串是否符合某个正则表达式的规则。
这里就有另外一个方法 search,它在匹配时会扫描整个字符串,然后返回第一个成功匹配的结果。也就是说,正则表达式可以是字符串的一部分,在匹配时,search 方法会依次扫描字符串,直到找到第一个符合规则的字符串,然后返回匹配内容,如果搜索完了还没有找到,就返回 None。
1 | import re |
这时就得到了匹配结果。
因此,为了匹配方便,我们可以尽量使用 search 方法。
findall
前面我们介绍了 search 方法的用法,它可以返回匹配正则表达式的第一个内容,但是如果想要获取匹配正则表达式的所有内容,那该怎么办呢?这时就要借助 findall 方法了。该方法会搜索整个字符串,然后返回匹配正则表达式的所有内容。
1 | import re |
可以看到,返回的列表中的每个元素都是元组类型,我们用对应的索引依次取出即可。
如果只是获取第一个内容,可以用 search 方法。当需要提取多个内容时,可以用 findall 方法。
finditer
finditer匹配字符串中所有的内容,返回迭代器
1 | import re |