正则表达式学习笔记

正则表达式在计算机领域可以说是用途广泛,写爬虫时,用他过滤无用的html标签,或者只保留汉字,预防代码注入漏洞时,可以用它来过滤危险命令,删除文件时,还可以通过正则表达式来批量删除指定的文件,所以很难不学习。

python与正则表达式

python的库功能强大,本文用python来学习正则表达式。在Python中,正则表达式主要通过re模块来实现。re库是Python的标准库之一,专门用于处理正则表达式。

re库主要有search,match,sub等方法。当使用search()match()finditer()时,返回的是一个Match对象。这个对象包含了匹配的详细信息。

re.match()函数用于搜索字符串的开始是否与给定的正则表达式模式匹配。如果匹配成功,它返回一个Match对象;如果没有匹配项,它返回Nonere.match()只从字符串的开头开始匹配。如果模式在字符串的开始位置找不到匹配项,即使在其他地方有匹配项,re.match()也不会成功。

re.search()用于搜索给定字符串中的第一个与指定正则表达式模式匹配的子串。与re.match()不同,re.search()会检查字符串中的所有位置,而不仅仅是开始位置。如果re.search()找到了一个匹配项,它将返回一个Match对象;如果没有找到匹配项,它将返回Nonere.search()只会返回第一个找到的匹配项的Match对象。如果你想找到字符串中所有的匹配项,应该使用re.findall()re.finditer()

下面是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import re

# 定义一个正则表达式来匹配电子邮件地址
email_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"

text = """
Hello, my email is john.doe@example.com. You can also reach
out to support@example.org or visit our website at http://example.net.
Please also CC jane_doe_123@example.co.uk for any further questions.
"""

# 使用findall查找所有的电子邮件地址
emails = re.findall(email_pattern, text)
print("Found Emails:", emails)

# 使用sub替换找到的电子邮件地址
protected_text = re.sub(email_pattern, "[PROTECTED]", text)
print(protected_text)

输出:

1
2
3
4
5
6

Found Emails: ['john.doe@example.com', 'support@example.org', 'jane_doe_123@example.co.uk']
Hello, my email is [PROTECTED]. You can also reach
out to [PROTECTED] or visit our website at http://example.net.
Please also CC [PROTECTED] for any further questions.

不妨从这段正则表达式入手[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

[a-zA-Z0-9._%+-] 定义了一个字符类,其中的字符可以是任何小写字母a-z、大写字母A-Z、数字0-9或特殊字符._%+-中的一个。 + 表示前面的字符类可以出现一次或多次。@匹配邮箱地址中的@.

[a-zA-Z0-9.-] 定义了一个字符类,其中的字符可以是任何小写字母a-z、大写字母A-Z、数字0-9或特殊字符.-中的一个。+ 表示前面的字符类可以出现一次或多次。由于.在正则表达式中是一个特殊字符,表示匹配任何字符(除了换行符),所以使用\对其进行转义,使其匹配实际的.字符。[a-zA-Z] 定义了一个字符类,其中的字符可以是任何小写字母a-z或大写字母A-Z中的一个。{2,} 表示前面的字符类必须至少出现两次。这是为了确保顶级域名至少有两个字符长,符合大多数实际情况。

正则表达式必知必会

来自书《正则表达式必知必会》

匹配单字符

匹配纯文本

正则表达式甚至可以只包含纯文本,但注意正则表达式区别大小写。

1
2
3
4
import re
s1="abcABCaBd"
match_output=re.findall(r"abc",s1)
print(match_output)
1
2
output:
['abc']

匹配任意字符

.字符可以匹配任何一个单个字符,包括.本身。

1
2
match_output=re.findall(r"b.t","bAtbotbatb.t")
print(match_output)
1
2
output:
['bAt', 'bot', 'bat', 'b.t']

.通常不匹配换行符

匹配特殊字符

如果要只匹配.呢?显然我们需要一个转义符号,在正则表达式中,\可以进行转义。

1
2
match_output=re.findall(r"b\.t","bAtbotbatb.t")
print(match_output)
1
2
output:
['b.t']

匹配一组字符

字符集合

在正则表达式中,字符集合允许我们匹配来自某一集合的任意一个字符。字符集合是通过将一组字符放在[]中来定义的。

比如:

1
2
match_output=re.findall(r"[bc]at","cat bat lat dat")
print(match_output)
1
2
output:
['cat', 'bat']

其中的[bc]是一个字符集合,这个集合将只匹配字符b或者c

字符集合区间

当我们需要匹配全部阿拉伯数字或者英文字母时,在字符集合里面一个一个穷举过于麻烦了,可以使用-定义范围,但-也可以被转义。

1
2
match_output=re.findall(r"[A-Z][0-9][0-9a-f]","A10 A2f B9c C1z d99 Q09")
print(match_output)
1
2
output:
['A10', 'A2f', 'B9c', 'Q09']

利用正则表达式匹配中文就用到了字符集合区间[一-龥]:

1
2
match_output=re.findall(r"[一-龥]","a中b文c匹配def")
print(match_output)
1
2
output:
['中', '文', '匹', '配']

当然匹配中文没那么简单,具体可以参考:

https://zhuanlan.zhihu.com/p/266732210

取非匹配

某些场合,我们需要排除一些不需要的字符,可以使用^来进行取非匹配。比如[^0-9]对阿拉伯数字进行取非。

1
2
match_output=re.findall(r"[^0-9]","abc123decade79")
print(match_output)
1
2
output:
['a', 'b', 'c', 'd', 'e', 'c', 'a', 'd', 'e']

或匹配

| 符号代表“或”操作,它用于指定两个或多个可选的模式中的一个。比如cat|dog,和[]有点类似,但是可以匹配字符串。

元字符的使用

元字符是具有特殊意义的字符,它们不匹配字面值,而是代表特定的模式。

数字/非数字和字母+数字/非字母+数字

用元字符\d匹配任何一个数字字符,\D匹配任何一个非数字,\w匹配任何一个字母数字或者下划线字符,等价于[a-zA-Z0-9_]\W相反。

1
2
match_output=re.findall(r"\d\D\w\W","0A_? 3B_b")
print(match_output)
1
2
output:
['0A_?']
空白字符

\s等价于[\f\n\r\t\v]\S与之相反。

十六进制或八进制

八进制和十六进制序列允许匹配指定的ASCII或Unicode字符。但python只支持十六进制和Unicode十六进制。

1
2
match_output=re.findall(r"\x41","01aA_? 3B_b")
print(match_output)
1
2
output:
['A']

重复匹配

前面所学的都只能匹配一个字符,如果要匹配多个呢?就需要一些其他的元字符来实现。

匹配一个或多个字符

元字符+匹配它前面的元素出现的一次或多次连续重复。

1
2
match_output=re.findall(r"\d+","123abc456hjkl")
print(match_output)
1
2
output:
['123', '456']

匹配零个或多个字符

这种匹配用*完成。

1
2
3
str_list=["b.","b","b...","c"]
matches = [s for s in str_list if re.search(r"b\.*", s)]
print(matches)
1
2
output:
['b.', 'b', 'b...']

匹配零个或一个字符

这种需求看似奇怪,其实很常见,比如匹配url,有httphttps两种协议,此外,访问文件服务器也有ftpsftp两种前缀。可以用元字符?解决。

1
2
3
str_list=["http://baidu.com","https://qwq.com","httpsssss://google.com","http://google.com"]
matches = [s for s in str_list if re.fullmatch(r"https?://[\w./]+", s)]
print(matches)
1
2
output:
['http://baidu.com', 'https://qwq.com', 'http://google.com']

匹配的重复次数

之前提到的+*,匹配的个数都没有上限,如何为匹配的个数设置一个区间呢?可以使用{}。比如匹配三位数的阿拉伯数字。

1
2
3
str_list=["111","22","333","4","666"]
matches = [s for s in str_list if re.fullmatch(r"\d{3}", s)]
print(matches)
1
2
output:
['111', '333', '666']

此外,还可以用{2,5}这样的格式,表示重复2-5次;{2,}表示最少两次;{,3}表示最多三次。{} 设置的重复是贪婪的,这意味着它会尽可能匹配更多的重复。例如,在字符串 “aaaaa” 中,正则表达式 a{2,4} 将匹配前四个 ‘a’。

防止过度匹配

如果希望匹配尽可能少的重复,可以在大括号后加上一个 ?。例如,对于字符串 “aaaaa”,正则表达式 a{2,4}? 将只匹配两个 ‘a’。

位置匹配

某些场合,需要对某段字符串的特定位置进行匹配。

边界

单词边界

由限定符\b指定的单词边界,\b匹配一个单词的开始或者结尾。单词边界可以被认为是 \w\W 之间的边界,或者位于字符串的开始或结束,并与 \w 相邻。

1
2
match_output=re.findall(r"\bcat\b","cat scatter cats")
print(match_output)
1
2
output:
['cat']

\b匹配且只匹配一个位置,可以发现匹配得到的结果是三个字符而不是五个。\b 表示单词边界,是一个“位置字符”,表示一个位置,而不是一个实际的字符。

如果要不匹配一个单词边界,可以使用\B\B\b 的反义,它表示非单词边界。就像 \b 是用来匹配位于单词边界的位置,\B 是用来匹配不在单词边界的位置。

1
2
match_output=re.search(r'\Bcat\B',"cat scatter cats")
print(match_output)
1
2
output:
<re.Match object; span=(5, 8), match='cat'>
字符串边界

^ 匹配字符串的开头,$匹配字符串的结尾。

1
2
3
str_list=["0x0a","0xff","333","0x04","666"]
matches = [s for s in str_list if re.search(r"^0x", s)]
print(matches)
1
2
output:
['0x0a', '0xff', '0x04']
1
2
3
str_list=["0x0a","0xff","333","0x04","666"]
matches = [s for s in str_list if re.search(r"f$", s)]
print(matches)
1
2
output:
['0xff']
分行匹配模式

在多行模式下,^$ 还可以匹配每一行的开始和结束。在Python的re模块中,可以使用re.MULTILINEre.M标志来激活多行模式。

1
2
3
4
5
6
7
8
9
s='''qwq
qaq
ovo
omo
oho
2333
o#o'''
match_output=re.findall(r'^o.{1}o$',s,re.MULTILINE)
print(match_output)
1
2
output:
['ovo', 'omo', 'oho', 'o#o']

子表达式的使用

有时候我们的需求比较复杂,比如需要匹配重复abc的字符串,那abc*肯定不能满足需求,这时候就要用到子表达式。子表达式(通常被称为捕获组或捕获括号)允许你将正则表达式的部分内容括在一对括号中,以此来捕获它们匹配的文本。子表达式用()

1
2
3
str_list=["0x0a","0xff","333","abc","abcabc"]
matches = [s for s in str_list if re.search(r"(abc)+", s)]
print(matches)
1
2
output:
['abc', 'abcabc']

非捕获组

非捕获组是正则表达式中的一种构造,允许你定义一个组来应用量词或者进行组合,但不捕获该组匹配的文本。这意味着匹配的结果不会保存供之后引用或检索。非捕获组在正则表达式中用 (?:...) 表示。使用非捕获组可能会提高正则表达式的性能,因为引擎不需要保存匹配的子字符串。

命名子表达式

使用 (?P<name>...) 来命名一个子表达式。

1
2
3
4
5
6
7
text = "123-abc"
match = re.search(r'(?P<number>\d+)-(?P<letters>\w+)', text)

if match:
print(match.group('number'))
print(match.group('letters'))

1
2
3
output:
123
abc

回溯引用

如果一个正则表达式中的部分与文本匹配,回溯引用可以在后面的表达式中引用或重新使用这部分匹配的文本。每个捕获组都有一个关联的编号。第一个捕获组是 1,第二个是 2,依此类推。比如查找重复的单词:

1
2
match_output=re.search(r'\b(\w+)\b\s+\1\b',"cat cat dog ")
print(match_output)
1
2
output:
<re.Match object; span=(0, 7), match='cat cat'>

还有一个例子是匹配HTML的标题标签(<H1>到<H6>)。

1
2
3
4
5
6
7
s='''<H1>title</H1>
<p>content</p>
<h2>qwq</h2>
<h3>title</h4>
'''
match_output=re.findall(r'(<[hH]([1-6])>.*?</[hH]\2>)',s)
print(match_output)
1
2
output:
[('<H1>title</H1>', '1'), ('<h2>qwq</h2>', '2')]

如果用传统的正则表达式<[hH][1-6]>,*?</[hH][1-6]>,那么<h3>title</h4>这种不合法的标题也会被匹配到。使用回溯引用来避免这种情况。

在替换中回溯引用也经常被用到。

1
2
3
s = "hello, world, good, morning"
pattern = r'(\w+), (\w+)'
replacement = r'\2, \1'
1
2
output:
world, hello, morning, good

前后查找

正则表达式中的前向和后向查找(也称为前瞻和后顾)是一种高级的匹配技术,允许你在匹配某个模式时考虑其前后的内容,但这些前后的内容并不包含在最终的匹配结果中。比如我们需要提取web页面的标题内容:

1
<head><title>Title</title></head>

如果用<title>.*</title>来匹配,显然<title></title>是我们不需要了,白白浪费性能。

可以使用前向查找来解决这个问题:

1
2
3
s="<head><title>Title</title></head>"
result=re.findall(r'(?<=<title>).*(?=</title>)',s)
print(result)
1
2
output:
['Title']

向前查找

一个向前查找模式就是一个以?=开头的子表达式,需要匹配的文本跟在=的后面。

比如匹配出URL地址中的协议名部分:

1
2
3
s="http://www.baidu.com"
result=re.findall(r'.+(?=:)',s)
print(result)
1
2
output:
['http']

向前查找用于查找出现在被匹配文本之后的字符,而不消费这个字符(可以简单理解为不包含在返回的匹配结果中)。

向后查找

向后查找操作符是?<=

书中有一个小提示帮助理解:向后查找符有小于号,这个小于号是一个箭头,指向文本阅读方向的后方。

比如匹配美元价格:

1
2
result=re.findall(r'(?<=\$)[0-9.]+',"$1.00 $5.99")
print(result)
1
2
output:
['1.00', '5.99']

最终匹配的结果没有美元的$字符。

向前查找和向后查找结合

参考最初给出的例子(?<=<title>).*(?=</title>),开头的(?<=<title>)用于向后查找<title>,结尾的(?=</title>)用于向前匹配</title>

对前后查找取非

前后查找还有一种不太常用的方法叫负前后查找。负向前查找将向前查找不与给定模式相匹配文本,负向后查找同理。前后查找用!来取非。

书中给出了各种前后查找操作符:

操作符 说明
(?=) 正向前查找
(?!) 负向前查找
(?<=) 正向后查找
(?<!) 负向后查找

举个例子,匹配非美元的价格:

1
2
result=re.findall(r'(?<!\$)[0-9]+\.[0-9]+',"$1.00 $5.99 ¥100.00 ¥4.99")
print(result)
1
2
output:
['100.00', '4.99']

嵌入条件

嵌入条件(也被称为条件表达式或条件子表达式)基于某些条件进行不同的匹配。条件可以是一个子组是否匹配,或者一个查找断言是否为真。

python中,需要安装regex库来支持这种高级特性:

1
pip install regex

正则表达式里的条件用?来定义。

子组存在条件

基于一个指定的子组是否匹配来决定接下来的匹配方式。语法:

1
(?(id/name)yes-pattern|no-pattern)
  • id/name:子组的ID或名称
  • yes-pattern:如果子组存在,应匹配的模式
  • no-pattern:如果子组不存在,应匹配的模式

比如:

1
2
3
pattern = regex.compile(r'((a)?b(?(2)c|d))')
result=pattern.findall('bd abc bc abd')
print(result)
1
2
output:
[('bd', ''), ('abc', 'a'), ('bd', '')]

其中最外面大括号是1(a)2,那么(?(2)c|d)的意思是,如果a被匹配到了,则应该匹配c,否则匹配d,返回结果也验证了之前的推理。这里把整个正则表达式括起来主要是为了输出匹配的结果。

一些常用的正则表达式

匹配中文

[一-龥]

更复杂的实现参考:

https://zhuanlan.zhihu.com/p/266732210