24、正则表达式库:regex_match/regex_search/regex_replace、正则语法、迭代器与tokenizer、实战:日志格式提取
正则表达式,说白了就是一套“模式匹配”的规则语言。很多C++开发者一听到正则就头疼,觉得语法晦涩、性能堪忧。嗯,我刚开始也有这感觉。直到有一次接手一个日志分析模块,几千行日志要提取关键字段,手写字符串解析写到崩溃……后来用regex_replace三行代码搞定,从此真香。
C++11正式把正则表达式纳入了标准库,头文件<regex>。虽然它不像Python或Perl的正则那么“原生”那么快,但胜在跨平台、零依赖。我个人习惯是:小规模文本处理用STL regex,大规模日志分析还是上PCRE或RE2。但日常开发,STL regex完全够用。
24.1 核心函数:regex_match、regex_search、regex_replace
这三个函数是正则库的“三驾马车”。我经常看到新手把match和search搞混,其实区别很简单:
- regex_match:整个字符串必须完全匹配模式。说白了就是“全字匹配”。
- regex_search:只要字符串中有一部分匹配就行。它找到第一个匹配就返回。
- regex_replace:匹配并替换,支持捕获组引用。
核心区别速记:match要“从头到尾”都对上,search只要“藏”在字符串里就行。
来看个例子。假设我们要验证一个字符串是不是合法的IPv4地址:
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string ip = "192.168.1.1";
// 这个模式要求整个字符串都是IP格式
std::regex ip_pattern(R"((\d{1,3}\.){3}\d{1,3})");
if (std::regex_match(ip, ip_pattern)) {
std::cout << "完全匹配:是IP地址" << std::endl;
}
std::string text = "服务器IP: 10.0.0.5, 端口: 8080";
std::smatch match_result;
// search只找子串,不要求全匹配
if (std::regex_search(text, match_result, ip_pattern)) {
std::cout << "找到IP: " << match_result.str() << std::endl;
}
// 替换所有IP为***.***.***.***
std::string masked = std::regex_replace(text, ip_pattern, "***.***.***.***");
std::cout << "脱敏后: " << masked << std::endl;
return 0;
}
输出结果:
完全匹配:是IP地址
找到IP: 10.0.0.5
脱敏后:服务器IP: ***.***.***.***, 端口: 8080
我的小建议:如果只是判断“存不存在”,用regex_search;如果要提取整个字段,用regex_match配合捕获组。regex_replace的替换字符串里用$1、$2引用捕获组,这个语法和很多脚本语言一致。
24.2 正则语法速览
STL regex默认使用ECMAScript语法,和JavaScript、Java的正则基本一致。我整理了一份常用语法表,你写代码时可以直接翻:
| 语法 | 含义 | 示例 |
|---|---|---|
. |
匹配任意单个字符(除换行) | a.c 匹配 "abc"、"a1c" |
* |
前一个字符出现0次或多次 | ab*c 匹配 "ac"、"abc"、"abbc" |
+ |
前一个字符出现1次或多次 | ab+c 匹配 "abc",不匹配 "ac" |
? |
前一个字符出现0次或1次 | ab?c 匹配 "ac"、"abc" |
\d |
数字,等价于[0-9] | \d{3} 匹配三位数字 |
\w |
单词字符(字母、数字、下划线) | \w+ 匹配一个单词 |
() |
捕获组 | (\d+)-(\w+) 提取两部分 |
[] |
字符集 | [aeiou] 匹配任意元音 |
^ |
行首 | ^Hello 匹配行首的Hello |
$ |
行尾 | world$ 匹配行尾的world |
注意转义:在C++字符串里写反斜杠要加倍。比如\d要写成"\\d"。我个人强烈建议用原始字符串字面量R"(...)",这样反斜杠不用转义,可读性高很多。
24.3 迭代器与tokenizer
有时候我们需要遍历字符串中所有匹配的位置,而不是只找第一个。这时候std::sregex_iterator就派上用场了。它就像一个“扫描器”,每次迭代返回一个std::smatch对象。
我曾经在解析配置文件时用过它。一个文件里可能有几十个key=value对,用迭代器一行一行扫,比手动find快多了。
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string log = "2024-01-15 10:30:45 [ERROR] 连接超时\n"
"2024-01-15 10:31:02 [INFO] 重试第1次\n"
"2024-01-15 10:32:18 [WARN] 磁盘空间不足";
// 匹配日志行中的时间、级别、消息
std::regex line_pattern(R"((\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+))");
// 用迭代器遍历所有匹配行
auto begin = std::sregex_iterator(log.begin(), log.end(), line_pattern);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
std::smatch match = *it;
std::cout << "时间: " << match[1].str()
<< ", 级别: " << match[2].str()
<< ", 消息: " << match[3].str()
<< std::endl;
}
return 0;
}
输出:
时间: 2024-01-15 10:30:45, 级别: ERROR, 消息: 连接超时
时间: 2024-01-15 10:31:02, 级别: INFO, 消息: 重试第1次
时间: 2024-01-15 10:32:18, 级别: WARN, 消息: 磁盘空间不足
另外还有个std::regex_token_iterator,它更灵活。你可以指定要提取哪个捕获组,或者把匹配结果当分隔符来拆分字符串。说白了,它就是一个“正则版”的strtok。
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string csv = "张三,25,工程师;李四,30,设计师;王五,28,产品经理";
// 用分号或逗号作为分隔符
std::regex sep(R"([;,])");
// tokenizer:遍历所有被分隔符分开的字段
auto begin = std::regex_token_iterator<std::string::const_iterator>(
csv.begin(), csv.end(), sep, -1); // -1表示返回非匹配部分
auto end = std::regex_token_iterator<std::string::const_iterator>();
int count = 0;
for (auto it = begin; it != end; ++it) {
std::cout << "字段" << ++count << ": " << it->str() << std::endl;
}
return 0;
}
输出:
字段1: 张三
字段2: 25
字段3: 工程师
字段4: 李四
字段5: 30
字段6: 设计师
字段7: 王五
字段8: 28
字段9: 产品经理
性能提醒:regex_token_iterator的-1参数表示“返回分隔符之间的内容”,相当于split。但注意,如果字符串开头或结尾有分隔符,会返回空字符串。我在项目中遇到过这个坑,后来加了个前置检查才搞定。
24.4 实战:日志格式提取
好了,理论说完了,咱们来点实战。假设你有一个服务器日志文件,每行格式如下:
[2024-03-15 14:23:45] [INFO] [模块A] 用户登录成功, userId=10086, ip=192.168.1.10
[2024-03-15 14:24:01] [ERROR] [模块B] 数据库连接失败, errno=1040, db=user_db
[2024-03-15 14:25:30] [WARN] [模块C] 磁盘使用率超过80%, current=85%
需求:提取每条日志的时间、级别、模块、消息、以及键值对参数。咱们用正则一步到位:
#include <iostream>
#include <regex>
#include <string>
#include <vector>
struct LogEntry {
std::string timestamp;
std::string level;
std::string module;
std::string message;
std::string params; // 原始参数字符串
};
int main() {
std::vector<std::string> logs = {
"[2024-03-15 14:23:45] [INFO] [模块A] 用户登录成功, userId=10086, ip=192.168.1.10",
"[2024-03-15 14:24:01] [ERROR] [模块B] 数据库连接失败, errno=1040, db=user_db",
"[2024-03-15 14:25:30] [WARN] [模块C] 磁盘使用率超过80%, current=85%"
};
// 这个模式匹配整行日志
std::regex log_pattern(
R"(\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] )" // 时间
R"(\[(\w+)\] )" // 级别
R"(\[(\w+)\] )" // 模块
R"(([^,]+))" // 消息(直到逗号或行尾)
R"(,?\s*(.*))" // 剩余参数(可选)
);
std::vector<LogEntry> entries;
for (const auto& line : logs) {
std::smatch match;
if (std::regex_match(line, match, log_pattern)) {
entries.push_back({
match[1].str(), // 时间
match[2].str(), // 级别
match[3].str(), // 模块
match[4].str(), // 消息
match[5].str() // 参数
});
}
}
// 打印提取结果
for (const auto& entry : entries) {
std::cout << "时间: " << entry.timestamp << "\n"
<< "级别: " << entry.level << "\n"
<< "模块: " << entry.module << "\n"
<< "消息: " << entry.message << "\n"
<< "参数: " << entry.params << "\n"
<< "---" << std::endl;
}
return 0;
}
输出:
时间: 2024-03-15 14:23:45
级别: INFO
模块: 模块A
消息: 用户登录成功
参数: userId=10086, ip=192.168.1.10
---
时间: 2024-03-15 14:24:01
级别: ERROR
模块: 模块B
消息: 数据库连接失败
参数: errno=1040, db=user_db
---
时间: 2024-03-15 14:25:30
级别: WARN
模块: 模块C
消息: 磁盘使用率超过80%
参数: current=85%
---
我曾经踩过的坑:日志里的消息部分可能包含逗号,比如“磁盘使用率超过80%, current=85%”。如果直接用逗号分割,会把消息和参数搞混。所以我用([^,]+)匹配消息,只匹配到第一个逗号之前,剩下的全当参数。这样虽然参数里还带着逗号,但至少消息是干净的。如果你需要更精细的解析,可以再对参数字符串做一次正则拆分。
最后,咱们用一张图总结本章的知识体系:
正则表达式库虽然强大,但也不是万能的。我个人建议:如果模式特别复杂(比如嵌套括号、递归匹配),或者数据量特别大(百万级行日志),还是考虑用专门的解析库。但日常的配置解析、日志提取、格式校验,STL regex完全能胜任。
嗯,这一章的内容就到这。记住三个核心函数、两种迭代器、以及那个日志提取的实战模式。下次你遇到字符串解析的需求,先想想能不能用正则三行搞定——说不定比你手写循环快得多。
公众号:蓝海资料掘金营,微信deep3321