64. STL与正则表达式:regex使用、模式匹配、替换操作
正则表达式这东西,说实话,我早年写C++时一直觉得它是个「黑魔法」。直到有一次接手一个日志分析模块,需要从几十万行服务器日志里提取IP、时间戳和错误码——用字符串手工解析?那代码写出来我自己都不想看第二遍。后来老老实实学了<regex>,嗯,真香。
今天我们就来聊聊C++ STL里的正则表达式库。它不像Python或JavaScript里那么「开箱即用」,但一旦掌握了套路,处理文本的效率能提升一个档次。
正则表达式的基本概念
正则表达式,说白了就是一套「模式匹配」的规则语言。你给它一个字符串,它告诉你:这个字符串里有没有符合规则的部分?如果有,在哪里?是什么?
C++11开始,STL正式引入了<regex>头文件。它支持多种语法风格:ECMAScript(默认)、basic、extended、awk、grep、egrep。我个人习惯用ECMAScript,因为它最接近我们平时在JavaScript或Python里用的写法。
| 语法类型 | 说明 | 使用场景 |
|---|---|---|
| ECMAScript | 默认语法,功能最全 | 日常开发首选 |
| basic | POSIX基本正则 | 兼容老旧系统 |
| extended | POSIX扩展正则 | 部分Unix工具 |
| awk/grep/egrep | 对应工具语法 | 移植已有规则 |
核心类与使用流程
C++正则库有三个核心类,你得记住它们:
std::regex—— 代表一个正则表达式对象std::smatch—— 存储匹配结果(针对std::string)std::regex_iterator—— 遍历所有匹配项
使用流程其实就三步:构造正则对象 → 执行匹配 → 处理结果。听起来简单,但坑不少。我们一个个来看。
模式匹配:regex_match vs regex_search
这两个函数经常有人搞混。我刚开始学的时候也踩过坑。
std::regex_match:要求整个字符串完全匹配正则模式std::regex_search:只要字符串中有一部分匹配即可
举个例子你就明白了:
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "我的手机号是13812345678,请保存。";
std::regex pattern(R"(\d{11})"); // 11位数字
// regex_match:整个字符串必须是11位数字 → 不匹配
if (std::regex_match(text, pattern)) {
std::cout << "match成功\n";
} else {
std::cout << "match失败\n"; // 会走这里
}
// regex_search:只要包含11位数字即可 → 匹配
std::smatch result;
if (std::regex_search(text, result, pattern)) {
std::cout << "找到手机号: " << result.str() << "\n";
}
return 0;
}
输出结果:
match失败
找到手机号: 13812345678
捕获组与子匹配
正则表达式里用括号()可以定义捕获组。这在提取结构化数据时特别有用。比如从日志里提取日期和时间:
std::string log = "2025-03-15 14:30:22 [ERROR] 连接超时";
std::regex pattern(R"((\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}))");
std::smatch match;
if (std::regex_search(log, match, pattern)) {
std::cout << "完整时间戳: " << match[0] << "\n";
std::cout << "年: " << match[1] << "\n";
std::cout << "月: " << match[2] << "\n";
std::cout << "日: " << match[3] << "\n";
std::cout << "时: " << match[4] << "\n";
std::cout << "分: " << match[5] << "\n";
std::cout << "秒: " << match[6] << "\n";
}
这里match[0]是整个匹配的字符串,match[1]到match[6]分别对应每个括号捕获的内容。你想想看,如果用手工去截取这些字段,得写多少行代码?
迭代匹配:处理多个结果
如果文本里有多个匹配项怎么办?比如从一份配置文件中提取所有IP地址。这时候就该std::regex_iterator出场了。
std::string config = R"(
server 192.168.1.1
server 10.0.0.5
server 172.16.0.10
)";
std::regex ip_pattern(R"((\d{1,3}\.){3}\d{1,3})");
auto begin = std::sregex_iterator(config.begin(), config.end(), ip_pattern);
auto end = std::sregex_iterator();
std::cout << "找到的IP地址:\n";
for (auto it = begin; it != end; ++it) {
std::cout << " - " << it->str() << "\n";
}
输出:
找到的IP地址:
- 192.168.1.1
- 10.0.0.5
- 172.16.0.10
这里要注意一点:sregex_iterator的构造开销不小。如果文本很长,建议先考虑是否真的需要遍历所有匹配项。我在项目中处理过百万行级别的日志,每次构造迭代器都会卡一下,后来改用regex_token_iterator做流式处理,性能好了不少。
替换操作:regex_replace
替换是正则表达式的另一个高频场景。C++提供了std::regex_replace,支持用捕获组引用进行灵活替换。
std::string text = "联系人: 张三(13812345678), 李四(13987654321)";
std::regex phone_pattern(R"(\d{11})");
// 简单替换:把手机号替换为[已隐藏]
std::string result1 = std::regex_replace(text, phone_pattern, "[已隐藏]");
std::cout << result1 << "\n";
// 输出: 联系人: 张三([已隐藏]), 李四([已隐藏])
// 高级替换:用捕获组重新格式化
std::regex format_pattern(R"((\d{3})(\d{4})(\d{4}))");
std::string result2 = std::regex_replace(text, format_pattern, "$1****$3");
std::cout << result2 << "\n";
// 输出: 联系人: 张三(138****5678), 李四(139****4321)
看到$1、$3了吗?它们分别引用第1个和第3个捕获组。这种写法在处理数据脱敏、格式转换时特别顺手。
std::regex对象缓存起来。每次构造regex对象都会编译模式,这个开销不小。我习惯把常用的正则表达式定义为全局常量或静态变量。
性能注意事项
正则表达式虽好,但别滥用。C++的<regex>实现有一个特点:编译期不做任何优化,所有工作都留到运行时。这意味着:
- 同一个正则表达式反复构造,性能会急剧下降
- 复杂的回溯模式(如嵌套量词)可能导致灾难性回溯
- 短字符串匹配用
string::find可能更快
我在项目中遇到过一个问题:用正则表达式解析CSV文件,结果一个字段里包含了几百个字符,正则引擎回溯了几十万次才匹配完。后来改用状态机手工解析,速度提升了100倍。所以,正则表达式适合「模式匹配」,不适合「语法解析」。
知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
避坑指南
最后,分享几个我这些年积累的经验:
- 转义问题:C++字符串里反斜杠本身需要转义,所以写
\\d才能表示数字。我建议用原始字符串字面量R"(...)",省心很多。 - 异常处理:正则表达式语法错误会抛出
std::regex_error异常。线上代码一定要捕获它,否则一个错误的模式就能让程序崩溃。 - 多字节字符:
std::regex对UTF-8的支持有限。如果你要处理中文,建议先转成宽字符或使用第三方库。 - 性能测试:别凭感觉判断正则表达式的快慢。用
std::chrono实际测一下,有时候一个简单的string::find比正则快10倍。
正则表达式是个好工具,但它不是万能的。学会在合适的场景使用它,才是真正的工程智慧。
公众号:蓝海资料掘金营,微信deep3321