正则表达式 | Python/Julia/Shell
唠唠闲话
正则表达式在爬虫,脚本编写等许多任务上都有重要应用,本篇系统地整理一遍正则表达式的语法,以及 Python, Julia 和 Shell 与正则表达式相关的工具。学习链接:编程胶囊
基础语法
公用语法
在大多数支持正则表达式的语言中,这部分规则一致,语言包括 Shell, Python 和 Julia。
-
匹配位置:
^
匹配开头,$
匹配结尾,比如^ab
匹配abc, abcd, abb, ab
^ab
不匹配aab, aac, bab
ab$
匹配aab, ab, abab
ab$
不匹配aabb, b, abb
^ab$
精确匹配ab
-
匹配字符
匹配符 说明 .
匹配除回⻋以外的任意⼀个字符 ( )
字符串分组 [ ]
定义字符类,匹配括号中的⼀个字符 \
转义字符 *
匹配(前边)字符的若干次(包括 0 次) ?
字符出现 0 次或 1 次 +
字符至少出现依次 {n,}
字符至少出现 n 次 {n, m}
字符至少出现 n 次,最多 m 次 {m}
字符正好出现 m 次 -
一些例子
^a.c$
匹配abc, a.c, aec
^a.c$
不匹配abbc, ac, .c
^a\.c
精确匹配a.c
^a*c
匹配c, ac, aac, ...
^a?c$
仅匹配c, ac
^a+c$
匹配ac, aac, ...
-
中括号表示匹配一个字符,中括号内部:
-
代表区间,注意右区间不能比左区间小,否则匹配无效[2-8]
表示匹配数字2, 3,..., 8
[ac-k]
表示匹配字母a, c, d,..., k
^
表示取否,比如[^13-9]
匹配除了1, 3,...,9
以外的字符\
匹配特殊字符,比如[\-]
匹配-
,[\[]
匹配[
贪婪匹配
?
在正则表达式中有多种含义:
- 放在字符后,代表该字符匹配 0/1 次
- 放在分组开头,
?:
代表不捕获分组内容 - 放在分组开头,
?=, ?!, ?<=, ?<!
代表断言 - 放在变长匹配后边,代表非贪婪匹配,也即匹配成功时即刻停止
-
例子
1
2
3
4
5
6reg1 = r"a{2,}"
reg2 = r"a{2,}?"
collect(eachmatch(reg1, "aaaaa"))
# 贪婪匹配,只匹配到一个元素 aaaaaa
collect(eachmatch(reg1, "aaaaa"))
# 非贪婪匹配,匹配到 2 个,"aa", "aa" -
比起“非贪婪匹配”,更准确的说法是“匹配即停”,也即对于当可变长的规则,比如
1
2
3
4
5
6reg1 = r"a.*b"
reg2 = r"a.*?b"
collect(eachmatch(reg1, "aaaaabaab"))
# 贪婪匹配,只匹配到一个元素 aaaaabaab
collect(eachmatch(reg2, "aaaaabaab"))
# 即停匹配,第一处匹配到 aaaaab,第二处匹配到 aab按“贪婪匹配”的说法,使用
reg2
匹配时,应该得到两处ab
而不是aaaaab
和aab
,我们从原理理解匹配的结果更自然:正则匹配从左到右,?
在成功匹配后即停止,并返回结果
特殊匹配
-
这部分规则在 Python 和 Julia 中的表述一致,与 Linux 的
Perl
也一致,但 shell 中的grep
用另一套“POSIX 特殊字符”,所以也分开讨论。 -
这些字符可以在中括号内,也可以在外部
\w
匹配单词字符,包括[_a-zA-Z0-9]
以及汉字等,不包括符号\d
匹配数字,相当于[0-9]
\s
匹配空白字符,包括空格,tab,换行等??\b
匹配单词的边界\babc
匹配带左边界的单词abc
,abcd
,.abcdef
\babc
不匹配aabc
,_abc
,ababc
\b\w{4}\b
匹配带左右边界的四字单词
\W, \D, \S
匹配相应的取反情形
分组
正则表达式提供了一种将表达式分组的机制,当使用分组时,除了获得整个匹配,还能够在匹配中选择每一个分组。
-
分组用括号抓取,比如
(\d{4})-(\d{2})-(\d{2})
匹配并提取数据2022-02-28
中的日期,一共三个分组
1
2
3## Python 正则
re.findall(r'(\d{4})-(\d{2})-(\d{2})', '2022-02-282321-02-21')
# [('2022', '02', '28'), ('2321', '02', '21')]1
2
3
4## Julia 正则
collect(eachmatch(r"(\d{4})-(\d{2})-(\d{2})", "2022-02-282321-02-21"))
# RegexMatch("2022-02-28", 1="2022", 2="02", 3="28")
# RegexMatch("2321-02-21", 1="2321", 2="02", 3="21") -
分组内,使用“或”运算
|
,比如(\.jpg|\.png)
匹配.jpg
或.png
(\.(jpg|png))
嵌套匹配,两个分组,组1为.jpg
或.png
,组2为jpg
或png
1
2
3
4
5
6
7
8### 匹配到两段信息,元组长度分别为 1
collect(eachmatch(r"\d(\.jpg|\.png)", "1.jpg, 2.png"))
# RegexMatch("1.jpg", 1=".jpg")
# RegexMatch("2.png", 1=".png")
### 匹配到两端信息,元组长度分别为 2
collect(eachmatch(r"\d(\.(jpg|png))", "1.jpg, 2.png"))
# RegexMatch("1.jpg", 1=".jpg", 2="jpg")
# RegexMatch("2.png", 1=".png", 2="png") -
有时候,我们并不需要捕获某个分组的内容,但是又想使用分组的特性(比如或运算)。这个时候就可以使用非捕获组
(?:表达式)
,从而不捕获数据,还能使用分组的功能,比如匹配前缀和后缀,但只捕获前缀的分组1
2
3
4## Julia
collect(eachmatch(r"(\w)\.(?:jpg|png)", "1.jpg, 2.png"))
# RegexMatch("1.jpg", 1="1")
# RegexMatch("2.png", 1="2")1
2
3# Python
re.findall(r"(\w)\.(?:jpg|png)", "1.jpg, 2.png")
# ['1', '2'] -
分组的回溯引用,
\N
表示表达式的第 N 个分组,比如1
2
3collect(eachmatch(r"(\w)(\w)\2\1", "abbaddadda"))
# RegexMatch("abba", 1="a", 2="b")
# RegexMatch("adda", 1="a", 2="d")1
2re.findall(r"(\w)(\w)\2\1", "abbaddadda")
# [('a', 'b'), ('a', 'd')]
断言
参考博客园:如同 ^
代表开头,$
代表结尾,\b
代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为“零宽”。
-
先行断言和后行断言总共有四种:
- 正向先行断言
- 反向先行断言
- 正向后行断言
- 反向后行断言
-
正向先行断言:
(?=pattern)
,代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配 pattern。 例如对a regular expression
这个字符串,要想匹配regular
中的re
,但不匹配expression
中的re
,可以用re(?=gular)
。该表达式限定了re
右边的位置,这个位置之后是gular
,但并不消耗gular
这些字符,将表达式改为re(?=gular)g
,将会匹配reg
,括号这一砣匹配了e
和g
之间的位置。 -
负向先行断言:
(?!pattern)
,代表字符串中的一个位置,紧接该位置之后的字符序列不能匹配pattern
。例如对regex represents regular expression
这个字符串,要想匹配除regex
和regular
之外的re
,可以用re(?!g)
。该表达式限定了re
右边的位置,这个位置后面不是字符g
。负向和正向的区别在于该位置之后的字符能否匹配括号中的表达式。 -
正向后行断言:
(?<=pattern)
,代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配pattern
。例如对regex represents regular expression
这个字符串,有 4 个单词,要想匹配单词内部的re
,但不匹配单词开头的re,可以用(?<=\w)re
。之所以叫后行断言,是因为正则表达式引擎在匹配字符串和表达式时,是从前向后逐个扫描字符串中的字符,并判断是否与表达式符合,当在表达式中遇到该断言时,正则表达式引擎需要往字符串前端检测已扫描过的字符,相对于扫描方向是向后的。 -
负向后行断言:
(?<!pattern)
,代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配pattern
。例如对regex represents regular expression
这个字符串,要想匹配单词开头的re
,可以用(?<!\w)re
,当然也可以用\bre
来匹配。 -
总之,正向
=
代表正匹配,负向!
代表反匹配;先行是顺着扫描,也限定该位置后的字符,后行<
是逆着扫描,限定该位置前的字符性质。 -
为了匹配
regex represents regular expression
的片段,有这几种断言方式:- 正向先行
re(?=g)
限定该位置后边必须是什么,匹配了regex, regular
的re
,排除了represents, expression
的re
- 负向先行
re(?!s|p)
限定该位置后边不能是什么,匹配了regex, regular
的re
,排除represents, expression
中的re
- 正向后行
(?<=r)e
限定该位置前边必须是什么,匹配所有带re
的单词内的e
- 负向后行
(?<!p)re
限定该位置前边不能是什么,排除represents, expression
中后一处re
- 经典例子:先行断言
(?=.*?[a-z])(?=.*?[A-Z]).+
匹配同时包含大小写字母的非空字符串- 第一个括号
(?=.*?[a-z])
匹配包含一个小写字母之后的所有位置 - 第二个括号
(?=.*?[A-Z])
匹配包含一个大写字母之后的所有位置 - 两个括号配合,表示至少包含一个大写和小写字符的位置
- 最后的
.+
表示字符串本身长度不小于 1 - 实际上由于前两条先行断言的限定,最后的匹配用
.*
和.{2,}
效果等价
- 第一个括号
- 一般地,这可以作为匹配用到模板,
(?=.*?[a-z]{m,})
表示至少包含m
个括号中的字符
- 正向先行
shell
-
grep -E
或egrep
命令对文本进行正则匹配,注意grep
默认参数为grep -e
,此时部分正则表达式无效,比如grep a?c
不匹配ac
和c
-
POSIX 特殊字符
特殊字符 说明 [:alnum:]
匹配任意字⺟字符0-9 a-z A-Z [:alpha:]
匹配任意字⺟,⼤写或⼩写 [:digit:]
数字 0-9 [:graph:]
⾮空字符( ⾮空格控制字符) [:lower:]
⼩写字符a-z [:upper:]
⼤写字符A-Z [:cntrl:]
控制字符 [:print:]
⾮空字符( 包括空格) [:punct:]
标点符号 [:blank:]
空格和TAB字符 [:xdigit:]
16 进制数字 [:space:]
所有空⽩字符( 新⾏、空格、制表符) -
注意匹配时,外层还要再套一层
[]
,比如[[:alpha:]]
匹配字母
Julia
推荐阅读:String 官方文档
-
Julia 中,字符串追加
r
代表正则表达式Regex
,举个例子1
2
3re = r"^\s*(?:#|\$)" # 规则:在若干空字符后,匹配 # 或者 $
typeof(re) # Regex
isconcretetype(Regex) # true例子涉及前边几节的语法:
^
代表从首位开始匹配,\s*
匹配若干空白字符,(?:...)
为不命名元组的写法,#|\$
在元组内表示或运算,#
或$
-
occursin
判断,match
匹配内容1
2
3
4occursin(r"^\s*(?:#|\$)", "# a comment") # true 判断是否匹配
match(r"^\s*(?:#|\$)", "# a comment", 2) # 从位置 2 开始匹配,未匹配到,返回 nothing
m = match(r"^\s*(?:#|\$)", "# a comment") # RegexMatch("#")
typeof(m) # RegexMatchmatch
匹配成功后,返回具体类型数据RegexMatch
,其包含表达式匹配到的详细信息,比如1
2
3
4
5m = match(r"(\d{2})([a-z]{2})", "aa12bb34cc")
m.match # 匹配到的字符串
m.captures # 匹配到的分组信息,未分组则返回空列表
m.offset # 返回匹配到的字符串的起始位置(整数)
m.offsets # 返回匹配到的分组的起始位置(列表) -
用
(?P<tag>expr)
将分组命名为tag
,其中字母P
可以省略1
2
3
4
5
6
7m = match(r"(?<tag>\d+):(\d+)","12:45")
m[:tag] # 使用标签调用分组
m[2] # 使用索引调用分组
m = match(r"(?P<hi>\d+):\g<hi>","12:12") # 用 \g + 名称,回溯调用分组内容
m = match(r"(?<hi>\d+):\g1","12:12") # 用 \g + 数字回溯调用分组内容
# Python 写法
# re.findall(r"(?P<hi>\d+):(?P=hi)","12:12")Julia 中的回溯调用的语法
\g<分组名>
\n, \gn, \g<n>
,n 为分组序号
-
replace
的键和值都支持正则表达,其中值字符串的前边追加参数s
1
2
3
4
5# 键值使用 s
replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<1>")
replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<minute>")
# \g<0> 代表整个字符串
replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<0>") -
匹配多项使用
eachmatch
,返回由RegMatch
类型数据构成的生成器1
2
3collect(eachmatch(r"\d\d", "1234")) # 匹配到两个
collect(eachmatch(r"\d\d", "1234"; overlap=true)) # 匹配到三个
collect(eachmatch(r"\d\n\d", "12\n34")) # 支持换行匹配 -
.
不能匹配换行号,需要换行匹配时,可以用[\s\S]
或[\w\W]
1
2match(r"123.*321", "123\n22\n321") # nothing
match(r"123[\s\S]321", "123\n22\n321") # RegexMatch("123\n22\n321") -
字符结尾的
ism
类似 Perl,后续再补充
Python
Python 自 1.5 版本起增加了 re 模块,它提供 Perl 风格的正则表达式模式。
-
match
从字符起始位置开始匹配1
2
3re.match(r"\d\d", "2a-2b\n20") # 返回 None,未匹配到
re.match(r"\w\w", "2a-2b\n20") # 返回匹配对象
# <re.Match object; span=(0, 2), match='2a'> -
search
扫描整个字符串,并返回第一个成功匹配的位置1
2
3
4
5
6
7
8
9
10
11
12
13
14re.search(r"\d\d", "2a-2b\n20")
# <re.Match object; span=(6, 8), match='20'>
info = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "ab2022-03-01cd")
print(info.start()) # 2 | 匹配到的起始位置
print(info.end()) # 12 | 匹配到的终点位置
info.span() # (2, 12) | 起始 和 终点
print(info.groups()) # ('2022', '03', '01') | 分组内容
print(info.group(0)) # 2022-03-01 | 位置 0 代表匹配的字符串
info.group() # 等同于 info.group(0)
info.group(1) # 2022 | 位置 1 代表分组第一个元素
info.regs # ((2, 12), (2, 6), (7, 9), (10, 12)) | 分组内容的索引
info.re # 返回使用的正则匹配规则
info.string # 返回被匹配的字符串
info.groupdict() # {'year': '2022', 'month': '03', 'day': '01'} | 字典形式返回匹配内容匹配对象的常用属性和方法
-
sub
检索和替换1
2
3
4
5# 提取日期,并将格式的 - 改为 /
txt = "ab2022-03-01cd"
reg = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
repl = r"\1/\2/\3"
re.sub(reg, repl, txt) # 'ab2022/03/01cd' -
compile
编译正则表达式,搭配其他函数使用1
2reg = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})") # 编译正则表达式
reg.search("ab2022-03-01cd") # 返回查找对象 -
findall
查找所有匹配的字符串1
2
3
4re.findall("\w{2}", "2022-03-01") # 返回字符串构成的列表
# ['20', '22', '03', '01']
re.findall("(\w{2})-(\w{2})", "20-22-03-01") # 返回字符串元组构成的列表
# [('20', '22'), ('03', '01')] -
finditer
返回迭代器形式的匹配信息1
2
3
4
5list(re.finditer("\w{2}", "2022-03-01"))
# [<re.Match object; span=(0, 2), match='20'>,
# <re.Match object; span=(2, 4), match='22'>,
# <re.Match object; span=(5, 7), match='03'>,
# <re.Match object; span=(8, 10), match='01'>] -
split
按匹配规则进行字符串拆分1
2re.split("\d", "1bb3cc1dd")
# ['', 'bb', 'cc', 'dd'] -
在使用以上函数时,支持修饰符,且多个修饰符用
|
指定,比如1
2re.findall(r"ab.*cd", "ab\ndd\nCD") # [] | 换行匹配不到
re.findall(r"ab.*cd", "ab\ndd\nCD", re.I | re.S) # ['ab\ndd\nCD'] | `.` 可以匹配换行,字母不分大小写 -
常见修饰符
修饰符 | 描述 |
---|---|
re.I |
使匹配对大小写不敏感 |
re.L |
做本地化识别匹配 |
re.M |
多行匹配,影响 ^ 和 $ |
re.S |
使 . 匹配包括换行在内的所有字符 |
re.U |
根据 Unicode 字符集解析字符。这个标志影响 \w, \W, \b, \B |
re.X |
该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解 |
Perl
shell 中使用 grep -P
使用 Perl 的正则表达式语法