正则表达式(Regex)是一种强大的工具,用于在文本中进行模式匹配、搜索、替换和验证。掌握其规则需要理解其基本构成元素和语法。下面详细解析正则表达式的书写规则:
核心概念:
正则表达式由普通字符(如字母、数字)和特殊字符(称为元字符,具有特殊含义)组成。通过组合这些字符,你可以定义复杂的搜索模式。
一、基本匹配
- 普通字符: 绝大多数字母(
a-z
,A-Z
)、数字(0-9
)和部分标点符号(如空格)直接匹配它们自身。- 示例:
cat
匹配字符串中的 “cat”。
- 示例:
- 字面匹配特殊字符: 元字符(如
.
,*
,?
,+
,^
,$
,(
,)
,[
,]
,{
,}
,|
,\
)本身具有特殊含义。要匹配它们自身,需要在前面加上反斜杠\
进行转义。- 示例:
\.
匹配一个实际的句点 “.”;\*
匹配一个实际的星号 “*”;\\
匹配一个实际的反斜杠 “”。
- 示例:
二、元字符及其含义
1. 单个字符匹配
.
(点号): 匹配任意单个字符(除了换行符\n
,除非使用s
修饰符)。- 示例:
.at
匹配 “cat”, “hat”, “bat”, “mat”, “@at”, " at" 等。
- 示例:
[...]
(字符类): 匹配方括号内的任意一个字符。[aeiou]
匹配任意一个元音字母。[a-z]
匹配任意一个小写字母(范围)。[A-Za-z0-9]
匹配任意一个字母或数字。[^...]
(脱字符在开头):否定字符类,匹配不在方括号内的任意一个字符。[^aeiou]
匹配任意一个非元音字母(包括辅音、数字、符号等)。[^0-9]
匹配任意一个非数字字符。
\d
: 匹配任意一个数字。等价于[0-9]
。\D
: 匹配任意一个非数字字符。等价于[^0-9]
。\w
: 匹配任意一个单词字符(通常包括字母、数字、下划线_
)。等价于[a-zA-Z0-9_]
。\W
: 匹配任意一个非单词字符。等价于[^a-zA-Z0-9_]
。\s
: 匹配任意一个空白字符(包括空格、制表符\t
、换行符\n
、回车符\r
、换页符\f
)。\S
: 匹配任意一个非空白字符。
2. 位置锚点
^
(脱字符):- 在正则表达式开头:匹配字符串的开始位置。
- 示例:
^Hello
匹配以 “Hello” 开头的字符串。
- 示例:
- 在字符类
[...]
内部开头:表示否定(见上文)。
- 在正则表达式开头:匹配字符串的开始位置。
$
(美元符): 匹配字符串的结束位置(或字符串结束前的换行符\n
之前的位置,如果使用m
修饰符)。- 示例:
world$
匹配以 “world” 结尾的字符串。
- 示例:
\b
: 匹配一个单词边界(单词字符\w
与非单词字符\W
之间的位置,或者字符串的开始/结束位置)。- 示例:
\bcat\b
匹配整个单词 “cat”,不会匹配 “catalog” 或 “concatenate” 中的 “cat”。
- 示例:
\B
: 匹配一个非单词边界。
3. 量词(指定前面元素出现的次数)
*
(星号): 匹配前面的元素零次或多次。- 示例:
ab*c
匹配 “ac”, “abc”, “abbc”, “abbbc”, …(0个或多个 ‘b’)。
- 示例:
+
(加号): 匹配前面的元素一次或多次。- 示例:
ab+c
匹配 “abc”, “abbc”, “abbbc”, …(至少1个 ‘b’),不匹配 “ac”。
- 示例:
?
(问号):- 匹配前面的元素零次或一次(即可选)。
- 示例:
colou?r
匹配 “color” 或 “colour”(‘u’ 可有可无)。
- 示例:
- 用在量词
*
,+
,?
,{n}
,{n,}
,{n,m}
之后:表示非贪婪匹配(也称为懒惰匹配)。默认的贪婪匹配会匹配尽可能长的字符串,非贪婪匹配会匹配尽可能短的字符串。- 示例:
a.*?b
匹配 “a” 和 “b” 之间最短的字符串。对于 “axb yb”,贪婪匹配a.*b
会匹配整个 “axb yb”,而非贪婪匹配a.*?b
会先匹配 “axb”。
- 示例:
- 匹配前面的元素零次或一次(即可选)。
{n}
: 匹配前面的元素恰好 n 次。- 示例:
a{3}
匹配 “aaa”。
- 示例:
{n,}
: 匹配前面的元素至少 n 次。- 示例:
a{2,}
匹配 “aa”, “aaa”, “aaaa”, …(至少2个 ‘a’)。
- 示例:
{n,m}
: 匹配前面的元素至少 n 次,但不超过 m 次。- 示例:
a{2,4}
匹配 “aa”, “aaa”, “aaaa”(2到4个 ‘a’)。
- 示例:
4. 分组和捕获
(...)
(圆括号):- 分组: 将多个元素组合成一个子表达式,以便应用量词或逻辑操作。
- 示例:
(ab)+
匹配 “ab”, “abab”, “ababab”, …(一个或多个 “ab” 序列)。
- 示例:
- 捕获: 匹配括号内子表达式的内容,并存储在捕获组中,后续可通过回溯引用(
\1
,\2
, …)或在处理结果中访问(如match.group(1)
)。- 示例:
(a)(b)\1\2
匹配 “abab”(\1
引用第一个捕获组 “(a)” 匹配的 ‘a’,\2
引用第二个捕获组 “(b)” 匹配的 ‘b’)。
- 示例:
- 分组: 将多个元素组合成一个子表达式,以便应用量词或逻辑操作。
(?:...)
(非捕获分组): 只进行分组,不捕获内容。性能通常优于捕获分组,当你不需要引用匹配内容时使用。- 示例:
(?:ab)+
匹配 “ab”, “abab”, …,但不会创建捕获组。
- 示例:
5. 逻辑操作(选择)
|
(竖线): 表示或(alternation)。匹配左边或右边的子表达式。- 示例:
cat|dog|bird
匹配 “cat”、“dog” 或 “bird”。 - 示例:
gr(a|e)y
匹配 “gray” 或 “grey”(等价于gr[ae]y
)。
- 示例:
6. 转义序列
\
+ 特定字符: 除了用于转义元字符本身(如\.
,\*
),还用于表示特殊字符:\n
:换行符 (LF)\r
:回车符 (CR)\t
:水平制表符 (Tab)\f
:换页符\v
:垂直制表符(某些实现)\xHH
:匹配 ASCII 码为十六进制值HH
的字符(如\x41
匹配 ‘A’)。\uHHHH
:匹配 Unicode 码点为十六进制值HHHH
的字符(如\u00A9
匹配版权符号 ©)。\cX
:匹配控制字符(如\cC
匹配 Ctrl-C)。
7. 预定义字符类(部分实现)
[:alpha:]
:字母(等价于[a-zA-Z]
)[:digit:]
:数字(等价于[0-9]
或\d
)[:alnum:]
:字母或数字(等价于[a-zA-Z0-9]
)[:space:]
:空白字符(等价于\s
)[:lower:]
:小写字母[:upper:]
:大写字母[:punct:]
:标点符号[:graph:]
:可见字符(非空白、非控制字符)[:print:]
:可打印字符(包括空格)[:cntrl:]
:控制字符[:xdigit:]
:十六进制数字([0-9a-fA-F]
)- 注意: 这些通常需要在POSIX字符类中使用,形式为
[[:digit:]]
。它们的具体定义可能因语言/环境略有差异。
8. 零宽断言(Lookaround Assertions)
这些结构用于检查一个位置是否满足某些条件,但不消耗任何字符(即它们匹配的是位置,而不是文本)。
(?=...)
(肯定顺序环视 / 正向先行断言): 断言当前位置之后能匹配表达式...
。- 示例:
Windows (?=95|98|NT|2000)
匹配 "Windows " 仅当其后紧跟 “95”, “98”, “NT” 或 “2000” 时(匹配结果只包含 "Windows ")。
- 示例:
(?!...)
(否定顺序环视 / 负向先行断言): 断言当前位置之后不能匹配表达式...
。- 示例:
Windows (?!95|98|NT|2000)
匹配 "Windows " 仅当其后不紧跟 “95”, “98”, “NT” 或 “2000” 时。
- 示例:
(?<=...)
(肯定逆序环视 / 正向后行断言): 断言当前位置之前能匹配表达式...
。- 示例:
(?<=95|98|NT|2000) Windows
匹配 " Windows" 仅当其前是 “95”, “98”, “NT” 或 “2000” 时(匹配结果只包含 " Windows")。
- 示例:
(?<!...)
(否定逆序环视 / 负向后行断言): 断言当前位置之前不能匹配表达式...
。- 示例:
(?<!95|98|NT|2000) Windows
匹配 " Windows" 仅当其前不是 “95”, “98”, “NT” 或 “2000” 时。
- 示例:
- 注意: 后行断言
(?<=...)
和(?<!...)
在很多实现中(尤其是JavaScript)对子表达式的长度或复杂性有限制(例如不能是任意长度或包含量词)。
三、修饰符(Flags / Options)
修饰符通常添加在正则表达式结束分隔符之后(如 /pattern/flags
),用于改变匹配行为的全局规则。
i
(ignore case): 忽略大小写。[A-Z]
也会匹配小写字母,反之亦然。g
(global): 全局匹配。查找所有匹配项,而不仅仅是第一个。m
(multiline): 多行模式。使^
和$
分别匹配每一行的开头和结尾(由换行符\n
或回车符\r
界定),而不仅仅是整个字符串的开头和结尾。s
(single line / dotall): 点号.
将匹配包括换行符\n
在内的所有字符(默认不匹配换行符)。u
(unicode): 启用完整的 Unicode 支持。正确处理四字节的 UTF-16 编码字符(如表情符号),并允许在模式中使用\u{...}
表示法。y
(sticky): 粘性匹配。要求匹配必须从目标字符串的当前索引位置(lastIndex
) 开始。主要用于高效的流式解析。x
(extended / verbose): 忽略模式中的空白和注释(#
到行尾),提高复杂正则的可读性(支持度较低,Python 的re.VERBOSE
或 Perl/PHP 的/x
修饰符支持)。
四、书写技巧和注意事项
- 明确目标: 清晰定义你要匹配什么模式。
- 从简单开始: 先写核心匹配部分,再逐步添加边界条件、量词、分组等。
- 测试!测试!测试! 使用在线正则表达式测试工具(如 regex101.com, regexr.com)或编程语言中的调试功能,用各种正面和负面测试用例验证你的正则表达式。
- 可读性:
- 对于复杂正则,使用
x
修饰符(如果支持)或添加注释(如(?# 这是一个注释)
)。 - 合理使用空格(如果
x
修饰符可用)或换行(在支持多行模式的编辑器中)。 - 使用非捕获分组
(?:...)
代替不必要的捕获分组(...)
。
- 对于复杂正则,使用
- 效率:
- 避免嵌套量词(如
(a+)+
)或可能导致灾难性回溯的模式。这会使匹配时间指数级增长。 - 优先使用具体字符类
[abc]
而不是点号.
(如果可能)。 - 使用锚点
^
,$
,\b
缩小匹配范围。 - 谨慎使用回溯引用,它们可能影响效率。
- 考虑使用原子分组
(?>...)
或占有量词*+
,++
,?+
,{n,m}+
(如果支持)来防止回溯。例如a++
等价于(?>a+)
。
- 避免嵌套量词(如
- 转义: 注意在字符串中写正则时,编程语言本身可能也需要转义反斜杠。例如,匹配一个反斜杠需要写成
"\\\\"
(字符串中的\\
表示一个\
,正则引擎看到的就是\
)。 - 特定语言差异: 不同编程语言(Perl, Python, JavaScript, Java, .NET, PHP等)的正则引擎在特性支持(如后行断言、原子分组、修饰符名称)和具体行为上可能存在细微差异。查阅目标语言的文档。
- 安全: 避免使用不受信任用户提供的正则表达式,尤其是复杂的或可能导致回溯爆炸的表达式(ReDoS 攻击)。
五、综合示例
- 匹配简单邮箱(非常基础,不完美):
^\w+@\w+\.\w+$
^
字符串开始\w+
1个或多个单词字符(用户名)@
字面 @ 符号\w+
1个或多个单词字符(域名)\.
字面点号 (.)\w+
1个或多个单词字符(顶级域名,如 com, org)$
字符串结束
- 匹配日期 (YYYY-MM-DD):
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$
^
开始\d{4}
4位数字(年)-
连字符(0[1-9]|1[0-2])
分组:0[1-9]
(01-09) 或1[0-2]
(10-12)(月)-
连字符(0[1-9]|[12][0-9]|3[01])
分组:0[1-9]
(01-09) 或[12][0-9]
(10-29) 或3[01]
(30-31)(日)$
结束
- 匹配双引号内的文本:
"([^"]*)"
或"([^"\\]*(?:\\.[^"\\]*)*)"
(支持转义引号,如\"
) - 移除HTML标签(非常简化):
</?[^>]+>
(使用g
标志进行全局替换为''
)