一、正则表达式

首先了解一下什么是正则表达式,正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。总结来说,就是一种用特使符号规则来匹配对应内容的方法,下面我们开始学习这种方法。

1.1 学在前面

在我们正式学习正则表达式的使用之前,我们需要了解一个网站:在线正则表达式测试网站。这是由开源中国提供的一个在线测试正则表达式的网站,可以用来学习和测试。

1.2 简单使用

用法就很简单了,在输入区域输入匹配内容,可以自己写正则表达式也可以使用常见的已经定义好的正则表达式,执行以后就可以在下面看到输出结果,可以通过结果确认自己写的正则表达式是否符合要求。

有时候自己的项目中懒得写或者不会写那些常见正则表达式时可以直接到这里来复制粘贴。
在这里插入图片描述

1.3 简单例子

这里我们以 URL 匹配为例,简单介绍一下正则表达式的使用。

首先, URL的组成格式则是协议类型 + 冒号加双斜线 + 域名和路径。我们可以看到对应的正则表达式是:

[a-zA-z]+://[^\s]*    //a-z 匹配任意的小写字母,\s 匹配任意的空白字符,* 匹配前面任意多个字符。

由此可见,一种业务的正则表达式其实是由多种匹配规则组成的,所以想看懂正则表达式或者梳理使用它,了解相关匹配规则就很重要了。

1.4 常用的匹配规则

单看这些规则对于初学者来说有点云里雾里的,不过没关系,了解就行,具体的使用方法会随着实例的学习慢慢掌握。

模 式描 述
\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 | b匹配 a 或 b
()匹配括号内的表达式,也表示一个组

二、Python 中使用正则表达式

正则表达式是应用于多种语言的,本系列的文章讲的是 Python中 的爬虫技术,故使用 Python 中的实例进行说明。

2.1 了解 re 库

Python 的 re 库提供了整个正则表达式的实现,利用这个库,可以在 Python 中使用正则表达式。

// 引用 re
import re

re 库采用raw string(原生字符串类型)表达正则表达式,表示为r’text’,raw string是不包含转义符的字符串,如 r’[1-9]\d{5}’、r’\d{3}-\d{8}|\d{4}-\d{7}’ 。

string类型更繁琐,需要对一些特殊符号进行转义,如 ‘[1-9]\\d{5}’、’\\d{3}-\\d{8}|\\d{4}-\\d{7}’。

备注:可能没明白上面的区别,简单来说就是因为正则表达式中含有一些特殊字符,正常情况下需要转义,可以使用后面介绍的 compile 函数解决。

Re库的主要功能函数:

函数名用途
re.compile()将正则表达式的字符串形式编译为一个 Pattern 对象
re.search()从一个字符串中搜索匹配正则表达式的第一个位置,返回match对象
re.match()从一个字符串的开始位置起匹配正则表达式,返回match对象
re.findall()搜索字符串,以列表类型返回全部能匹配的子串
re.split()将一个字符串按照正则表达式匹配结果进行分割,返回列表类型
re.finditer()搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象
re.sub()在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串

re 模块的一般使用步骤如下:
1、使用 compile() 函数将正则表达式的字符串形式编译为一个 Pattern 对象;
2、通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果,一个 Match 对象;
3、最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作。

2.2 compile 函数

使用 compile() 函数可以将正则表达式的字符串形式编译为一个 Pattern 对象,方便使用。

import re

pattern = re.compile(r'\d+')

2.3 match 函数

match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果。它的一般使用形式:match(string[, pos[, endpos]]),如果匹配,就返回匹配成功的结果;如果不匹配,就返回 None。

//代码示例
import re

str1='abcd1234ABCD!@#$ abcd'
pattern = re.compile(r'\d+') #匹配至少一个数字
print('匹配内容'+str1)

result1 = pattern.match(str1) # 从头开始匹配
print('从内容开始匹配数字结果:')
print(result1)

result2 = pattern.match(str1,2,5) # 从第3位开始匹配
print('从第3位开始匹配数字结果:')
print(result2)

result3 = pattern.match(str1,5,10) # 从第6位开始匹配
print('从第6位开始匹配数字结果:')
print(result3)

print(result3.group()) #获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 group()group(0)print(result3.start()) #获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0print(result3.end()) #获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0print(result3.span()) #返回 (start(group), end(group)).

pattern1 = re.compile(r'([a-z]+) ([a-z]+)', re.I)  # re.I 表示忽略大小写
result4 = pattern1.match('abcd ABCD')
print(result4)

#效果演示,这里不做详细解释,建议根据结果自己揣摩,加深学习效果。
print(result4.group())
print(result4.group(1))
print(result4.span(1))
print(result4.group(2))
print(result4.span(2))
print(result4.groups())

输出结果

//输出结果

匹配内容abcd1234ABCD!@#$ abcd
从内容开始匹配数字结果:
None
从内容开始匹配数字结果:
None
从内容开始匹配数字结果:
<re.Match object; span=(5, 8), match='234'>
234
5
8
(5, 8)
<re.Match object; span=(0, 9), match='abcd ABCD'>
abcd ABCD
abcd
(0, 4)
ABCD
(5, 9)
('abcd', 'ABCD')

2.4 search 函数

search 函数与 match 函数的用法有些类似,这里用一些例子来说明一下两者的却别。

import re

str1='abcd1234ABCD!@#$ abcd'
pattern = re.compile(r'\d+') #匹配至少一个数字
print('匹配内容:'+str1)

result1 = pattern.search(str1) # 从头开始匹配
print('search 函数匹配结果:')
print(result1)
result2 = pattern.match(str1) # 从头开始匹配
print('match 函数匹配结果:')
print(result2)

查看输出结果:

匹配内容:abcd1234ABCD!@#$ abcd
search 函数匹配结果:
<re.Match object; span=(4, 8), match='1234'>
match 函数匹配结果:
None

可以很明显的看出 search 函数匹配出了结果,而 match 函数没有,原因就是因为 search 函数是从字符串的开头开始匹配,直到字符串结束,这个过程中匹配到了就返回结果(只匹配一次结果),没有就返回 None。 match 函数只会按照你指定开始匹配的位置,只匹配一次,默认从头开始匹配。

如果从身份证号中匹配出生日信息可以用 match 函数,如果从一段文字中匹配电话号码可以用 search 函数。

这里需要注意的是,search 函数同样只匹配一次。

import re

str1='abcd1234 5678'
pattern = re.compile(r'\d+') #匹配至少一个数字
print('匹配内容:'+str1)

result1 = pattern.search(str1) # 从头开始匹配
print('search 函数匹配结果:')
print(result1)

=======输出结果=========
匹配内容:abcd1234 5678
search 函数匹配结果:
<re.Match object; span=(4, 8), match='1234'> #只匹配到了第一组数字

2.5 findall 函数

match 和 search 函数都是一次匹配,找到了一个匹配的结果就返回。然而在大多数场景下,我们需要搜索整个字符串,获得所有匹配的结果,所以需要使用 findall 函数。

import re

str1='abcd1234 5678'
pattern = re.compile(r'\d+') #匹配至少一个数字
print('匹配内容:'+str1)

result1 = pattern.findall(str1) # 从头开始匹配
result2 = pattern.findall(str1,6,10)
print('findall 函数匹配结果:')
print(result1)
print(result2)

========输出结果=========
匹配内容:abcd1234 5678
findall 函数匹配结果:
['1234', '5678']
['34', '5']

从上面的例子,可以看出 findall 函数将所有符合的结果都匹配到了,并且可以选择匹配位置。

2.6 finditer 函数

finditer 函数的行为跟 findall 的效果类似,也是搜索整个字符串,获得所有匹配的结果。

但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器。

import re

str1='abcd1234 5678'
pattern = re.compile(r'\d+') #匹配至少一个数字

result1 = pattern.finditer(str1)
for item in result1:
	print(item)

========输出结果=========
<re.Match object; span=(4, 8), match='1234'>
<re.Match object; span=(9, 13), match='5678'>

2.7 split 函数

split 函数的主要作用是分割。

函数使用方式为:split(string[, maxsplit]) ,中,maxsplit 用于指定最大分割次数,不指定将全部分割。

import re

pattern  = re.compile(r'[\s\,\;\.]+')
print (pattern .split('a,b;..c  d'))

========输出结果=========
['a', 'b', 'c', 'd']

2.8 sub 函数

sub 方法用于替换。

函数使用方式为:sub(repl, string[, count]) 。

repl 可以是字符串也可以是一个函数:

如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 id 的形式来引用分组,但不能使用编号 0;

如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。

count 用于指定最多替换次数,不指定时全部替换。

import re

pattern = re.compile(r'(\w+) (\w+)') 
s = 'hello Yao, hello Ming'
print ('-------')
print (pattern.sub(r'hello Python', s)) 
print ('-------')
print (pattern.sub(r'\1 \2', s)) # 引用分组
print (pattern.sub(r'\2 \1', s)) # 引用分组 倒序
print ('-------')
def funa(m):
	print(m)
	return '你好'+ ' ' + m.group(2)
print ('-------')
print (pattern.sub(funa, s)) #多次sub,每次sub的结果传递给funa
print ('-------')
print (pattern.sub(funa, s, 1)) # 替换一次
print ('-------')

========输出结果=========
hello Python, hello Python
-------
hello Yao, hello Ming
Yao hello, Ming hello
-------
-------
<re.Match object; span=(0, 9), match='hello Yao'>
<re.Match object; span=(11, 21), match='hello Ming'>
hi Yao, hi Ming
-------
<re.Match object; span=(0, 9), match='hello Yao'>
hi Yao, hello Ming
-------

三、补充内容

3.1 贪婪与非贪婪

使用通用匹配 .* 时,有时候匹配到的并不是我们想要的结果。

如下这个示例,希望获取的是字符串中间的数字部分。

import re

content = 'hello Yao 12345 666 999 hello Ming'
result = re.match('^he.*(\d+).*Ming$', content)
print(result)
print(result.group())
print(result.group(1))

========输出结果=========
<re.Match object; span=(0, 34), match='hello Yao 12345 666 999 hello Ming'>
hello Yao 12345 666 999 hello Ming
9

我们使用的是 match 函数 ,理论伤应该输出的结果是 12345,这里却匹配的是 9 ,这就是贪婪匹配的原因。

在贪婪匹配下,.* 会匹配尽可能多的字符。正则表达式中 .* 后面是 \d+,也就是至少一个数字,并没有指定具体多少个数字,因此,.* 就尽可能匹配多的字符,这里就把 12345 666 99 匹配了,给 \d+ 留下一个可满足条件的数字 9,最后得到的内容就只有数字 9 了。

所以我们使用非贪婪匹配试一下:

import re

content = 'hello Yao 12345 666 999 hello Ming'
result = re.match('^he.*?(\d+).*Ming$', content)
print(result)
print(result.group())
print(result.group(1))

========输出结果=========
<re.Match object; span=(0, 34), match='hello Yao 12345 666 999 hello Ming'>
hello Yao 12345 666 999 hello Ming
12345

此时成功获取 12345 了。因为贪婪匹配是尽可能匹配多的字符,非贪婪匹配就是尽可能匹配少的字符。当 .*? 匹配到 hello 后面的空白字符时,再往后的字符就是数字了,而 \d+ 恰好可以匹配,那么 .*? 就不再进行匹配,交给 \d+ 去匹配后面的数字。这样 .*? 匹配了尽可能少的字符,\d+ 的结果就是 12345 了。

所以,在做匹配的时候,字符串中间尽量使用非贪婪匹配,也就是用 .*? 来代替 .*,以免出现匹配结果缺失的情况。

但需要注意的是,如果匹配的结果在字符串结尾,.*? 就有可能匹配不到任何内容了,因为它会匹配尽可能少的字符

3.2 修饰符

正则表达式可以包含一些可选标志修饰符来控制匹配的模式。修饰符被指定为一个可选的标志。

还是上面的例子,但是中间有换行,看一下执行的结果。

import re

content = '''hello Yao 12345 666 999 
hello Ming'''
result = re.match('^he.*?(\d+).*Ming$', content)
print(result)
print(result.group(0))
print(result.group(1))

========输出结果=========
执行会报错,我的环境直接报了匹配为无,所以 group 没有内容,抛出异常。
None
Traceback (most recent call last):
  File "RE_2.py", line 8, in <module>
    print(result.group(0))
AttributeError: 'NoneType' object has no attribute 'group'

这是因为我们匹配的是除换行符之外的任意字符,当遇到换行符时,.*? 就不能匹配了,导致匹配失败。
这里只需加一个修饰符 re.S,即可修正这个错误:

import re

content = '''hello Yao 12345 666 999 
hello Ming'''
result = re.match('^he.*?(\d+).*Ming$', content,re.S)
print(result)
print(result.group(0))
print(result.group(1))

========输出结果=========(正常输出)
<re.Match object; span=(0, 35), match='hello Yao 12345 666 999 \nhello Ming'>
hello Yao 12345 666 999 
hello Ming
12345

除此外,还有一些其他修饰符:

修饰符描述
re.I使匹配对大小写不敏感
re.L做本地化识别(locale-aware)匹配
re.M多行匹配,影响 ^ 和 $
re.S使匹配包括换行在内的所有字符
re.U根据 Unicode 字符集解析字符。这个标志影响 \w、\W、\b 和 \B
re.X该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

3.3 转义匹配

我们知道正则表达式定义了许多匹配模式,如匹配除换行符以外的任意字符,但如果目标字符串里面就包含 .,那该怎么办呢?

这里就需要用到转义匹配了,示例如下:

import re

content = '(百度) www.baidu.com'
result = re.match('\(百度 \) www\.baidu\.com', content)
print(result)

四、其他

上述内容为 Python 中关于正则表达的基础知识部分,都是通俗易懂的内容,这里没有过多的介绍网页爬取的实战内容,后面时间充足会在此章补充大量示例,为大家的学习提供参考。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐