在上一节Python 网络爬虫从0到1 (4):Beautiful Soup 4库入门详解中,我们已经能够从由Requests库发起请求得到的HTML响应报文主体中解析页面标签树并能遍历、搜索以得到需要的精确信息。但同时我们也发现了一个问题:标签搜索
find()
类函数中,只有搜索条件精确匹配,搜索函数才能搜索到需要的内容。但是,在实际应用中,我们常常会遇到一类情况,我们需要获取某一类具有共同特征的一组标签数据,但其各属性并不完全相同(但类似)。这时候,传统的完全匹配条件无法满足需要,这就需要使用正则表达式进行表达式匹配。
准备知识
正则表达式概述
正则表达式的匹配模式主要是为了拓展常规搜索的局限。由于常规搜索要求提供与搜索结果完全匹配的条件,即使足够用于静态文本匹配置换,但其还不能轻松胜任匹配某一类型的动态文本的工作。这时候就需要使用模式匹配技术。继通配符后,功能更加强大的正则表达式的出现,为我们大大简化了字符串匹配的复杂性。正则表达式的中心是一个匹配模式(pattern
),模式中有普通字符与限定符,所有的非完全匹配都有限定符支撑。
具体的匹配模式将在下文介绍
Python中的正则表达式
Python中的正则表达式功能通过一个标准库re
实现,核心的匹配模式与标准正则表达式相同,但其他语法由于语言间差异而有所变动。re
库主要提供如下方法/函数,
方法\函数 | 说明 |
---|---|
re.match() | 从字符串头开始匹配模式,如果没有匹配,返回空 |
re.search() | 从全文搜索并返回第一个匹配子串(没有re.find()方法) |
re.sub() | 检索并替换匹配子串 |
re.complie() | 生成一个正则表达式对象实例,实例方法与类相同(不需要重新指定模式) |
re.findall() | 全文搜索并找到所有匹配的子串,返回一个列表 |
re.finditer() | 全文搜索并找到所有匹配的子串,返回一个可迭代对象 |
re.split() | 按全文中符合模式的子串作为分隔符分割字符串,返回一个列表 |
re
库同时提供了修饰符,用来设定匹配的特定条件,这些修饰符分别对应一个整形常量,参数名flag
,通常在pattern
和string
后面。当需要两个及以上修饰符时,可将这些修饰符进行与运算(或将其代表的数字直接相加(由于修饰符实际上是一个二进制位开关,其数值都为2的整数次幂)),使用re.<key name>.value
查看对应的数字(key name
为修饰符名称)
修饰符 | 说明 | 代表数字 |
---|---|---|
re.I | 匹配对大小写不敏感 | 2 |
re.L | 本地化识别匹配(编码字符集相关) | 4 |
re.M | 多行匹配,影响^与$ | 8 |
re.S | 使.匹配包括换行符在内的所有字符 | 16 |
re.U | 与L对应,使用Unicode字符集识别 | 32 |
re.X | 使能够以更灵活的格式编写以提高阅读性 | 64 |
有关详细的re库以及正则表达式使用方法,我们将在下面介绍。
正则表达式使用
由于正则表达式(匹配模式pattern
)是整个re库的核心部分,缺少正则表达式,re
库就缺少了灵魂,甚至不能进行一个普通的样例实验。所以我们就先来学习一下它的语法格式。
正则表达式的限定符
正则表达式主要由普通字符与限定符(也叫匹配模式)构成。普通字符用于限定具体的文本内容,限定符则用于限定这些具体内容的组织结构。限定符及其具体指代内容如下,
限定符 | 说明 | 样例 |
---|---|---|
^ | 匹配行首,在正则表达式最前面,表示以后面的模式开头 | ^123 匹配123开头的字符串 |
$ | 匹配行尾,在正则表达式最后面,表示以前面的模式结尾 | 123$ 匹配123结尾的字符串 |
. | 一般情况下匹配除了换行符外的任意一个字符(不能为空) | a.c 匹配abc,adc等 |
[] | 匹配方括号中的任意一个字符 | a[123]c 匹配a1c, a2c, a3c |
[^] | 字符写在^后面,不匹配方括号中的任意一个字符 | [^123] 匹配不是1,2,3的一个字符 |
- (方括号中) | 表示一个区间 | [0-9] 匹配任意一个数字 |
* | 匹配其前面一个字符的0次及以上出现 | ur*a 表示ua, ura, urra等 |
+ | 匹配其前面一个字符的1次及以上出现 | ur+a 表示ura, urra, urrrra 等 |
? | 匹配其前面一个字符0或1次出现 | abc? 表示ab, abc |
{n} | 匹配前面一个字符n次出现(精确) | ‘a{3}b’ 表示aaab |
{n,m} | 匹配前面一个字符出现n到m次(两边包括) | ‘a{1,3}b’ 表示ab, aab, aaab |
| | 匹配其两侧表达式任意一个,整体用小括号包含 | rub(e|y) 表示rube, ruby |
() | 可以将其中内容作为一个整体参与后面处理中 | (u+r+a+){3} 表示乌拉(?)三次 |
{n,m}
的特殊用法:如果n,m中有一个为空,则表示该方向上不受限制(最小仍是0个,最大无穷)。比如,a{3,}
表示a
至少出现三次,a{,10}
表示a
至多出现10次
正则表达式的特殊符号
另外,正则表达式还提供一些通过\
转义的特殊符号,提供某些特殊用法
特殊符号 | 说明 |
---|---|
\w |
匹配一个数字字母或下划线 |
\W |
匹配一个非数字字母下划线 |
\s |
匹配任意一个空白符(Tab \t,space,return \r,newline \n, form feed \f换页符)。\r\n即为windows回车; \n unix\linux回车; \r MacOS 回车 |
\S |
匹配任意非空字符(与\s 相反) |
\d |
匹配任意数字,相当于[0-9] |
\D |
匹配任意非数字 |
\A |
匹配字符串开始 |
\Z |
匹配字符串结束 |
\b |
匹配单词边界,即单词与空格相接的地方(如 ‘er\b’,能匹配’programmer’,但不能匹配’experience of’) |
\B |
匹配非单词边界 |
\b \t |
见\s 说明 |
\u.... |
\u 后加入一个字符的UTF-8编码,代表这个UTF-8编码的对应字符,例如中文匹配[\u4e00-\u9fa5] |
\n | n为数字,表示第n组内容(在sub 字符串匹配替换的时候需要) |
正则表达式的greedy \ lazy模式
正则表达式中的限定符*
、+
、?
、{}
都是greedy
(贪婪的),它们总是尽可能地多匹配字符,但某些时候我们并不需要这样,这时候在这些限定符后加上?
修饰符变为*?
、+?
、??
、{}?
,可以将其变为lazy
(懒惰的),它们尽可能少地匹配字符。
以下示例将帮助更快地理解两个模式的区别
- 字符串
'abb'
,模式'ab?b'
将匹配abb
(优先匹配出现一次),而模式ab??b
将匹配ab
(优先匹配出现零次) - 字符串
<p>Hello</p>
,模式<.+>
将匹配<p>Hello</p>
(整个字符串可完全匹配,尽量多),模式<.+?>
将匹配<p>
(尽量少) - 字符串
aaaaaaa
,模式a{3,5}
将匹配aaaaa
(五个,尽量多),模式a{3,5}?
将匹配aaa
(三个,尽量少)
正则表达式进阶样例
普通样例在介绍限定符的时候已经给出,这里不再赘述,下面介绍一些由多个限定符组合而成的较复杂样例
- 国内邮编(第一位1-9,共6位):
[1-9]\d{5}
- 匹配带有
er
(任何位置,此处er仅举例)的单词(整体):\b.*?er.*?\b
- 匹配末尾是
er
的单词:\b.*?er\b
- 匹配一个IPv4地址(每一段都为0-255,对于255以下的限制需要进行分类讨论):
(([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5]).){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])
Python 正则表达式(re)标准库的使用
Python中的正则表达式库使用原生字符串raw string
类型表示正则表达式,表示为r'pattern'
,例如r'\b.*?er.*?\b'
有关正则表达式标准库的功能在上面准备知识
中已经给出,下面逐个介绍重要方法。
compile
compile函数将一个原生字符串编译称为一个正则表达式pattern
对象实例,这个实例将可以使用find()
、match()
等re库提供的正则表达式方法。通过编译的pattern
对象在使用对应方法时,将不需要再次输入re.
类方法中的第一个参数pattern
。这对于同一个模式需要匹配多次的程序来说是一大优化。
a = re.compile(pattern, flag=0)
其中,
- pattern: 要编译的原生字符串
- flag: 正则表达式编译的修饰符,具体见上
准备知识
,多个修饰符连用可用|
或逻辑运算或将这几个修饰符代表的数字相加
例如:
test = re.compile(r'(([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])')
match
match函数将尝试从字符串的起始位置匹配一个模式,如果没有成功,则将返回None
b = re.match(pattern, string, flags=0)
b = a.match(string)
以上两行代码功能相同,第二行需要先进行complie
函数的编译以生成pattern
对象a
如果match
函数匹配成功,则会返回一个match
对象(re.Match
),Match
对象提供如下方法(属性)
方法/属性 | 说明 |
---|---|
.group() | 返回匹配到的字符串内容 |
.start() | 返回匹配到字符串的开头位置 |
.end() | 返回匹配到字符串的末尾位置 |
.span() | 返回匹配到字符串的开头结尾,以元组形式 |
- 如果在
group
后加上非0参数n(加0与不加相同),将返回第n组匹配到的结果(组是指小括号,一对括号看左括号,越靠左组号越小)如果遇到一组重复匹配则仅给出最后一轮匹配结果 - 使用
.groups()
函数输出一个所有组的元组
例如:
test = re.compile(r'(([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])')
a = test.match('192.168.0.1')
print(a.groups())
#以下是返回结果---
('0.', '0', '1')
search
search函数将从任何位置开始尝试匹配模式,如果匹配到,同样返回一个match
对象,除了开始匹配的位置不同外,其余与re.match
函数完全相同,不再赘述。
fullmatch
尝试用整个字符串匹配模式,如果匹配,返回一个match
对象,如果不匹配则返回None
,使用方法同上。
split
re.split(pattern, string, maxsplit=0, flags=0)
a.split(string, maxsplit=0)
split
函数将字符串以匹配模式为界进行分割,将返回一个包含各部分的列表
- 如果将
pattern
用小括号包含,那么得到的结果中也将包含这些匹配成功的内容,否则仅作为分界符而不包含 maxsplit
指定最大分割次数(1次分割为两段),re
会从前向后搜索字符串,只分割前面n次,后面内容会整段输出(不再进行匹配尝试),0次表示不限制分段次数- 如果一个字符串的末尾与模式匹配,则在结果的最后将会带有一个空字符串,开头匹配则在前面有。
sub
re.sub(pattern, repl, string, count=0, flags=0)
a.sub(repl, string, count=0)
sub
函数将返回一个字符串,这个字符串将所有匹配的内容进行了替换(将替换为repl
中的内容)。所有识别都将在不重叠的情况下依次进行(每匹配成功依次执行并替换一次)
如果repl
是一个字符串,则其中的所有转义特殊字符都能被识别,否则将被按原样放置。
repl
可以是一个函数,这个函数接收一个match
对象(即match()
函数返回值),对其进行处理并返回一个字符串(作为替换字符串)
- 参数
count
表示最大替换次数,如果为0,则不进行限制。
例如,(以下两个实例)
# example 1
test = re.compile(r'\d+')
print(test.sub('test', '123_Hello_456'))
#-----------
test_Hello_test
# example 2
def dashrepl(matchobj):
if matchobj.group(0) == '-': return ' '
else: return '-'
re.sub('-{1,2}', dashrepl, 'pro----gram-files')
#--------------
'pro--gram files'