当你需要在一段文本中找出所有邮箱地址、验证用户输入的手机号格式、或者批量替换代码中的变量名时,正则表达式就是你的利器。它是一种用来描述字符串模式的工具,虽然语法看起来有些神秘,但一旦掌握,你会发现它能解决大量看似复杂的文本处理问题。

正则表达式是什么

正则表达式(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:可视化展示正则表达式的状态机

这些工具不仅能帮助你调试正则表达式,还能加深对匹配机制的理解。

小结

正则表达式是一项需要反复练习才能熟练掌握的技能。从简单的字面匹配开始,逐步掌握元字符、字符类、量词、分组等概念,你会发现正则表达式能解决大量文本处理问题。

记住几个核心原则:

  • 从简单开始,逐步构建复杂模式
  • 使用在线工具测试和调试
  • 考虑可读性,必要时添加注释
  • 知道什么时候不该用正则表达式

掌握了这些基础知识,你已经可以处理日常开发中大部分的文本匹配需求了。接下来,就是在实际项目中不断练习和应用。


参考资料

  1. Regular expression - Wikipedia. https://en.wikipedia.org/wiki/Regular_expression
  2. RexEgg - Regex Tutorial. https://www.rexegg.com/
  3. MDN Web Docs - Regular expressions. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
  4. Microsoft Learn - Regular Expression Language. https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference
  5. Python Documentation - re module. https://docs.python.org/3/library/re.html
  6. Jan Goyvaerts. Regular-Expressions.info. https://www.regular-expressions.info/
  7. Ken Thompson. 1968. Programming Techniques: Regular expression search algorithm. Communications of the ACM.