17、C++11 实用工具:std::regex正则表达式,文本处理的瑞士军刀。
文本处理,是每个C++程序员都绕不开的活。
我早年做网络协议解析的时候,每天面对的就是一堆堆的日志、配置文件和协议报文。那时候用的还是C风格的字符串函数,strstr、sscanf、自己手写状态机……说实话,又慢又容易出错。直到C++11带来了std::regex,我才感觉终于有了一把趁手的“瑞士军刀”。
今天我们就来聊聊这个工具。它不是什么黑魔法,但用好了,能让你处理文本的效率翻倍。
正则表达式:到底是什么?
说白了,正则表达式就是一套“模式匹配”的语言。你给它一个字符串,它告诉你:这个字符串里有没有符合某种规则的部分。
比如你想从一堆日志里找出所有IP地址。IP地址的格式是“数字.数字.数字.数字”。用正则表达式写出来就是:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}。然后std::regex就能帮你把匹配到的内容全部拎出来。
我个人习惯把正则表达式看作“文本的模板”。你定义好模板,剩下的交给引擎去匹配。
std::regex 的核心组件
C++11的regex库主要由几个类组成。我列个表,你一看就明白:
| 组件 | 作用 | 我常用的场景 |
|---|---|---|
| std::regex | 表示一个正则表达式对象 | 编译正则表达式,供后续匹配使用 |
| std::smatch | 存储匹配结果(针对std::string) | 提取匹配到的子串 |
| std::regex_match | 判断整个字符串是否完全匹配 | 验证输入格式,比如检查邮箱格式 |
| std::regex_search | 在字符串中搜索第一个匹配 | 从日志中提取关键信息 |
| std::regex_replace | 替换匹配到的内容 | 批量替换文本中的敏感词 |
| std::sregex_iterator | 迭代所有匹配结果 | 遍历文本中所有符合模式的内容 |
嗯,这里要注意:std::regex_match 和 std::regex_search 的区别。前者要求整个字符串完全匹配,后者只要求部分匹配。我刚开始用的时候经常搞混,后来养成了习惯:验证格式用match,提取内容用search。
实战:从日志中提取IP地址
光说不练假把式。我们直接看代码。假设你有一份服务器日志,里面混杂着各种信息,你想把所有IP地址提取出来。
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string log = R"(
192.168.1.1 - - [10/Oct/2023:13:55:36] "GET /index.html"
10.0.0.5 - - [10/Oct/2023:13:56:01] "POST /api/data"
错误地址: 999.999.999.999 应该被忽略
172.16.0.10 - - [10/Oct/2023:13:57:22] "GET /favicon.ico"
)";
// 定义IP地址的正则表达式
std::regex ip_pattern(R"(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b)");
// 使用迭代器遍历所有匹配
std::sregex_iterator it(log.begin(), log.end(), ip_pattern);
std::sregex_iterator end;
std::cout << "找到的IP地址:" << std::endl;
for (; it != end; ++it) {
std::smatch match = *it;
std::cout << " - " << match.str() << std::endl;
}
return 0;
}
输出结果:
找到的IP地址:
- 192.168.1.1
- 10.0.0.5
- 172.16.0.10
你看,999.999.999.999 被自动忽略了。为什么?因为正则表达式里的 \d{1,3} 只匹配1到3位数字,而999虽然符合位数,但整个模式没有额外的校验。如果你想更严格,可以加上数值范围判断,不过那就需要结合代码逻辑了。
\\d 直接写成 \d,清爽多了。
避坑指南:性能与异常
std::regex 虽然好用,但也不是没有坑。我曾经在一个高并发的服务里用正则表达式解析每一行日志,结果性能直接崩了。
为什么会这样?因为每次调用 std::regex_search 时,如果传入的是临时构造的 std::regex 对象,编译器会重复编译正则表达式。编译正则表达式是很耗时的操作。
另外,正则表达式语法错误会抛出 std::regex_error 异常。我建议你在构造 regex 对象时,用 try-catch 包一下:
try {
std::regex pattern(R"([a-z]+)");
// 使用 pattern
} catch (const std::regex_error& e) {
std::cerr << "正则表达式错误: " << e.what() << std::endl;
std::cerr << "错误代码: " << e.code() << std::endl;
}
嗯,这一点很多人会忽略。尤其是当你从配置文件或用户输入中读取正则表达式时,一定要做异常处理。
知识体系:std::regex 的核心逻辑
为了让你更直观地理解 std::regex 的工作流程,我画了一张图:
这张图展示了 std::regex 的三种核心操作。你输入文本,构造好正则表达式对象,然后根据需求选择匹配、搜索或替换。我个人觉得,理解了这个流程,你就掌握了 std::regex 的80%。
更多实用场景
除了提取IP地址,std::regex 还能干很多事。我简单列几个我实际用过的:
- 验证用户输入: 检查邮箱格式、电话号码、身份证号等。用
regex_match一次搞定。 - 配置文件解析: 从INI或JSON格式的配置中提取键值对。用
regex_search配合分组捕获。 - 日志分析: 统计某个错误码出现的次数。用
sregex_iterator遍历所有匹配。 - 文本清洗: 批量删除或替换HTML标签、特殊字符。用
regex_replace。
举个例子,替换所有HTML标签:
std::string html = "<p>Hello <b>World</b></p>";
std::regex tag_pattern(R"(<[^>]*>)");
std::string result = std::regex_replace(html, tag_pattern, "");
// result 变成 "Hello World"
你看,一行代码就搞定了。以前用C风格函数,得写几十行状态机。
性能优化建议
最后,再分享几个我踩过的坑:
- 避免使用默认语法: std::regex 默认使用ECMAScript语法,但你可以指定优化选项。比如
std::regex::optimize告诉编译器花更多时间优化匹配速度。 - 长文本分段处理: 如果你要处理几MB的文本,不要一次性喂给 regex。分段处理,每段几KB,这样内存和CPU都更友好。
- 考虑使用 std::string_view: C++17引入了
std::string_view,可以避免拷贝。不过 std::regex 对 string_view 的支持有限,需要自己注意生命周期。
嗯,关于 std::regex 就聊这么多。它不是什么万能药,但绝对是C++文本处理工具箱里最锋利的一把刀。下次遇到文本解析任务,不妨先想想:能不能用正则表达式解决?
公众号:蓝海资料掘金营,微信deep3321