4. 分组与捕获:子表达式分组、捕获匹配内容、使用regmatch_t结构体、提取子串
正则表达式里有个很实用的功能——分组与捕获。说白了,就是让你能从一段文本里,精准地掏出你想要的某一部分。比如从"2024-05-15"里单独拿出月份,或者从日志里提取IP地址。我个人觉得,这是正则从"能用"到"好用"的关键一步。
4.1 子表达式分组:用小括号圈出你的目标
在正则里,用小括号 () 包起来的部分,就是一个子表达式分组。它有两个作用:一是改变优先级(跟数学里的括号一样),二是让这部分内容可以被"捕获"到内存里。
举个例子,匹配日期格式 2024-05-15:
#include <stdio.h>
#include <regex.h>
int main() {
regex_t regex;
// 三个分组:年、月、日
const char *pattern = "([0-9]{4})-([0-9]{2})-([0-9]{2})";
const char *text = "2024-05-15";
if (regcomp(®ex, pattern, REG_EXTENDED) != 0) {
printf("编译正则失败\n");
return 1;
}
regmatch_t matches[4]; // 数组大小 = 分组数 + 1
int ret = regexec(®ex, text, 4, matches, 0);
if (ret == 0) {
printf("匹配成功!\n");
}
regfree(®ex);
return 0;
}
这里 ([0-9]{4}) 就是第一个分组,([0-9]{2}) 是第二个和第三个。我在项目中经常用这种模式去解析日志文件里的时间戳,比手写字符串切割靠谱多了。
4.2 捕获匹配内容:regmatch_t 结构体详解
匹配完了,怎么把内容取出来?这就轮到 regmatch_t 结构体登场了。它的定义很简单:
typedef struct {
regoff_t rm_so; // 匹配子串的起始偏移(从0开始)
regoff_t rm_eo; // 匹配子串的结束偏移(不包含该位置)
} regmatch_t;
rm_so 和 rm_eo 就像两个指针,告诉你每个分组在原始字符串里的起止位置。嗯,这里要注意:rm_eo 指向的是匹配结束的下一个字符,所以提取子串时长度是 rm_eo - rm_so。
我曾经踩过一个坑:直接用 rm_eo 作为字符串结束位置去打印,结果把后面的字符也带出来了。后来才意识到,必须用 rm_eo - rm_so 计算长度。
4.3 提取子串:从匹配结果到可用字符串
有了偏移量,提取子串就很简单了。我们可以用 strncpy 或者直接手动拷贝:
#include <stdio.h>
#include <string.h>
#include <regex.h>
int main() {
regex_t regex;
const char *pattern = "([0-9]{4})-([0-9]{2})-([0-9]{2})";
const char *text = "2024-05-15";
regcomp(®ex, pattern, REG_EXTENDED);
regmatch_t matches[4];
if (regexec(®ex, text, 4, matches, 0) == 0) {
char year[5] = {0};
char month[3] = {0};
char day[3] = {0};
// 提取年:从 matches[1].rm_so 到 matches[1].rm_eo
int len1 = matches[1].rm_eo - matches[1].rm_so;
strncpy(year, text + matches[1].rm_so, len1);
// 提取月
int len2 = matches[2].rm_eo - matches[2].rm_so;
strncpy(month, text + matches[2].rm_so, len2);
// 提取日
int len3 = matches[3].rm_eo - matches[3].rm_so;
strncpy(day, text + matches[3].rm_so, len3);
printf("年: %s, 月: %s, 日: %s\n", year, month, day);
}
regfree(®ex);
return 0;
}
输出结果:
年: 2024, 月: 05, 日: 15
你想想看,如果不用分组,你得自己算偏移量、数位数,多麻烦。分组捕获一出,代码清晰多了。
4.4 多个匹配与循环提取
有时候文本里有多处匹配,比如从一行日志里提取多个IP地址。这时候可以用 regexec 配合偏移量循环处理:
#include <stdio.h>
#include <string.h>
#include <regex.h>
int main() {
regex_t regex;
const char *pattern = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})";
const char *text = "访问来源: 192.168.1.1, 192.168.1.2, 10.0.0.1";
regcomp(®ex, pattern, REG_EXTENDED);
regmatch_t matches[2];
const char *cursor = text;
while (regexec(®ex, cursor, 2, matches, 0) == 0) {
int len = matches[1].rm_eo - matches[1].rm_so;
char ip[16] = {0};
strncpy(ip, cursor + matches[1].rm_so, len);
printf("提取到IP: %s\n", ip);
// 移动游标到本次匹配结束位置,继续查找
cursor += matches[1].rm_eo;
}
regfree(®ex);
return 0;
}
输出:
提取到IP: 192.168.1.1
提取到IP: 192.168.1.2
提取到IP: 10.0.0.1
这里的关键是每次匹配后,把 cursor 移动到本次匹配的末尾,然后继续调用 regexec。我在处理网络设备日志时经常用这个技巧,一行日志里能抓出十几个IP。
4.5 本章知识体系
下面这张图总结了分组与捕获的核心流程:
4.6 避坑指南
最后分享几个我实际工作中遇到的坑:
- 数组越界:
regmatch_t数组大小一定要大于分组数。我见过有人只声明了regmatch_t matches[1],结果匹配三个分组时直接崩了。 - 空分组: 如果某个分组没匹配到内容,
rm_so会是 -1。提取前一定要判断一下。 - 性能问题: 循环匹配大量文本时,每次调用
regexec都有开销。如果文本很长,建议先按行分割再匹配。 - 转义字符: C字符串里写正则时,反斜杠要双写。比如
\d要写成\\d,这个我刚开始写时经常忘。
核心要点:
- 用小括号
()创建分组,每个分组对应一个regmatch_t rm_so和rm_eo是提取子串的关键- 数组下标0存整个匹配,下标1开始存各个分组
- 循环匹配时记得移动游标,避免死循环
分组与捕获是正则实战的基石。掌握了它,你就能从杂乱文本里精准提取信息。我个人觉得,这是C语言正则里最值得花时间搞懂的部分。多写几个例子,很快就能上手。
公众号:蓝海资料掘金营,微信deep3321