正则表达式 | 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
到8
[a-c]
匹配字母a
,b
,c
^
表示取反,例如[^1-3]
匹配除了1
,2
,3
以外的字符\
用于匹配特殊字符,例如[\-]
匹配-
,[\[]
匹配[
贪婪匹配
-
?
的多重含义- 放在字符后,表示匹配 0 次或 1 次
- 放在分组前,
?:
表示不捕获分组内容 - 用于断言时,
?=
,?!
,?<=
,?<!
表示正向/反向断言 - 放在可变长匹配后,表示非贪婪匹配,即尽可能少地匹配字符
-
示例
1
2
3
4
5
6reg1 = r"a{2,}"
reg2 = r"a{2,}?"
collect(eachmatch(reg1, "aaaaa"))
# 贪婪匹配,匹配到一个元素 "aaaaa"
collect(eachmatch(reg2, "aaaaa"))
# 非贪婪匹配,匹配到两个 "aa", "aa" -
贪婪匹配 vs 即停匹配
比起“非贪婪匹配”,更合适的说法是“匹配即停”。
对于可变长规则,贪婪匹配会尽可能多地匹配字符,而即停匹配在成功匹配到最短的子串后立即停止。1
2
3
4
5
6reg1 = r"a.*b"
reg2 = r"a.*?b"
collect(eachmatch(reg1, "aaaaabaab"))
# 贪婪匹配,匹配到 "aaaaabaab"
collect(eachmatch(reg2, "aaaaabaab"))
# 即停匹配,匹配到 "aaaaab" 和 "aab"
特殊匹配
-
以下规则在 Python 和 Julia 中一致,并与 Linux 的 Perl 兼容,而 shell 中的
grep
使用 POSIX 特殊字符,需要单独处理。 -
常见特殊字符
\w
匹配单词字符,包括[a-zA-Z0-9_]
和汉字等,不包括符号\d
匹配数字,等价于[0-9]
\s
匹配空白字符,包括空格、制表符、换行符等\b
匹配单词边界- 例如:
\babc
匹配以abc
开头的单词abc
,abcd
,.abcdef
- 不匹配
aabc
,_abc
,ababc
\b\w{4}\b
匹配长度为 4 的单词
- 例如:
\W
,\D
,\S
分别匹配\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')]
断言
正则表达式中的断言(Assertions)与 ^
(匹配字符串开头)、$
(匹配字符串结尾)或 \b
(匹配单词边界)一样,都是用于匹配字符串中的某个位置,而不是匹配具体的字符。由于它们只匹配位置而不消耗字符,因此也称为“零宽断言”。
1. 断言类型
断言可以分为四种类型:
- 正向先行断言:
(?=pattern)
- 负向先行断言:
(?!pattern)
- 正向后行断言:
(?<=pattern)
- 负向后行断言:
(?<!pattern)
2. 正向先行断言
正向先行断言 (?=pattern)
匹配的是这样一个位置:该位置之后的字符序列可以匹配给定的 pattern
。例如:
在字符串 a regular expression
中,我们想匹配单词 regular
中的 re
,但不匹配 expression
中的 re
,可以使用以下正则表达式:
1 | re(?=gular) |
这个表达式限定了匹配的 re
之后必须跟着 gular
,但并不消耗 gular
这些字符。如果修改为 re(?=gular)g
,则会匹配 reg
,因为在 re
和 g
之间的位置满足了 gular
的条件。
3. 负向先行断言
负向先行断言 (?!pattern)
匹配的是这样一个位置:该位置之后的字符序列不能匹配给定的 pattern
。例如:
在字符串 regex represents regular expression
中,如果我们想匹配 regex
和 regular
中的 re
,但排除 represents
和 expression
中的 re
,可以使用:
1 | re(?!g) |
这个表达式表示 re
之后不能有字母 g
。
4. 正向后行断言
正向后行断言 (?<=pattern)
匹配的是这样一个位置:该位置之前的字符序列可以匹配给定的 pattern
。例如:
在字符串 regex represents regular expression
中,假设我们想匹配单词中的 re
,但不匹配出现在单词开头的 re
,可以使用:
1 | (?<=\w)re |
这里的断言要求 re
之前必须有一个单词字符(\w
),所以不会匹配 regex
中的 re
。
5. 负向后行断言
负向后行断言 (?<!pattern)
匹配的是这样一个位置:该位置之前的字符序列不能匹配给定的 pattern
。例如:
在字符串 regex represents regular expression
中,如果我们想排除匹配 represents
和 expression
中的 re
,但匹配 regex
中的 re
,可以使用:
1 | (?<!\w)re |
这表示 re
之前不能有任何单词字符。
6. 断言总结
- 正向断言用
=
,表示断言的条件必须成立; - 负向断言用
!
,表示断言的条件不能成立; - 先行断言从当前位置开始向后匹配,限定该位置之后的字符;
- 后行断言从当前位置开始向前匹配,限定该位置之前的字符。
7. 断言示例
为了更好地理解断言,我们可以以字符串 regex represents regular expression
为例,尝试几种断言组合:
- 正向先行断言:
re(?=g)
限定re
之后必须有g
,因此匹配regex
和regular
中的re
,排除了represents
和expression
中的re
。 - 负向先行断言:
re(?!s|p)
限定re
之后不能是s
或p
,匹配了regex
和regular
的re
,排除了represents
和expression
。 - 正向后行断言:
(?<=r)e
限定e
之前必须是r
,匹配所有单词中的e
。 - 负向后行断言:
(?<!p)re
限定re
之前不能是p
,排除了represents
和expression
中的re
。
8. 经典断言应用
我们可以用断言来匹配一个同时包含大小写字母的非空字符串。例如:
1 | (?=.*?[a-z])(?=.*?[A-Z]).+ |
解释:
(?=.*?[a-z])
匹配包含至少一个小写字母的位置;(?=.*?[A-Z])
匹配包含至少一个大写字母的位置;- 最后的
.+
表示字符串本身至少有一个字符。
通过这种方式,两个断言结合,确保字符串同时包含大写和小写字母。类似地,我们可以使用 (?=.*?[a-z]{m,})
来匹配至少包含 m
个小写字母的位置。
Shell
-
grep -E
和egrep
命令
在 Shell 中,可以使用grep -E
或egrep
命令进行正则表达式匹配。需要注意的是,默认的grep
是grep -e
,而-e
只支持基础正则表达式(BRE),这会导致一些高级的正则语法无法使用。例如,grep a?c
在默认模式下不会匹配ac
和c
。 -
POSIX 特殊字符
POSIX 标准定义了一组特殊字符类,这些字符类可用于匹配特定的字符类型。在使用grep
时,可以通过[:class:]
来引用这些字符类。特殊字符 说明 [:alnum:]
匹配任意字母字符和数字( 0-9a-zA-Z
)[:alpha:]
匹配任意字母字符(大写或小写) [:digit:]
匹配数字 0-9
[:graph:]
匹配除空格外的所有可打印字符 [:lower:]
匹配小写字母 a-z
[:upper:]
匹配大写字母 A-Z
[:cntrl:]
匹配控制字符 [:print:]
匹配所有可打印字符,包括空格 [:punct:]
匹配标点符号 [:blank:]
匹配空格和制表符 [:xdigit:]
匹配十六进制数字 [:space:]
匹配所有空白字符(如换行符、空格、制表符) -
外层字符匹配
在使用 POSIX 字符类时,通常需要将它们放在方括号内。例如:1
grep '[[:alpha:]]' filename # 匹配文件中的字母字符
Julia
在 Julia 中,正则表达式通过 r
前缀表示,使用非常灵活,并且支持与 Python 类似的丰富功能。推荐参考 Julia 官方文档。
-
基本使用
在 Julia 中,使用r"..."
来定义正则表达式。例如:1
2
3re = r"^\s*(?:#|\$)"
typeof(re) # 输出: Regex
isconcretetype(Regex) # 输出: true该正则表达式匹配以若干空格开头,后跟
#
或$
的字符串。其具体语法为:^
:匹配行首;\s*
:匹配若干空白字符;(?:...)
:非捕获组,用于组合多个条件;#|\$
:匹配#
或$
。
-
occursin
和match
occursin
用于判断字符串中是否存在匹配项:
1
occursin(r"^\s*(?:#|\$)", "# a comment") # 输出: true
match
用于获取具体的匹配结果:
1
2
3
4
5m = match(r"(\d{2})([a-z]{2})", "aa12bb34cc")
m.match # 输出: "12bb"
m.captures # 输出: ["12", "bb"]
m.offset # 匹配的起始位置
m.offsets # 每个捕获组的起始位置 -
分组命名和回溯引用
Julia 支持命名分组,并且允许通过名称或索引来引用分组:1
2
3m = match(r"(?<tag>\d+):(\d+)", "12:45")
m[:tag] # 使用标签调用分组,输出: "12"
m[2] # 使用索引调用分组,输出: "45"同时,支持回溯调用分组内容:
1
2m = match(r"(?P<hi>\d+):\g<hi>", "12:12") # 用 \g<hi> 调用分组
m = match(r"(?<hi>\d+):\g1", "12:12") # 使用分组序号 -
replace
函数
replace
函数在 Julia 中支持正则表达式,并允许使用捕获组中的内容:1
replace("--12:34--", r"(?<hour>\d+):(?<minute>\d+)" => s"\g<minute>") # 输出: "--34--"
-
eachmatch
函数
eachmatch
返回一个RegexMatch
对象的迭代器,用于多项匹配:1
collect(eachmatch(r"\d\d", "1234")) # 输出: 两个匹配项 ["12", "34"]
-
换行匹配
.
无法匹配换行符,要匹配换行符可以使用[\s\S]
或[\w\W]
:1
match(r"123[\s\S]321", "123\n22\n321") # 输出: RegexMatch("123\n22\n321")
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 |
该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解 |
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
5re.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.groups()) # ('2022', '03', '01')
info.groupdict() # {'year': '2022', 'month': '03', 'day': '01'} -
sub
查找并替换匹配的字符串:1
2
3
4txt = "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
re.findall(r"\w{2}", "2022-03-01") # 返回 ['20', '22', '03', '01']
-
finditer
返回迭代器形式的匹配对象:1
2
3matches = list(re.finditer(r"\w{2}", "2022-03-01"))
for match in matches:
print(match.group()) # 输出匹配的子串 -
split
按照正则表达式拆分字符串:1
re.split(r"\d", "1bb3cc1dd") # 输出: ['', 'bb', 'cc', 'dd']
-
修饰符 支持多种正则修饰符,多个修饰符可用
|
组合使用:1
re.findall(r"ab.*cd", "ab\ndd\nCD", re.I | re.S) # ['ab\ndd\nCD'] | `.` 可匹配换行且不区分大小写
-
常见修饰符表:
修饰符 说明 re.I
匹配时忽略大小写 re.M
多行模式,影响 ^
和$
re.S
使 .
匹配所有字符,包括换行符re.X
允许更灵活的格式,使正则表达式更易于理解
Perl
Perl 是正则表达式的先驱语言之一,其强大的正则功能被很多现代编程语言借鉴。我们可以通过 Shell 中的 grep -P
来使用 Perl 风格的正则表达式。以下是 Perl 和 Python 正则表达式的一些比较和补充:
-
Perl 中的正则语法
Perl 使用类似于 Python 的正则表达式语法,但增加了一些独有的功能。比如,Perl 支持更复杂的嵌套分组和条件匹配。 -
捕获和命名分组
Perl 支持命名捕获组和回溯引用,类似于 Python:1
2
3
4# Perl 写法
if ($str =~ /(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})/) {
print "$+{year}/$+{month}/$+{day}\n"; # 使用命名分组
} -
正则表达式修饰符
Perl 提供了强大的修饰符功能,包括m
(多行模式)、s
(单行模式)、x
(扩展模式)等,类似 Python 的修饰符机制:1
2$str =~ /pattern/i; # 大小写不敏感匹配
$str =~ /pattern/s; # 允许 `.` 匹配换行符 -
grep -P
在 shell 中,grep -P
允许你使用 Perl 的正则表达式语法来进行文本处理:1
grep -P '\d{4}-\d{2}-\d{2}' file.txt # 使用 Perl 风格的正则表达式匹配日期
-
扩展和嵌套正则
Perl 支持复杂的条件表达式和递归模式匹配,例如在括号匹配或 XML 解析等应用场景中非常强大。