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_matchstd::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虽然符合位数,但整个模式没有额外的校验。如果你想更严格,可以加上数值范围判断,不过那就需要结合代码逻辑了。

我的小技巧: 写正则表达式时,尽量用原始字符串字面量(R"(...)")。这样可以避免转义字符的噩梦。比如 \\d 直接写成 \d,清爽多了。

避坑指南:性能与异常

std::regex 虽然好用,但也不是没有坑。我曾经在一个高并发的服务里用正则表达式解析每一行日志,结果性能直接崩了。

为什么会这样?因为每次调用 std::regex_search 时,如果传入的是临时构造的 std::regex 对象,编译器会重复编译正则表达式。编译正则表达式是很耗时的操作。

警告: 不要在循环或频繁调用的函数中反复构造 std::regex 对象。应该把它定义为 static 或全局变量,只编译一次。

另外,正则表达式语法错误会抛出 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::string) std::regex 对象(编译后) regex_match 完全匹配 regex_search 搜索第一个匹配 regex_replace 替换匹配内容 bool (匹配结果) std::smatch (匹配到的子串) std::string (替换后)

这张图展示了 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 是C++11提供的标准正则表达式库。它虽然性能不如一些专用库(比如Google的RE2),但胜在跨平台、零依赖。对于大多数日常文本处理任务,它完全够用。

性能优化建议

最后,再分享几个我踩过的坑:

  1. 避免使用默认语法: std::regex 默认使用ECMAScript语法,但你可以指定优化选项。比如 std::regex::optimize 告诉编译器花更多时间优化匹配速度。
  2. 长文本分段处理: 如果你要处理几MB的文本,不要一次性喂给 regex。分段处理,每段几KB,这样内存和CPU都更友好。
  3. 考虑使用 std::string_view: C++17引入了 std::string_view,可以避免拷贝。不过 std::regex 对 string_view 的支持有限,需要自己注意生命周期。

嗯,关于 std::regex 就聊这么多。它不是什么万能药,但绝对是C++文本处理工具箱里最锋利的一把刀。下次遇到文本解析任务,不妨先想想:能不能用正则表达式解决?

一句话总结: std::regex 让C++的正则表达式处理变得标准化、易用化。掌握它,你的文本处理能力会上一个台阶。

公众号:蓝海资料掘金营,微信deep3321