前言
几乎在所有的语言和平台上都可以通过正则表达式(简称:regex)来执行各种复杂的文本处理和操作,它是处理字符串的强大工具,在网络爬虫中,也有许多提取html中数据的工具,例如:XPATH、Beautiful Soup、pyquery,相较于这些regex无疑是看起来最复杂的,并不如其他工具那么直观,让人望而却步,但regex的用处是更为广泛的,理解后其实并没有那么复杂,现对此结合python实例加以理解分析。
常用的匹配规则总结
模式 | 描述 |
\w | 匹配字母、数字及下划线 |
\W | 匹配不是字母、数字及下划线的字符 |
\s | 匹配任意空白字符 |
\S | 匹配任意非空字符 |
\d | 匹配任意数字 |
\D | 匹配任意非数字字符 |
\A | 匹配字符串开头 |
\Z | 匹配字符串结尾。如果存在换行,只匹配到换行前的结束字符串 |
\z | 匹配字符串结尾,如果存在换行,同时还会匹配换行符 |
\G | 匹配最后匹配完成的位置 |
\n | 匹配一个换行符 |
\t | 匹配一个制表符 |
^ | 匹配一行字符串的开头 |
$ | 匹配一行字符串的结尾 |
. | 匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符 |
[...] | 用来表示一组字符,单独列出 |
[^...] | 匹配不在 [...] 中的字符 |
* | 匹配0个或多个表达式 |
+ | 匹配1个或多个表达式 |
? | 匹配0个或1个前面的表达式定义的片断,非贪婪模式 |
{n} | 精确匹配n个前面的表达式 |
{n,m} | 匹配n到m次由前面正则表达式定义的片断,贪婪模式 |
a|b | 匹配a或b |
() | 匹配括号内的表达式,也表示一个组 |
Python中的正则表达式
python通过re模块提供了正则表达式的支持:
import re
python支持下列正则表达式函数:
正则表达式函数 | 功能 |
prep_grep() | 执行搜索并以数组形式返回匹配结果 |
findall() | 查找所有子串并以列表形式将其返回 |
finditer() | 查找所有子串并以迭代器形式将其返回 |
match() | 在字符串的开头执行正则表达式搜索 |
search() | 搜索字符串中的所有匹配项 |
splite() | 将字符串转换成列表,在模式匹配的地方将其分割 |
sub() | 用指定的子串替换匹配项 |
subn() | 返回一个字符串,其中匹配项被指定的子串替换 |
compile() | 预编译正则表达式,生成一个正则表达式( Pattern )对象,match() 和 search() 可以直接调用预编译的正则表达式使用 |
正则表达式
1. 匹配单个字符串
1.1 匹配普通文本
import re
text = 'hello world, Yy_Rose'
result = re.findall('Rose', text)
print('匹配到的结果是:', result) # 匹配到的结果是: ['Rose']
这里首先声明了一个字符串,re.findall() 函数中 ‘Rose’是一个普通文本,它也算是一个正则表达式,在正则表达式中可以包含普通文本,甚至可以只包含普通文本, 以上成功匹配到了原始文本中 Rose 字符串。
注意:在正则表达式中是区分字母大小写的,如下则只能匹配到大写的 Y :
import re
text = 'hello world, Yy_Rose'
result = re.findall('Y', text)
print('匹配到的结果是:', result) # 匹配到的结果是: ['Y']
1.2 匹配任意字符
. 字符可以匹配任意单个字符、字母、数字、以及 . 字符本身:
import re
text = 'python, php, regex.py'
result1 = re.findall('p.', text) # 匹配 p后面一个字符
result2 = re.findall('.h.', text) # 匹配 h前后各一个字符
result3 = re.findall('regex.', text) # . 字符可以匹配到本身
print('匹配到的结果是:', result1) # 匹配到的结果是: ['py', 'ph', 'p,', 'py']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['tho', 'php']
print('匹配到的结果是:', result3) # 匹配到的结果是: ['regex.']
1.3 匹配特殊字符
. 字符在正则表达式中有特殊含义,若只输入一个 . 匹配结果如下:
text = 'python.py'
result1 = re.findall('.', text)
print('匹配到的结果是:', result1) # 匹配到的结果是: ['p', 'y', 't', 'h', 'o', 'n', '.', 'p', 'y']
这里通过 re.findall 方法匹配到了所有满足条件的单个字符,若只想获取到 . 字符本身而不是它在正则表达式中的特殊含义,则需要在 . 字符的前面加上一个 \ (反斜杠)字符来对它进行转义。\ 是一个元字符,代表这个字符有特殊含义,而不是字符本身。
综上所述:单独一个 . 字符表示匹配任意单个字符,而 \. 表示匹配 . 字符本身:
import re
text = 'python.py'
result1 = re.findall('\.', text)
print('匹配到的结果是:', result1) # 匹配到的结果是: ['.']
2. 匹配一组字符
2.1 匹配多个字符中的某一个
在正则表达式里可以使用元字符 [ 和 ] 来定义一个字符集和,在使用 [ 和 ] 定义的字符集合里,出现在 [ 和 ] 之间的所有字符都是该集合的组成部分,必须匹配其中的某个成员(但并非全部):
import re
text = 'python Yy_Rose system'
result1 = re.findall('.y', text) # 组成部分:匹配 y之前的单个字符 和 y
result2 = re.findall('[ps]y', text) # 组成部分:匹配 p或s 和 y
print('匹配到的结果是:', result1) # 匹配到的结果是: ['py', 'Yy', 'sy']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['py', 'sy']
2.2 利用字符集合区间
在使用正则表达式时会频繁的使用到一些正则表达式区间,例如 [0123456789] 表示匹配0到9这个字符集合,为了简化字符区间的定义,正则表达式中使用 - 连字符来定义区间,以上则可写为[0-9] ,例如:
import re
text = 'Rose1 RoseY'
result1 = re.findall('Rose.', text)
result2 = re.findall('Rose[0-9]', text) # 匹配Rose后的数字字符
print('匹配到的结果是:', result1) # 匹配到的结果是: ['Rose1', 'RoseY']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['Rose1']
字符区间并不仅限于数字,常用的还有字母区间:
A-Z | 匹配从A到Z的所有大写字母 |
a-z | 匹配从a到z的所有小写字母 |
- (连字符)是一个特殊的元字符,它只有出现在 [ 和 ] 之间的时候才是元字符,在字符集合以外的地方,- 只是一个普通字符,只能与 - 本身匹配,所以在正则表达式中 - 字符不需要被转义。
在同一个字符集合里可以给出多个字符区间,例如:[0-9A-Za-z] 可以匹配任何一个字母(无论大小写)或数字。
2.3 排除指定字符
字符集合不仅可以用来匹配符合定义的字符,也可以用来排除字符集合里指定的那些字符,通过元字符 ^ 来排除某个字符集合,将上例更改,得到不同的匹配结果:
import re
text = 'Rose1 RoseY'
result1 = re.findall('Rose.', text)
result2 = re.findall('Rose[^0-9]', text) # 匹配Rose后不是数字的字符
print('匹配到的结果是:', result1) # 匹配到的结果是: ['Rose1', 'RoseY']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['RoseY']
3. 使用元字符
3.1 元字符
元字符指在正则表达式中有特殊含义的字符,上述内容就介绍了几个元字符的用法,任何一个元字符都可以通过在前面加上一个反斜杠符号( \ )来进行转义,从而能匹配到元字符本身,例子可参考 1.3 内容。
3.2 匹配空白元字符
元字符大概可以分为两种:1.用来匹配文本的( . )
2.正则表达式语法的组成部分( [ 和 ] )
在通过正则表达式进行检索时,经常需要匹配非打印空白字符,例如 制表符或者换行符,在正则表达式中输入这类字符相对较为不便,以下列出一些特殊元字符:
元字符 | 说明 |
[\b] | 回退(并删除)一个字符 |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\v | 垂直制表符 |
\r、\n 都是对普通字符进行转义变为了空白元字符具有了特殊的含义,windows系统将\r\n用作文本行的结束标记。
3.3 匹配特定的字符类型
前文讲了通过字符集合来匹配一组字符中的某一个字符,可以通过一些特殊的元字符来替代字符集合,使用起来更为方便,接下来介绍几种特殊的元字符:
数字元字符 | 说明 |
\d | 任何一个数字字符( 等价于 [0-9] ) |
\D | 任何一个非数字字符( 等价于 [^0-9] ) |
字母数字元字符 | |
\w | 任何一个字母数字字符(大小写均可)或下划线字符( 等价于[ a-zA-Z0-9_ ] ) |
\W | 任何一个非字母数字或下划线字符( 等价于[ ^a-zA-Z0-9_ ] ) |
空白字符元字符 | |
\s | 任何一个空白字符( 等价于 [ \f\ n \r \t \v ] ) |
\S | 任何一个非空白字符( 等价于 [ ^\f\ n \r \t \v ] ) |
进制匹配 | |
\x | 匹配十六进制 |
\0 | 匹配八进制 |
POSIX字符类 | |
[:xdigit:] | 任何十六进制数字( 等价于[ a-fA-F0-9 ] ) |
[:alnum:] | 任何一个字母或数字( 等价于 [a-zA-Z0-9] ) |
[:alpha:] | 任何一个字母( 等价于 [ a-zA-Z ] ) |
[:upper:] | 任何一个大写字母( 等价于 [ a-z ] ) |
[:lower:] | 任何一个小写字母( 等价于 [ A-Z ] ) |
[:digit:] | 任何一个数字( 等价于 [ 0-9 ] ) |
[:blank:] | 任何一个制表符( 等价于 [ \t ] ) |
POSIX是一种特殊的标准字符类集,许多正则表达式实现都支持的一种简写形式,语法与之前见过的元字符不大一样,POSIX字符类必须在 [: 和 :] 之间,然后外层用 [ 和 ] 定义一个字符集合,例如 [[:xdigit:]] 。
4. 重复匹配
4.1 匹配一个或多个字符
要想匹配某个字符(或字符集合) 的一次或多次重复,只需要在后面加个 + 字符就行了,+ 字符匹配一个或多个字符(至少一个)。
注意:必须将+放在字符集合外面,如 [0-9]+就是匹配一个或多个数字,同时+是一个元字符,所以如果要匹配它本身需要进行转义。
import re
text = 'Yy_Rose'
result1 = re.findall('[a-zA-Z]+', text) # 匹配多个字母,+ 是贪婪匹配
print('匹配到的结果是:', result1) # 匹配到的结果是: ['Yy', 'Rose']
import re
text = 'python@qq.com'
result1 = re.findall('\w+@\w+\.\w+', text) # 匹配邮箱号
# 或者 \w+@[\w.]+\w+
print('匹配到的结果是:', result1) # 匹配到的结果是: ['python@qq.com']
4.2 匹配零个或多个字符
+ 字符只能匹配至少一个字符,若想匹配零个或多个字符,可以用 * 来实现,用法与+一样,放在某个字符或字符集合的后面,就能匹配该字符零次或多次的情况,* 也是一个元字符。
import re
text = 'python@qq.com'
result1 = re.findall('[\w.]*@[\w.]*\w*', text) # 匹配邮箱号
print('匹配到的结果是:', result1) # 匹配到的结果是: ['python@qq.com']
4.3 匹配零个或一个字符
?符号可以匹配零个或一个字符,至多一个:
import re
text = 'https://www.csdn.net'
result1 = re.findall('https?:\/\/[\w.\/]+', text) # 匹配http或https开头的URL地址
# 或者写为:http[s]?:\/\/[\w.\/]+
print('匹配到的结果是:', result1) # 匹配到的结果是: ['https://www.csdn.net']
4.4 重复匹配次数
以上的匹配方式匹配的字符数量要么就没有上限,要么就是零个或一个,无法确定具体的匹配次数,为了获取对重复匹配的控制权,正则表达式允许使用重复范围,重复范围在 { 和 } 之间指定,例如:
import re
text = 'Yy_Rose_Python'
result1 = re.match('[a-zA-Z_]{5}', text) # 重复匹配该字符5次
print('匹配到的结果是:', result1) # match='Yy_Ro'
也可以为重复匹配次数设置一个区间,语法格式为 {min, max} ,区间范围可以从零开始:
import re
text = '12345@qq.com 45@qq.com'
result1 = re.findall('[0-9]{3,5}@[\w.]+\w+', text) # 匹配开头为3到5个数字的邮箱地址
print('匹配到的结果是:', result1) # 匹配到的结果是: ['12345@qq.com']
4.5 防止过度匹配
上述 + 和 * 字符都是贪婪匹配的,所谓贪婪匹配指的是匹配的个数没有上限且多多益善,它们会尽可能的从文本开头匹配到文件末尾,而不是碰到第一个匹配的就停止,这样会导致过度匹配问题的出现:
import re
text = 'python 1989'
result1 = re.match('(.*)(\d+)', text) # 这里用到了子表达式,第六部分会讲到
print('匹配到的结果是:', result1.group(1)) # 匹配到的结果是: python 198
print('匹配到的结果是:', result1.group(2)) # 匹配到的结果是: 9
我们想要获取数字1989,结果却只匹配到了9,原因是 .* 会尽可能的匹配多个字符,而 \d+ 为至少匹配一个数字,并没有指定要匹配多少个,导致除了最后一个数字外全部匹配给了前面的,\d+ 则只匹配到了一个9。
若将这些贪婪型的元字符改写成懒惰型的元字符,则会变为尽可能少的匹配字符,改写方法是在其后加一个 ?字符:
贪婪型量词 | 懒惰型量词 |
* | *? |
+ | +? |
{n,} | {n,}? |
import re
text = 'python 1989'
result1 = re.match('(.*?)(\d+)', text)
print('匹配到的结果是:', result1.group(1)) # 匹配到的结果是: python
print('匹配到的结果是:', result1.group(2)) # 匹配到的结果是: 1989
# 这就成功匹配到想要的1989数字串了
5. 位置匹配
5.1 单词边界
边界是指一些用于指定模式前后位置的特殊元字符,而单词边界就是指单词和符号之间的边界,单词可以是中文字符,英文字符,数字,符号可以是中文符号,英文符号,空格,制表符,换行符等,例如你想匹配 py,而文本中还存在python,就会匹配到多余的字符串,这时候就需要设定边界来处理此类问题:
\b | 匹配单词边界 |
\B | 等价于 [ ^\b ],匹配非单词边界 |
import re
text = ' Y YTHON'
result1 = re.findall(r'\bY\b', text) # 匹配左右双边界
result2 = re.findall(r'\bY', text) # 匹配左边界
print('匹配到的结果是:', result1) # 匹配到的结果是: ['Y']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['Y', 'Y']
import re
text = ' python ' # python两边为空格
content = 'mpython ' # python左边为英文字符m
result1 = re.findall(r'\bpython\b', text)
result2 = re.findall(r'\bpython', content) # \b 匹配单词边界
result3 = re.findall(r'\Bpython', content) # \B 匹配非单词边界
print('匹配到的结果是:', result1) # 匹配到的结果是: ['python']
print('匹配到的结果是:', result2) # 匹配到的结果是: [] \b 前后不能都为单词
print('匹配到的结果是:', result3) # 匹配到的结果是: ['python']
\b 匹配的是一个位置,而不是任何实际的字符,使用 \bpython\b 匹配到的字符串的长度是5个·字符(python)而不是7个。
5.2 字符串边界
单词边界可以用来对单词位置进行匹配,字符串匹配有着类似的用途,只不过用于在字符串首尾进行模式匹配,字符串边界元字符有两个:^ 代表字符串的开头,$ 代表字符串的结尾。
import re
text = 'Yy_Rose'
result1 = re.findall(r'^Yy', text)
result2 = re.findall(r'se$', text)
result3 = re.findall(r'^yy', text)
result4 = re.findall(r's$', text)
print('匹配到的结果是:', result1) # 匹配到的结果是: ['Yy']
print('匹配到的结果是:', result2) # 匹配到的结果是: ['se']
print('匹配到的结果是:', result3) # 匹配到的结果是: [] 不存在yy开头则匹配不到
print('匹配到的结果是:', result4) # 匹配到的结果是: [] 不存在s结尾则匹配不到
6. 使用子表达式
6.1 子表达式的作用
前面说到 { 和 } 中可以填入需要重复匹配的次数,但是它的局限性在于其只作用于紧挨着它的前一个字符或者元字符,例如我们知道的html中常用的空格表示  ;若对其直接用之前的匹配方法: {3} 匹配到的并不是多个空格,而是 ;; ,只会将前面的分好匹配三次,{3}只是将紧挨着它的前一个字符匹配了三次,若想解决这个问题,就需要引入子表达式,用子表达式进行分组匹配 ,子表达式出现在( 和 )之间,可以写为( ){3} 将前面的划为一个整体。
6.2 使用子表达式
import re
text = 'Yy_Rose 2021'
result1 = re.match(r'(.*?)(\d+)', text) # 使用子表达式划为两组
print('匹配到的结果是:', result1.group(1)) # 匹配到的结果是: Yy_Rose
print('匹配到的结果是:', result1.group(2)) # 匹配到的结果是: 2021
import re
html = '<p>兴趣:<input type="text">围棋</p>'
result1 = re.search('<p>(.*?)<.*?>(.*?)</p>', html)
print('匹配到的结果是:', result1.group(1), result1.group(2)) # 匹配到的结果是: 兴趣: 围棋
7. 反向引用
7.1 为什么使用反向引用
html中网页标签有六级,<h1>到<h6>,如果想匹配到所有符合条件的标签,可以像下例:
import re
html = '''<h1>python</h1>
<h2>hello</h2>
<h3>world</h3>
'''
result1 = re.findall('<[Hh]\d>.*?<\/[Hh]\d>', html)
print('匹配到的结果是:', result1) # 匹配到的结果是: ['<h1>python</h1>', '<h2>hello</h2>', '<h3>world</h3>']
但是问题在于如果出现无效标题,例如 <h2>Yy_Rose</h3>,<h2> 开头 </h3> 结束,显然不匹配是无效的,但是上述方法依然会匹配出来:
import re
html = '''<h1>python</h1>
<h2>Yy_Rose</h3> # 标签开头结果不匹配
'''
result1 = re.findall('<[Hh]\d>.*?<\/[Hh]\d>', html)
print('匹配到的结果是:', result1) # 匹配到的结果是: ['<h1>python</h1>', '<h2>Yy_Rose</h3>']
7.2 使用反向引用
上述情况就需要使用到反向引用来确保匹配的准确性了:
import re
html1 = '<h1>Yy_Rose</h1>'
html2 = '<h2>python</h3>'
result1 = re.search(r'<([Hh]\d)>.*?</\1>', html1)
result2 = re.search(r'<([Hh]\d)>.*?</\1>', html2)
print('匹配到的结果是:', result1[0]) # 匹配到的结果是: <h1>Yy_Rose</h1>
print('匹配到的结果是:', result2) # 匹配到的结果是: None
正如看到的,子表达式是按照其相对位置来引用的:\1 对应着第一个子表达式,\2 对应着第二个子表达式。
反向引用只能用来引用括号里的子表达式,反向引用匹配通常从1开始计数(\1、\2等),在许多实现里,第0个匹配(\0)可以用来代表整个正则表达式。
反向引用存在一个严重不足:移动或编辑子表达式(子表达式的位置会因此改变)可能会使模式失败,删除或添加子表达式的后果会更严重,为了弥补这一不足,较新的正则表达式实现支持“命名捕获”:给某个子表达式起一个唯一的名称,随后用该名称(而不是相对位置)来引用这个子表达式,语法格式为:(?P<任意命名>匹配模式) 开始,(?P=所命名字) 结束,可参考下例:
import re
html = '<h1>Yy_Rose</h1>'
result = re.search(r'<(?P<Yy_Rose>[Hh]\d)>.*?</(?P=Yy_Rose)>', html)
print('匹配到的结果是:', result[0]) # 匹配到的结果是: <h1>Yy_Rose</h1>
“命名捕获”相关内容可参考:第11.17节 Python 正则表达式扩展功能:命名组功能及组的反向引用_老猿Python-CSDN博客
7.3 替换操作
import re
text = '2021/12/26'
print(text)
print(re.sub(r'(\w+)/(\w+)/(\w+)', r'\3-\1-\2', text))
# 2021/12/26
# 26-2021-12
8. 环视
8.1 向前查看
向前查看指定了一个必须匹配但不用在结果中返回的模式,向前查看其实就是一个子表达式,它的语法格式以 ?= 开头,需要匹配的文本在 = 的后面,即可匹配到其之前的全部内容,如下在使用向前查看时,正则表达式解析器将向前查看并处理 :匹配,但不会将其包括在最终的匹配结果里:
import re
text = 'https://www.csdn.net/'
result = re.match(r'.*(?=:)', text)
print("匹配到的结果为:", result[0]) # 匹配到的结果为: https
8.2 向后查看
向后查看与向前查看类似,向后查看的语法格式以 ?<= 开头,需要匹配的文本同样在 = 的后面:
import re
text = 'https://www.csdn.net/'
result = re.search(r'(?<=:).*', text)
print("匹配到的结果为:", result[0]) # 匹配到的结果为: //www.csdn.net/
8.3 否定式环视
向前查看和向后查看通常都是用来匹配文本,主要用于指定作为匹配结果返回的文本位置,这种用法被称为肯定式向前查看和肯定式向后查看,肯定式指的是执行的是匹配操作。
环视还有一种不常见的形式叫作否定式环视。否定式向前查看会向前查看不匹配指定模式的文本;否定式向后查看则向后查看不匹配指定模式的文本。
import re
text = '$20 21'
result1 = re.search(r'(?<=\$)\d+', text) # 匹配前面有$的数
result2 = re.search(r'\b(?<!\$)\d+', text) # 匹配前面没有$的数
print("匹配到的结果为:", result1[0]) # 匹配到的结果为: 20
print("匹配到的结果为:", result2[0]) # 匹配到的结果为: 21
环视类型 | 说明 |
(?=) | 肯定式向前查看 |
(?!) | 否定式向前查看 |
(?<=) | 肯定式向后查看 |
(?<!) | 否定式向后查看 |
9. 修饰符
修饰符 | 描述 |
re.I | 使匹配对大小写不敏感 |
re.L | 做本地化识别(local-aware)匹配 |
re.M | 多行匹配,映像 ^ 和 $ |
re.S | 使 . 匹配包括换行在内的所有字符(匹配 html 时经常会用到) |
re.U | 根据 Unicode 字符集解析字符,这个标志影响 \w、\W、\b、\B |
re.X | 该标志通过给予更灵活的格式以便将正则表达式写得更易于理解 |
具体可参考:正则表达式 – 修饰符(标记) | 菜鸟教程
总结
以上是对正则表达式结合python的归纳总结,在实际编程中用途广泛,希望上述文章能对您有所帮助,如有错误或建议欢迎各位指评交流~