正则表达式学习笔记
正则表达式在计算机领域可以说是用途广泛,写爬虫时,用他过滤无用的html
标签,或者只保留汉字,预防代码注入漏洞时,可以用它来过滤危险命令,删除文件时,还可以通过正则表达式来批量删除指定的文件,所以很难不学习。
python与正则表达式
python的库功能强大,本文用python来学习正则表达式。在Python中,正则表达式主要通过re
模块来实现。re
库是Python的标准库之一,专门用于处理正则表达式。
re
库主要有search
,match
,sub
等方法。当使用search()
、match()
或finditer()
时,返回的是一个Match对象。这个对象包含了匹配的详细信息。
re.match()
函数用于搜索字符串的开始是否与给定的正则表达式模式匹配。如果匹配成功,它返回一个Match对象;如果没有匹配项,它返回None
。re.match()
只从字符串的开头开始匹配。如果模式在字符串的开始位置找不到匹配项,即使在其他地方有匹配项,re.match()
也不会成功。
re.search()
用于搜索给定字符串中的第一个与指定正则表达式模式匹配的子串。与re.match()
不同,re.search()
会检查字符串中的所有位置,而不仅仅是开始位置。如果re.search()
找到了一个匹配项,它将返回一个Match对象;如果没有找到匹配项,它将返回None
。re.search()
只会返回第一个找到的匹配项的Match对象。如果你想找到字符串中所有的匹配项,应该使用re.findall()
或re.finditer()
。
下面是一段示例代码:
1 | import re |
输出:
1 |
|
不妨从这段正则表达式入手[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 | import re |
1 | output: |
匹配任意字符
.
字符可以匹配任何一个单个字符,包括.
本身。
1 | match_output=re.findall(r"b.t","bAtbotbatb.t") |
1 | output: |
.
通常不匹配换行符。
匹配特殊字符
如果要只匹配.
呢?显然我们需要一个转义符号,在正则表达式中,\
可以进行转义。
1 | match_output=re.findall(r"b\.t","bAtbotbatb.t") |
1 | output: |
匹配一组字符
字符集合
在正则表达式中,字符集合允许我们匹配来自某一集合的任意一个字符。字符集合是通过将一组字符放在[]
中来定义的。
比如:
1 | match_output=re.findall(r"[bc]at","cat bat lat dat") |
1 | output: |
其中的[bc]
是一个字符集合,这个集合将只匹配字符b
或者c
。
字符集合区间
当我们需要匹配全部阿拉伯数字或者英文字母时,在字符集合里面一个一个穷举过于麻烦了,可以使用-
定义范围,但-
也可以被转义。
1 | match_output=re.findall(r"[A-Z][0-9][0-9a-f]","A10 A2f B9c C1z d99 Q09") |
1 | output: |
利用正则表达式匹配中文就用到了字符集合区间[一-龥]
:
1 | match_output=re.findall(r"[一-龥]","a中b文c匹配def") |
1 | output: |
当然匹配中文没那么简单,具体可以参考:
取非匹配
某些场合,我们需要排除一些不需要的字符,可以使用^
来进行取非匹配。比如[^0-9]
对阿拉伯数字进行取非。
1 | match_output=re.findall(r"[^0-9]","abc123decade79") |
1 | output: |
或匹配
|
符号代表“或”操作,它用于指定两个或多个可选的模式中的一个。比如cat|dog
,和[]
有点类似,但是可以匹配字符串。
元字符的使用
元字符是具有特殊意义的字符,它们不匹配字面值,而是代表特定的模式。
数字/非数字和字母+数字/非字母+数字
用元字符\d
匹配任何一个数字字符,\D
匹配任何一个非数字,\w
匹配任何一个字母数字或者下划线字符,等价于[a-zA-Z0-9_]
,\W
相反。
1 | match_output=re.findall(r"\d\D\w\W","0A_? 3B_b") |
1 | output: |
空白字符
\s
等价于[\f\n\r\t\v]
,\S
与之相反。
十六进制或八进制
八进制和十六进制序列允许匹配指定的ASCII或Unicode字符。但python
只支持十六进制和Unicode十六进制。
1 | match_output=re.findall(r"\x41","01aA_? 3B_b") |
1 | output: |
重复匹配
前面所学的都只能匹配一个字符,如果要匹配多个呢?就需要一些其他的元字符来实现。
匹配一个或多个字符
元字符+
匹配它前面的元素出现的一次或多次连续重复。
1 | match_output=re.findall(r"\d+","123abc456hjkl") |
1 | output: |
匹配零个或多个字符
这种匹配用*
完成。
1 | str_list=["b.","b","b...","c"] |
1 | output: |
匹配零个或一个字符
这种需求看似奇怪,其实很常见,比如匹配url,有http
和https
两种协议,此外,访问文件服务器也有ftp
和sftp
两种前缀。可以用元字符?
解决。
1 | str_list=["http://baidu.com","https://qwq.com","httpsssss://google.com","http://google.com"] |
1 | output: |
匹配的重复次数
之前提到的+
和*
,匹配的个数都没有上限,如何为匹配的个数设置一个区间呢?可以使用{}
。比如匹配三位数的阿拉伯数字。
1 | str_list=["111","22","333","4","666"] |
1 | output: |
此外,还可以用{2,5}
这样的格式,表示重复2-5次;{2,}
表示最少两次;{,3}
表示最多三次。{}
设置的重复是贪婪的,这意味着它会尽可能匹配更多的重复。例如,在字符串 “aaaaa” 中,正则表达式 a{2,4}
将匹配前四个 ‘a’。
防止过度匹配
如果希望匹配尽可能少的重复,可以在大括号后加上一个 ?
。例如,对于字符串 “aaaaa”,正则表达式 a{2,4}?
将只匹配两个 ‘a’。
位置匹配
某些场合,需要对某段字符串的特定位置进行匹配。
边界
单词边界
由限定符\b
指定的单词边界,\b
匹配一个单词的开始或者结尾。单词边界可以被认为是 \w
和 \W
之间的边界,或者位于字符串的开始或结束,并与 \w
相邻。
1 | match_output=re.findall(r"\bcat\b","cat scatter cats") |
1 | output: |
\b
匹配且只匹配一个位置,可以发现匹配得到的结果是三个字符而不是五个。\b
表示单词边界,是一个“位置字符”,表示一个位置,而不是一个实际的字符。
如果要不匹配一个单词边界,可以使用\B
。\B
是 \b
的反义,它表示非单词边界。就像 \b
是用来匹配位于单词边界的位置,\B
是用来匹配不在单词边界的位置。
1 | match_output=re.search(r'\Bcat\B',"cat scatter cats") |
1 | output: |
字符串边界
^
匹配字符串的开头,$
匹配字符串的结尾。
1 | str_list=["0x0a","0xff","333","0x04","666"] |
1 | output: |
1 | str_list=["0x0a","0xff","333","0x04","666"] |
1 | output: |
分行匹配模式
在多行模式下,^
和 $
还可以匹配每一行的开始和结束。在Python的re
模块中,可以使用re.MULTILINE
或re.M
标志来激活多行模式。
1 | s='''qwq |
1 | output: |
子表达式的使用
有时候我们的需求比较复杂,比如需要匹配重复abc
的字符串,那abc*
肯定不能满足需求,这时候就要用到子表达式。子表达式(通常被称为捕获组或捕获括号)允许你将正则表达式的部分内容括在一对括号中,以此来捕获它们匹配的文本。子表达式用()
。
1 | str_list=["0x0a","0xff","333","abc","abcabc"] |
1 | output: |
非捕获组
非捕获组是正则表达式中的一种构造,允许你定义一个组来应用量词或者进行组合,但不捕获该组匹配的文本。这意味着匹配的结果不会保存供之后引用或检索。非捕获组在正则表达式中用 (?:...)
表示。使用非捕获组可能会提高正则表达式的性能,因为引擎不需要保存匹配的子字符串。
命名子表达式
使用 (?P<name>...)
来命名一个子表达式。
1 | text = "123-abc" |
1 | output: |
回溯引用
如果一个正则表达式中的部分与文本匹配,回溯引用可以在后面的表达式中引用或重新使用这部分匹配的文本。每个捕获组都有一个关联的编号。第一个捕获组是 1
,第二个是 2
,依此类推。比如查找重复的单词:
1 | match_output=re.search(r'\b(\w+)\b\s+\1\b',"cat cat dog ") |
1 | output: |
还有一个例子是匹配HTML的标题标签(<H1>到<H6>
)。
1 | s='''<H1>title</H1> |
1 | output: |
如果用传统的正则表达式<[hH][1-6]>,*?</[hH][1-6]>
,那么<h3>title</h4>
这种不合法的标题也会被匹配到。使用回溯引用来避免这种情况。
在替换中回溯引用也经常被用到。
1 | s = "hello, world, good, morning" |
1 | output: |
前后查找
正则表达式中的前向和后向查找(也称为前瞻和后顾)是一种高级的匹配技术,允许你在匹配某个模式时考虑其前后的内容,但这些前后的内容并不包含在最终的匹配结果中。比如我们需要提取web页面的标题内容:
1 | <head><title>Title</title></head> |
如果用<title>.*</title>
来匹配,显然<title>
和</title>
是我们不需要了,白白浪费性能。
可以使用前向查找来解决这个问题:
1 | s="<head><title>Title</title></head>" |
1 | output: |
向前查找
一个向前查找模式就是一个以?=
开头的子表达式,需要匹配的文本跟在=
的后面。
比如匹配出URL地址中的协议名部分:
1 | s="http://www.baidu.com" |
1 | output: |
向前查找用于查找出现在被匹配文本之后的字符,而不消费这个字符(可以简单理解为不包含在返回的匹配结果中)。
向后查找
向后查找操作符是?<=
。
书中有一个小提示帮助理解:向后查找符有小于号,这个小于号是一个箭头,指向文本阅读方向的后方。
比如匹配美元价格:
1 | result=re.findall(r'(?<=\$)[0-9.]+',"$1.00 $5.99") |
1 | output: |
最终匹配的结果没有美元的$
字符。
向前查找和向后查找结合
参考最初给出的例子(?<=<title>).*(?=</title>)
,开头的(?<=<title>)
用于向后查找<title>
,结尾的(?=</title>)
用于向前匹配</title>
。
对前后查找取非
前后查找还有一种不太常用的方法叫负前后查找。负向前查找将向前查找不与给定模式相匹配文本,负向后查找同理。前后查找用!
来取非。
书中给出了各种前后查找操作符:
操作符 | 说明 |
---|---|
(?=) | 正向前查找 |
(?!) | 负向前查找 |
(?<=) | 正向后查找 |
(?<!) | 负向后查找 |
举个例子,匹配非美元的价格:
1 | result=re.findall(r'(?<!\$)[0-9]+\.[0-9]+',"$1.00 $5.99 ¥100.00 ¥4.99") |
1 | output: |
嵌入条件
嵌入条件(也被称为条件表达式或条件子表达式)基于某些条件进行不同的匹配。条件可以是一个子组是否匹配,或者一个查找断言是否为真。
在python
中,需要安装regex
库来支持这种高级特性:
1 | pip install regex |
正则表达式里的条件用?
来定义。
子组存在条件
基于一个指定的子组是否匹配来决定接下来的匹配方式。语法:
1 | (?(id/name)yes-pattern|no-pattern) |
id/name
:子组的ID或名称yes-pattern
:如果子组存在,应匹配的模式no-pattern
:如果子组不存在,应匹配的模式
比如:
1 | pattern = regex.compile(r'((a)?b(?(2)c|d))') |
1 | output: |
其中最外面大括号是1
,(a)
是2
,那么(?(2)c|d)
的意思是,如果a
被匹配到了,则应该匹配c
,否则匹配d
,返回结果也验证了之前的推理。这里把整个正则表达式括起来主要是为了输出匹配的结果。
一些常用的正则表达式
匹配中文
[一-龥]
更复杂的实现参考: