正则表达式 详解

前言

        几乎在所有的语言和平台上都可以通过正则表达式(简称: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的归纳总结,在实际编程中用途广泛,希望上述文章能对您有所帮助,如有错误或建议欢迎各位指评交流~ 

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值