正则表达式(Regex)是一种强大的文本提取工具——从非结构化文本中提取特定子串,如电话号码、电子邮件地址、日期或键值对。与简单的字符串搜索不同,正则表达式允许你定义灵活的模式来适应格式变化。
本指南涵盖基于正则表达式的提取核心概念,演示多种语言(C++、Java、Python)的真实示例,突出常见陷阱,并展示如何使用我们的正则测试器调试你的模式。

为什么使用正则表达式进行文本提取?
文本提取是数据处理、日志分析和表单验证中的常见任务。正则表达式之所以出色,是因为:
- 灵活性:匹配像“3位数字、短横、4位数字”(
\d{3}-\d{4})这样的模式,而不是固定字符串。 - 捕获组:使用括号
()只提取你关心的部分。 - 否定和重复:排除不需要的字符或匹配重复的结构。
没有正则表达式,你需要编写几十行手动解析代码。有了正则表达式,一个模式就能完成工作。
提取的核心正则概念
在深入代码之前,了解这些关键构建块:
| 概念 | 语法 | 示例 | 匹配 |
|---|---|---|---|
| 数字 | \d |
\d{3} |
123 |
| 单词字符 | \w |
\w+ |
hello |
| 空白 | \s |
\s |
空格、制表符 |
| 任意字符 | . |
a.c |
abc, a c |
| 零次或多次 | * |
ab*c |
ac, abc, abbc |
| 一次或多次 | + |
ab+c |
abc, abbc(不包括 ac) |
| 捕获组 | (...) |
(\d{3}) |
从 123-456 中捕获 123 |
| 非捕获组 | (?:...) |
(?:\d{3}) |
分组但不捕获 |
| 前瞻 | (?=...) |
\d(?=px) |
5px 中的 5 |
| 后顾 | (?<=...) |
(?<=\$)\d+ |
$100 中的 100 |
完整示例:从电话簿中提取姓名和号码
假设你有一个文本文件,条目如下:
Alice: 555-1234
Bob: 555-5678
Charlie: 555-9012
你想分别提取每个姓名及其对应的电话号码。
第一步:定义模式
每行遵循模式:姓名: 号码。我们将使用带有两个捕获组的正则表达式:
^(\w+):\s*(\d{3}-\d{4})$
^— 行首(\w+)— 捕获一个或多个单词字符(姓名):— 字面冒号\s*— 可选空白(\d{3}-\d{4})— 捕获电话号码(3位数字、短横、4位数字)$— 行尾
第二步:在不同语言中提取
C++(使用 <regex>)
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "Alice: 555-1234\nBob: 555-5678\nCharlie: 555-9012\n";
std::regex pattern(R"(^(\w+):\s*(\d{3}-\d{4})$)", std::regex::multiline);
std::smatch matches;
std::string::const_iterator searchStart(text.cbegin());
while (std::regex_search(searchStart, text.cend(), matches, pattern)) {
std::cout << "Name: " << matches[1] << ", Number: " << matches[2] << std::endl;
searchStart = matches.suffix().first;
}
return 0;
}
输出:
Name: Alice, Number: 555-1234
Name: Bob, Number: 555-5678
Name: Charlie, Number: 555-9012
Java(使用 java.util.regex)
import java.util.regex.*;
public class ExtractPhonebook {
public static void main(String[] args) {
String text = "Alice: 555-1234\nBob: 555-5678\nCharlie: 555-9012\n";
Pattern pattern = Pattern.compile("^(\\w+):\\s*(\\d{3}-\\d{4})quot;, Pattern.MULTILINE);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Name: " + matcher.group(1) + ", Number: " + matcher.group(2));
}
}
}
Python(使用 re)
import re
text = """Alice: 555-1234
Bob: 555-5678
Charlie: 555-9012"""
pattern = r"^(\w+):\s*(\d{3}-\d{4})quot;
for match in re.finditer(pattern, text, re.MULTILINE):
print(f"Name: {match.group(1)}, Number: {match.group(2)}")
所有三个示例产生相同的输出。关键区别在于每种语言如何处理正则语法(例如,Python 中的原始字符串,Java 中的双反斜杠,C++ 中的原始字符串字面量 R"(...)")。
Unicode 与国际文本
从非英语文本中提取时,你需要 Unicode 感知的模式。例如,提取中文字符或日语平假名:
| 语言 | Unicode 属性 | 示例模式 | 匹配 |
|---|---|---|---|
| 中文 | \p{script=Han} |
\p{script=Han}+ |
你好 |
| 日语平假名 | \p{script=Hiragana} |
\p{script=Hiragana}+ |
あいう |
| 任何字母 | \p{L} |
\p{L}+ |
hello你好 |
Java 示例(JDK 7+):
Pattern p = Pattern.compile("\\p{script=Han}+");
Matcher m = p.matcher("Hello 世界!");
while (m.find()) {
System.out.println(m.group()); // 输出 "世界"
}
Python 示例:
import re
pattern = re.compile(r"\p{Han}+", re.UNICODE) # Python 通过 regex 模块支持 \p{},而非 re
# 使用 `regex` 库以获得完整的 Unicode 属性支持:pip install regex
import regex
pattern = regex.compile(r"\p{Han}+")
for match in pattern.findall("Hello 世界!"):
print(match) # 输出 "世界"
C++ 示例(C++11 的 std::regex 对 Unicode 支持有限;使用 boost::regex 或 ICU 以获得完整支持)。
常见陷阱
- 贪婪与懒惰匹配:
.*尽可能多地匹配;使用.*?进行最小匹配。例如,从123 456中提取第一个数字,(\d+).*捕获123但.*吃掉其余部分;使用(\d+).*?(\d+)来获取两者。 - 代码中的转义:在 Java 字符串中,反斜杠必须加倍(
\\d)。在 C++ 原始字符串字面量R"(...)"中避免了这一点。Python 原始字符串r"..."也有帮助。 - 没有多行模式的锚点:默认情况下,
^和$匹配字符串的开始/结束。使用多行标志来匹配行边界。 - 重叠匹配:默认情况下,
find()查找非重叠匹配。对于重叠匹配,使用前瞻技巧。 - 性能:具有大量分支或回溯的复杂模式可能很慢。尽可能使用原子组
(?>...)或占有量词++。
使用我们的正则测试器实时交互测试你的模式。
常见问题
如何从文本中提取所有电子邮件地址?
使用类似 [\w.-]+@[\w.-]+\.\w+ 的模式。注意,完全符合 RFC 的正则表达式非常复杂;这个模式覆盖了大多数实际场景。
捕获组和非捕获组有什么区别?
捕获组 (...) 存储匹配的子串供以后使用(例如,\1 反向引用或 group(1))。非捕获组 (?:...) 对模式部分进行分组而不存储,节省内存并简化反向引用编号。
如何处理多行文本提取?
启用多行标志(Python 中的 re.MULTILINE,Java 中的 Pattern.MULTILINE,C++ 中的 std::regex::multiline),使 ^ 和 $ 匹配行边界而不是字符串边界。
可以提取重叠匹配吗?
默认情况下,正则引擎查找非重叠匹配。要查找重叠匹配,使用前瞻:(?=(pattern))。前瞻在不消耗字符的情况下捕获匹配,允许下一次搜索从后一个字符开始。
为什么我的正则表达式在测试器中有效但在代码中无效?
检查字符串转义:在 Java 中,你需要 \\d;在 C++ 原始字面量 R"(\d)" 中有效。同时确保你使用了正确的标志(例如,多行、不区分大小写)。
结论
正则表达式文本提取是一项多才多艺的技能,可以节省时间并降低代码复杂度。通过掌握捕获组、Unicode 属性和特定语言的语法,你可以优雅地处理大多数提取任务。从简单的模式开始,使用我们的正则测试器逐步测试,并始终考虑空匹配或特殊字符等边缘情况。