当你需要在一段文本中找出所有邮箱地址、验证用户输入的手机号格式、或者批量替换代码中的变量名时,正则表达式就是你的利器。它是一种用来描述字符串模式的工具,虽然语法看起来有些神秘,但一旦掌握,你会发现它能解决大量看似复杂的文本处理问题。
正则表达式是什么
正则表达式(Regular Expression,常简写为regex或regexp)是一种用于描述字符串模式的表达式。它定义了一个搜索模式,可以用来检查一个字符串是否匹配某个模式、从字符串中提取符合模式的子串、或者替换符合模式的文本。
正则表达式的概念源于理论计算机科学。1951年,美国数学家Stephen Cole Kleene在研究神经网络和有限自动机时,提出了一种描述"正则语言"的数学符号。1968年,Ken Thompson将这种数学符号引入到计算机程序中,实现了QED文本编辑器中的模式匹配功能。后来,这一功能被引入Unix系统,诞生了著名的grep工具(grep这个名字来自ed编辑器的命令"g/re/p",意为"全局搜索正则表达式并打印匹配行")。
今天,正则表达式已经成为几乎所有编程语言和文本处理工具的标准功能。无论是Python、JavaScript、Java,还是文本编辑器、命令行工具,都支持正则表达式。
从字面匹配开始
最简单的正则表达式就是字面文本本身。如果你想匹配字符串中的"hello",那么正则表达式就是hello。
文本:hello world, hello everyone
正则:hello
匹配:hello(出现两次)
这种字面匹配非常直观:正则表达式中的每个字符都要与目标文本中的对应字符完全一致。但如果正则表达式只能做字面匹配,那它和普通的字符串查找没什么区别。正则表达式的强大之处在于它的元字符——这些特殊字符赋予了正则表达式描述复杂模式的能力。
元字符:正则表达式的核心语法
元字符是正则表达式中具有特殊含义的字符。它们不再代表字面意义,而是用来构建更复杂的匹配模式。
点号(.):匹配任意单个字符
点号是最常用的元字符,它可以匹配除换行符外的任意单个字符。
文本:cat, cut, cot, cart
正则:c.t
匹配:cat, cut, cot
注意,c.t不会匹配"cart",因为点号只能匹配一个字符,而"cart"在"c"和"t"之间有两个字符"ar"。
星号(*):匹配零次或多次
星号表示前面的元素可以出现零次或多次。
文本:ac, abc, abbc, abbbc
正则:ab*c
匹配:ac, abc, abbc, abbbc
这里b*表示字母"b"可以出现零次或任意多次,所以"ac"也能匹配(零个b)。
加号(+):匹配一次或多次
加号与星号类似,但要求前面的元素至少出现一次。
文本:ac, abc, abbc, abbbc
正则:ab+c
匹配:abc, abbc, abbbc
这次"ac"不能匹配了,因为b+要求至少有一个"b"。
问号(?):匹配零次或一次
问号表示前面的元素是可选的,可以出现零次或一次。
文本:color, colour
正则:colou?r
匹配:color, colour
这个例子展示了问号的实用场景:匹配两种拼写方式的单词。
竖线(|):或运算
竖线用于指定多个可选模式,类似于逻辑"或"。
文本:cat, dog, bird
正则:cat|dog
匹配:cat, dog
方括号([]):字符类
方括号用于定义一个字符集合,匹配其中任意一个字符。
文本:cat, cut, cot
正则:c[auo]t
匹配:cat, cut, cot
在方括号内,可以使用连字符-表示范围:
文本:a1, b2, c3, d4
正则:[a-z][0-9]
匹配:a1, b2, c3, d4
如果在方括号内的第一个字符是^,则表示匹配不在集合中的字符:
文本:a1, b2, c3, xY
正则:[^0-9]
匹配:a, b, c, x, Y(非数字字符)
脱字符(^)和美元符($):行锚点
^匹配字符串的开头,$匹配字符串的结尾。
文本:hello world
正则:^hello
匹配:hello(仅在开头)
文本:hello world
正则:world$
匹配:world(仅在结尾)
这两个锚点常用于验证整个字符串的格式。例如,^\d{11}$可以匹配一个恰好11位的数字字符串。
反斜杠(\):转义字符
当你需要匹配元字符本身的字面意义时,需要用反斜杠进行转义。
文本:3.14, 3+14, 3*14
正则:3\.14
匹配:3.14(只匹配点号,不匹配加号或星号)
预定义字符类
正则表达式提供了一些常用的预定义字符类,让模式更简洁:
| 符号 | 含义 | 等价表示 |
|---|---|---|
\d |
数字字符 | [0-9] |
\D |
非数字字符 | [^0-9] |
\w |
单词字符(字母、数字、下划线) | [a-zA-Z0-9_] |
\W |
非单词字符 | [^a-zA-Z0-9_] |
\s |
空白字符(空格、制表符、换行等) | [ \t\n\r\f\v] |
\S |
非空白字符 | [^ \t\n\r\f\v] |
使用这些预定义字符类,可以让正则表达式更加简洁易读:
文本:file_123, file_abc, file_456
正则:file_\d+
匹配:file_123, file_456
量词:精确控制匹配次数
除了*、+、?,正则表达式还提供了更精确的量词语法:
固定次数:{n}
文本:a, aa, aaa, aaaa
正则:a{3}
匹配:aaa
范围次数:{n,m}
文本:a, aa, aaa, aaaa
正则:a{2,3}
匹配:aa, aaa
最少次数:{n,}
文本:a, aa, aaa, aaaa
正则:a{2,}
匹配:aa, aaa, aaaa
贪婪与非贪婪
默认情况下,量词是贪婪的,会尽可能多地匹配字符。在量词后面加上?可以使其变为非贪婪(也叫懒惰),尽可能少地匹配字符。
文本:<div>content</div>
正则:<.*>
匹配:<div>content</div>(贪婪,匹配整个字符串)
文本:<div>content</div>
正则:<.*?>
匹配:<div> 和 </div>(非贪婪,分两次匹配)
理解贪婪与非贪婪的区别,对于处理HTML、XML等标签文本非常重要。
锚点与边界
除了^和$,正则表达式还提供了单词边界锚点。
单词边界:\b
\b匹配单词字符和非单词字符之间的位置,也就是单词的边界。
文本:cat catalog concat
正则:\bcat\b
匹配:cat(只匹配独立的单词cat)
这对于避免部分匹配很有用。上例中,“catalog"和"concat"中的"cat"不会被匹配,因为它们不是独立的单词。
非单词边界:\B
\B与\b相反,匹配不是单词边界的位置。
文本:cat catalog concat
正则:\Bcat
匹配:concat中的cat(非单词边界开头的cat)
分组与捕获
圆括号()用于创建分组。分组有两个主要用途:将多个元素作为一个整体、以及捕获匹配的文本。
基本分组
文本:ababab
正则:(ab)+
匹配:ababab
这里(ab)+表示"ab"这个整体可以出现一次或多次。
捕获组与反向引用
每个分组会自动成为一个捕获组,可以使用\1、\2等引用前面捕获的内容:
文本:word word
正则:(\w+)\s\1
匹配:word word(\1引用第一个分组捕获的word)
这个模式可以用来匹配重复的单词:(\w+)\s+\1会匹配两个相同的单词。
非捕获分组
如果只需要分组功能而不需要捕获,可以使用(?:...):
文本:abcabc
正则:(?:abc)+
匹配:abcabc
非捕获分组不会影响捕获组的编号,在复杂表达式中可以提高性能。
转义字符速查
在正则表达式中,以下字符是元字符,需要用反斜杠转义才能匹配字面意义:
. * + ? ^ $ | \ ( ) [ ] { }
例如,要匹配字符串"price: $100",需要写成`price: \$100`。
在编程语言中使用正则表达式
正则表达式被几乎所有主流编程语言支持,但语法和函数名略有不同。
JavaScript
// 创建正则表达式
const regex = /\d+/g; // 字面量方式
const regex2 = new RegExp('\\d+', 'g'); // 构造函数方式
// 测试是否匹配
regex.test('123'); // true
// 查找匹配
'abc 123 def 456'.match(/\d+/g); // ['123', '456']
// 替换
'abc 123'.replace(/\d+/, 'XXX'); // 'abc XXX'
Python
import re
# 查找所有匹配
re.findall(r'\d+', 'abc 123 def 456') # ['123', '456']
# 搜索第一个匹配
match = re.search(r'\d+', 'abc 123')
if match:
print(match.group()) # 123
# 替换
re.sub(r'\d+', 'XXX', 'abc 123') # 'abc XXX'
Java
import java.util.regex.*;
// 创建Pattern和Matcher
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher("abc 123 def 456");
// 查找所有匹配
while (matcher.find()) {
System.out.println(matcher.group());
}
// 输出:123, 456
常用正则表达式示例
匹配手机号
^1[3-9]\d{9}$
解释:以1开头,第二位是3-9中的一个数字,后面跟着9位数字。
匹配邮箱地址
^[\w.-]+@[\w.-]+\.\w+$
解释:这是简化版的邮箱正则表达式,实际应用中可能需要更复杂的模式。
匹配日期格式(YYYY-MM-DD)
^\d{4}-\d{2}-\d{2}$
匹配IP地址
^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$
注意:这个模式只检查格式,不检查数值是否在有效范围内(0-255)。
匹配URL
^https?://[\w.-]+(/[\w./-]*)?$
常见陷阱与最佳实践
1. 避免过度使用贪婪量词
贪婪量词可能导致意外的长匹配。如果只需要匹配到第一个终止符,使用非贪婪模式:
文本:<div><p>text</p></div>
正则:<div>.*</div> # 贪婪:匹配整个字符串
正则:<div>.*?</div> # 非贪婪:只匹配<div><p>text</p></div>
2. 使用原始字符串
在Python等语言中,使用原始字符串(前缀r)可以避免双重转义:
# 需要双重转义
re.search('\\d+', text)
# 使用原始字符串更清晰
re.search(r'\d+', text)
3. 复杂正则表达式要添加注释
对于复杂的正则表达式,使用注释模式(如Python的re.VERBOSE)提高可读性:
pattern = r'''
^ # 字符串开头
\d{4} # 年份:4位数字
- # 分隔符
\d{2} # 月份:2位数字
- # 分隔符
\d{2} # 日期:2位数字
$ # 字符串结尾
'''
re.match(pattern, '2024-03-08', re.VERBOSE)
4. 正则表达式不是万能的
有些任务用正则表达式并不是最佳选择:
- 解析嵌套结构(如HTML标签、括号配对)
- 验证复杂的语义规则(如日期是否有效)
- 处理自然语言
在这些场景中,使用专门的解析器或验证库会更可靠。
测试与调试工具
学习正则表达式时,使用在线测试工具可以帮助理解匹配过程:
- regex101.com:支持多种语言风格,提供详细的匹配解释
- regexr.com:交互式学习工具,带有速查表
- debuggex.com:可视化展示正则表达式的状态机
这些工具不仅能帮助你调试正则表达式,还能加深对匹配机制的理解。
小结
正则表达式是一项需要反复练习才能熟练掌握的技能。从简单的字面匹配开始,逐步掌握元字符、字符类、量词、分组等概念,你会发现正则表达式能解决大量文本处理问题。
记住几个核心原则:
- 从简单开始,逐步构建复杂模式
- 使用在线工具测试和调试
- 考虑可读性,必要时添加注释
- 知道什么时候不该用正则表达式
掌握了这些基础知识,你已经可以处理日常开发中大部分的文本匹配需求了。接下来,就是在实际项目中不断练习和应用。
参考资料
- Regular expression - Wikipedia. https://en.wikipedia.org/wiki/Regular_expression
- RexEgg - Regex Tutorial. https://www.rexegg.com/
- MDN Web Docs - Regular expressions. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
- Microsoft Learn - Regular Expression Language. https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference
- Python Documentation - re module. https://docs.python.org/3/library/re.html
- Jan Goyvaerts. Regular-Expressions.info. https://www.regular-expressions.info/
- Ken Thompson. 1968. Programming Techniques: Regular expression search algorithm. Communications of the ACM.