1.元字符
元字符是构造正则表达式的一种基本元素,可以匹配一个或多个字符,或者根据特定的规则进行匹配。
元字符 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串结束 |
匹配有abc开头的字符串:
\babc或者^abc匹配8位数字的QQ号码:
^\d\d\d\d\d\d\d\d$匹配1开头11位数字的手机号码:
^1\d\d\d\d\d\d\d\d\d\d$
2.重复限定符
正则表达式中一些重复限定符,把重复部分用合适的限定符替代。
语法 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
匹配8位数字的QQ号码:^\d{8}$匹配1开头11位数字的手机号码:
^1\d{10}$匹配银行卡号是14~18位的数字:
^\d{14,18}$匹配以a开头的,0个或多个b结尾的字符串
^ab*$
3.分组
从上面的例子中看到,限定符是作用在与他左边最近的一个字符,那么问题来了,如果我想要 ab 同时被限定那怎么办呢?
正则表达式中用小括号 () 来做分组,也就是括号中的内容作为一个整体。
匹配字符串中包含 0 到多个 ab 开头:^(ab)*
4.转义
我们看到正则表达式用小括号来做分组,那么问题来了:
如果要匹配的字符串中本身就包含小括号,那是不是冲突?应该怎么办?
针对这种情况,正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简答,就是在要转义的字符前面加个斜杠,也就是\即可。
例如,要匹配以 (ab) 开头:^(\(ab\))*
5. 条件或
正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。
比如联通有 130/131/132/155/156/185/186/145/176 等号段
假如让我们匹配一个联通的号码,就可以用或条件来处理这个问题^(130|131|132|155|156|185|186|145|176)\d{8}$
6.区间
正则提供一个元字符中括号 [] 来表示区间条件。
限定 0 到 9 可以写成 [0-9]
限定 A-Z 写成 [A-Z]
限定某些数字 [165]
^(130|131|132|155|156|185|186|145|176)\d{8}$
可以简化为
^((13[0-2])|(15[56])|(18[5-6])|145|176)\d{8}$
7.零宽断言
零宽断言是正则表达式中的一种方法,用于查找在某些内容之前或之后的东西,但不包括这些内容
本身。它们像\b, ^, $等锚点一样,用于指定一个位置,这个位置应该满足一定的条件(即断言),
因此它们也被称为零宽断言。
正则表达式的先行断言和后行断言一共有 4 种形式:
- (?=pattern) 零宽正向先行断言
- (?!pattern) 零宽负向先行断言
- (?<=pattern) 零宽正向后行断言
- (?<!pattern) 零宽负向后行断言
概念说明
- 正向(Positive): 匹配括号中的表达式,即断言所作的条件判断是肯定的,即只有当条件成立时,匹配才成功。
- 负向(Negative): 不匹配括号中的表达式,即断言所作的条件判断是否定的,即只有当条件不成立时,匹配才成功。
- 先行(Lookahead): 表示断言发生在匹配位置之前。
- 后行(Lookbehind): 表示断言发生在匹配位置之后。
假设我们要用爬虫抓取 csdn 里的文章阅读量。通过查看源代码可以看到文章阅读量这个内容是这样的结构。
"<span class="read-count">阅读数:641</span>"
要取到阅读量,在正则表达式中就意味着要能匹配到‘</span>’前面的数字内容,按照上所说的正
向先行断言可以匹配表达式前面的内容。
import re# 定义正则表达式和测试文本
reg = r".+(?=</span>)"
test = "<span class=\"read-count\">阅读数:641</span>"print("文本:" + test)
print("正则表达式:" + reg)# 编译正则表达式
pattern = re.compile(reg)# 创建匹配器
mc = pattern.finditer(test)# 遍历所有匹配项
for match in mc:print("匹配结果:")print(match.group())运行结果:
文本:<span class="read-count">阅读数:641</span>
正则表达式:.+(?=</span>)
匹配结果:
<span class="read-count">阅读数:641
如果只要数字,那么匹配数字 \d, 那可以改成:
import re# 定义正则表达式和测试文本
reg = r"\d+(?=</span>)"
test = "<span class=\"read-count\">阅读数:641</span>"print("文本:" + test)
print("正则表达式:" + reg)# 编译正则表达式
pattern = re.compile(reg)# 创建匹配器
mc = pattern.finditer(test)# 遍历所有匹配项
for match in mc:print(match.group())//匹配结果:
//641
先行是匹配前面的内容,那后行就是匹配后面的内容啦。可以用后行断言来处理:
import re# 定义正则表达式和测试文本
reg = r"(?<=<span class=\"read-count\">阅读数:)\d+"
test = "<span class=\"read-count\">阅读数:641</span>"print("文本:" + test)
print("正则表达式:" + reg)# 编译正则表达式
pattern = re.compile(reg)# 创建匹配器
mc = pattern.finditer(test)# 遍历所有匹配项
for match in mc:print(match.group())输出结果:
文本:<span class="read-count">阅读数:641</span>
正则表达式:(?<=<span class="read-count">阅读数:)\d+
641
8.捕获与非捕获组
捕获通常和分组联系在一起,也就是“捕获组”。捕获组通过圆括号()
来标识,能够在匹配时将括号
内的内容单独保存,并在后续处理中提取或引用。捕获组的出现是为了简化复杂模式的匹配,并方
便从文本中提取关键信息,如提取日期、邮箱、URL 等。
在正则表达式中,通过将部分模式用圆括号括起来,形成一个捕获组。
每个捕获组会分配一个组号,从左到右依次编号,最左边的组为第一个,编号为 1
。
编号为 0
的组总是指整个正则表达式匹配的内容。
通过这些组号,匹配到子字符串可以在后续处理中被引用和操作。
而根据命名方式的不同,又可以分为两种组:
1.数字编号捕获组:
语法:(exp)
解释:从表达式左侧开始,每出现一个左括号和它对应的右括号之间的内容为一个分组,在分组中,第 0 组为整个表达式,第一组开始为分组。
比如固定电话的:020-85653333
他的正则表达式为:(0\d{2})-(\d{8})
按照左括号的顺序,这个表达式有如下分组:
序号 | 编号 | 分组 | 内容 |
---|---|---|---|
0 | 0 | (0\d{2})-(\d{8}) | 020-85653333 |
1 | 1 | (0\d{2}) | 020 |
2 | 2 | (\d{8}) | 85653333 |
2.命名编号捕获组
语法:(?<name>exp)
解释:分组的命名由表达式中的 name 指定
比如区号也可以这样写:(?<quhao>\0\d{2})-(?<haoma>\d{8}),按照左括号的顺序,这个表达式有如下分组:
序号 | 名称 | 分组 | 内容 |
---|---|---|---|
0 | 0 | (0\d{2})-(\d{8}) | 020-85653333 |
1 | quhao | (0\d{2}) | 020 |
2 | haoma | (\d{8}) | 85653333 |
非捕获组
语法:(?:exp)
它不捕获文本 ,也不针对组合计进行计数。比如上面的正则表达式,程序不需要用到第一个分组,那就可以这样写:
(?:\0\d{2})-(\d{8})
序号 | 编号 | 分组 | 内容 |
---|---|---|---|
0 | 0 | (0\d{2})-(\d{8}) | 020-85653333 |
1 | 1 | (\d{8}) | 85653333 |
9.反向组
捕获会返回一个捕获组,这个分组是保存在内存中,不仅可以在正则表达式外部通过程序进行引
用,也可以在正则表达式内部进行引用,这种引用方式就是反向引用。它的作用主要是用来查找一
些重复的内容或者做替换指定字符。
正则表达式的匹配是从左到右进行的。当遇到反向引用时,会尝试用之前捕获组匹配到的内容去替
换当前位置,如果替换后能够继续匹配,则继续;否则,匹配失败。
根据捕获组的命名规则,反向引用可分为:
-
数字编号组反向引用:\k 或\number
-
命名编号组反向引用:\k 或者\'name'
例如,找一串字母"aabbbbgbddesddfiid"里成对的字母
思路:
1)匹配到一个字母
2)匹配第下一个字母,检查是否和上一个字母是否一样
3)如果一样,则匹配成功,否则失败
首先匹配一个字母:\w,我们需要做成分组才能捕获,因此写成这样:(\w)
那这个表达式就有一个捕获组:(\w)
然后我们要用这个捕获组作为条件,那就可以:(\w)\1这样就大功告成了。
捕获组和反向引用的工作原理
-
捕获组
(\\w)
-
每次
\\w
匹配到一个字符时,这个字符会被捕获到一个组中。这个组的编号为1。 -
捕获组的内容是动态的,每次匹配到新的字符时,组中的内容会被更新。
-
-
反向引用
\\1
-
\\1
会匹配与第一个捕获组(即\\w
)相同的字符。 -
每次
\\w
匹配到新的字符时,\\1
会匹配与这个新字符相同的下一个字符。
-
-
字符串:
aabbbbgbddesddfiid
-
匹配过程:
-
aa
:第一个\\w
匹配a
,捕获到组1中,\\1
匹配a
,输出aa
。 -
bb
:第一个\\w
匹配b
,捕获到组1中,\\1
匹配b
,输出bb
。 -
bb
:第一个\\w
匹配b
,捕获到组1中,\\1
匹配b
,输出bb
。 -
bb
:第一个\\w
匹配b
,捕获到组1中,\\1
匹配b
,输出bb
。 -
dd
:第一个\\w
匹配d
,捕获到组1中,\\1
匹配d
,输出dd
。 -
dd
:第一个\\w
匹配d
,捕获到组1中,\\1
匹配d
,输出dd
。 -
ii
:第一个\\w
匹配i
,捕获到组1中,\\1
匹配i
,输出ii
。
-
import re# 待匹配的字符串
text = 'aabbbbgbddesddfiid'# 正则表达式,匹配连续的相同字符对
pattern = r'(\w)\1'# 使用re模块的findall方法查找所有匹配的字符对
matches = re.findall(pattern, text)# 输出匹配结果
for match in matches:print(match + match) # 输出连续的相同字符对
10.贪婪与非贪婪
在正则表达式中,贪婪(greedy)和非贪婪(non-greedy)是两种不同的匹配模式,它们决定了正
则表达式在匹配字符串时的行为。贪婪模式是正则表达式的默认匹配模式。在这种模式下,正则表
达式会尽可能多地匹配字符。非贪婪模式(也称为懒惰模式)与贪婪模式相反。在这种模式下,正
则表达式会尽可能少地匹配字符。
前面我们讲过重复限定符,其实这些限定符就是贪婪量词,比如表达式\d{3,6}:
文本:61762828 176 2991 871
贪婪模式:\\d{3,6}
匹配结果:617628
匹配结果:176
匹配结果:2991
匹配结果:871贪婪模式:正则表达式 \d{3,6} 会尽可能多地匹配字符,但不会超过6位。
匹配结果:匹配所有长度在3到6位之间的数字。匹配 61762828
正则表达式 \d{3,6} 从字符串的开头开始匹配。
它会尽可能多地匹配数字,但不会超过6位。
因此,它匹配了前6位数字 617628匹配过的字符不会再参与后续的匹配。
正则表达式引擎在找到一个匹配后,会从匹配结束的位置继续查找下一个匹配
不会重新考虑已经匹配过的字符
当多个贪婪量词在一起时,正则表达式引擎会按照从左到右的顺序进行匹配,每个量词都会尽可
能多地匹配字符,但会受到其他量词和整个正则表达式的约束。如果某个量词的贪婪匹配导致后续
部分无法匹配成功,正则表达式引擎会进行回溯,尝试减少当前量词的匹配长度,以便后续部分能
够成功匹配。
示例
假设我们有字符串 abcde12345
,要匹配的正则表达式是 (.*)(\d+)
。
-
初始贪婪匹配:
-
(.*)
是一个贪婪量词,它会尽可能多地匹配字符。在匹配字符串abcde12345
时,它会先匹配整个字符串,即abcde12345
。 -
\d+
也表示贪婪匹配一个或多个数字,但由于前面的(.*)
已经匹配了整个字符串,此时\d+
无法再匹配到数字,因为已经没有剩余的字符了。
-
-
回溯过程:
-
正则表达式引擎发现
(\d+)
无法匹配,于是开始回溯。它会减少(.*)
的匹配长度,尝试让后续的(\d+)
能够匹配成功。 -
(.*)
先减少一个字符,匹配abcde1234
,此时(\d+)
可以匹配到数字5
,匹配成功。所以最终匹配结果是abcde1234
被(.*)
匹配,5
被(\d+)
匹配。
-
import re# 待匹配的字符串
text = 'abcde12345'# 正则表达式,将\d+部分也用圆括号括起来,使其成为一个捕获组
pattern = r'(.*)(\d+)'# 使用re模块的search方法进行匹配
match = re.search(pattern, text)if match:# 输出匹配结果print("匹配成功!")print("正则表达式:" + pattern)print("匹配的整个字符串为:", match.group(0))print("第一个捕获组匹配的内容为:", match.group(1))print("第二个捕获组匹配的内容为:", match.group(2))
else:print("匹配失败!")输出结果:
匹配成功!
正则表达式:(.*)(\d+)
匹配的整个字符串为: abcde12345
第一个捕获组匹配的内容为: abcde1234
第二个捕获组匹配的内容为: 5
懒惰(非贪婪)
懒惰(非贪婪)量词在正则表达式中用于尽可能少地匹配字符。与贪婪量词相反,懒惰量词会尝试
匹配最少的字符,以满足整个正则表达式的匹配条件。懒惰量词通常通过在量词后面加上一个问号
?
来表示。
代码 | 说明 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
假设我们有字符串 "abc123def456",使用正则表达式 (\w+?)(\d+):
(\w+?):懒惰模式,匹配一个或多个字母、数字或下划线,但尽可能少。
(\d+):贪婪模式,匹配一个或多个数字。匹配过程
从文本“abc123def456”开始,正则表达式首先尝试用(\\w+?)匹配。
它匹配“abc”,因为“abc”是第一个字母序列,且非贪婪匹配使得它在这里停止。
接下来,正则表达式用(\\d+)匹配。在“abc”之后,它匹配“123”,因为“123”是第一个数字序列。import re# 定义正则表达式和测试文本
reg = r"(\w+?)(\d+)"
test = "abc123def456"print("文本:" + test)
print("正则表达式:" + reg)# 编译正则表达式
p1 = re.compile(reg)# 创建匹配器
m1 = p1.search(test)if m1:print("匹配结果:")print("整个匹配:" + m1.group(0))print("部分1:" + m1.group(1))print("部分2:" + m1.group(2))
else:print("没有匹配到任何内容")运行结果:
文本:abc123def456
正则表达式:(\w+?)(\d+)
匹配结果:
整个匹配:abc123
部分1:abc
部分2:123