14、正则表达式:regex.h库、编译正则、匹配与捕获、实际日志解析案例
正则表达式,说白了就是一套「模式匹配」的规则语言。你给它一个字符串,它告诉你:这个字符串里有没有符合规则的部分?有的话,具体是哪一段?
我在做日志分析工具的时候,几乎天天跟正则打交道。系统日志、访问日志、错误日志……格式五花八门,但只要你掌握了正则,再乱的日志也能给你拆得明明白白。
这一章,我们就来聊聊 C 语言里怎么用正则表达式。标准库提供了 regex.h,功能虽然不如 Perl 或 Python 那么强大,但应付日常的日志解析绰绰有余。
14.1 regex.h 库:C 语言的正则工具箱
regex.h 是 POSIX 标准定义的 C 语言正则表达式库。它提供了三个核心函数:
regcomp()—— 编译正则表达式regexec()—— 执行匹配regerror()—— 获取错误信息regfree()—— 释放资源
嗯,这里要注意:regex.h 支持的是 POSIX 扩展正则表达式(ERE),不是 Perl 兼容的正则(PCRE)。所以像 \d、\w 这种简写是不支持的,你得用 [0-9]、[a-zA-Z0-9_] 来代替。
regex.h 不支持。遇到这种情况,我一般会换个思路,或者干脆用 PCRE 库。
14.2 编译正则:regcomp() 的使用
正则表达式不能直接用,得先「编译」成内部格式。就像你写好的 C 代码,得先编译成机器码才能跑。
regcomp() 的函数签名如下:
int regcomp(regex_t *preg, const char *regex, int cflags);
preg:指向regex_t结构体的指针,用来存放编译结果regex:正则表达式字符串cflags:编译标志,常用值有:
| 标志 | 含义 |
|---|---|
REG_EXTENDED |
使用扩展正则表达式(推荐) |
REG_ICASE |
忽略大小写 |
REG_NOSUB |
不报告子表达式位置(提高性能) |
REG_NEWLINE |
将换行符视为特殊字符 |
返回值:成功返回 0,失败返回非 0 错误码。
我个人习惯在编译时加上 REG_EXTENDED,因为扩展正则支持 +、?、| 这些常用操作符,写起来更顺手。
#include <stdio.h>
#include <regex.h>
int main() {
regex_t regex;
int ret;
// 编译正则:匹配 IP 地址
ret = regcomp(®ex, "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", REG_EXTENDED);
if (ret != 0) {
printf("正则编译失败\n");
return 1;
}
printf("正则编译成功\n");
regfree(®ex);
return 0;
}
REG_NOSUB 标志。这样 regexec() 会跑得更快。我在处理海量日志时经常这么干。
14.3 匹配与捕获:regexec() 的实战用法
编译好正则之后,就该上场干活了。regexec() 负责执行匹配:
int regexec(const regex_t *preg, const char *string,
size_t nmatch, regmatch_t pmatch[], int eflags);
preg:编译好的正则string:待匹配的字符串nmatch:pmatch数组的大小pmatch:存放匹配结果的数组eflags:执行标志,一般填 0
regmatch_t 结构体有两个字段:rm_so(匹配起始位置)和 rm_eo(匹配结束位置)。如果 rm_so 为 -1,表示该子表达式没有匹配。
来看一个完整的例子:从日志行中提取时间戳和错误级别。
#include <stdio.h>
#include <regex.h>
#include <string.h>
int main() {
regex_t regex;
regmatch_t matches[3];
char *log_line = "2024-01-15 14:30:22 [ERROR] 数据库连接超时";
char *pattern = "^([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) \\[([A-Z]+)\\]";
if (regcomp(®ex, pattern, REG_EXTENDED) != 0) {
printf("正则编译失败\n");
return 1;
}
if (regexec(®ex, log_line, 3, matches, 0) == 0) {
// 提取时间戳(子表达式 1)
int start = matches[1].rm_so;
int end = matches[1].rm_eo;
char timestamp[32];
strncpy(timestamp, log_line + start, end - start);
timestamp[end - start] = '\0';
printf("时间戳: %s\n", timestamp);
// 提取错误级别(子表达式 2)
start = matches[2].rm_so;
end = matches[2].rm_eo;
char level[16];
strncpy(level, log_line + start, end - start);
level[end - start] = '\0';
printf("错误级别: %s\n", level);
} else {
printf("未匹配到日志格式\n");
}
regfree(®ex);
return 0;
}
输出结果:
时间戳: 2024-01-15 14:30:22
错误级别: ERROR
你看,通过子表达式捕获,我们可以轻松地从一行日志里「抠」出想要的信息。我在项目中遇到过很多次,日志格式变了,只需要改正则表达式,代码逻辑完全不用动。
14.4 实际日志解析案例:Nginx 访问日志
光说不练假把式。我们拿一个真实的 Nginx 访问日志来练手。
典型的 Nginx 日志格式长这样:
192.168.1.100 - - [15/Jan/2024:14:30:22 +0800] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
我们需要从中提取:客户端 IP、请求时间、HTTP 方法、请求路径、状态码、响应大小。
对应的正则表达式:
^([0-9.]+) - - \[([^\]]+)\] "([A-Z]+) ([^ ]+) [^"]+" ([0-9]+) ([0-9]+)
完整代码:
#include <stdio.h>
#include <regex.h>
#include <string.h>
int main() {
regex_t regex;
regmatch_t matches[7];
char *log = "192.168.1.100 - - [15/Jan/2024:14:30:22 +0800] \"GET /api/users HTTP/1.1\" 200 1234 \"-\" \"Mozilla/5.0\"";
char *pattern = "^([0-9.]+) - - \\[([^\\]]+)\\] \"([A-Z]+) ([^ ]+) [^\"]+\" ([0-9]+) ([0-9]+)";
if (regcomp(®ex, pattern, REG_EXTENDED) != 0) {
printf("正则编译失败\n");
return 1;
}
if (regexec(®ex, log, 7, matches, 0) == 0) {
char *fields[] = {"IP", "时间", "方法", "路径", "状态码", "大小"};
for (int i = 1; i <= 6; i++) {
int start = matches[i].rm_so;
int end = matches[i].rm_eo;
char value[128];
strncpy(value, log + start, end - start);
value[end - start] = '\0';
printf("%s: %s\n", fields[i-1], value);
}
} else {
printf("未匹配到日志格式\n");
}
regfree(®ex);
return 0;
}
输出:
IP: 192.168.1.100
时间: 15/Jan/2024:14:30:22 +0800
方法: GET
路径: /api/users
状态码: 200
大小: 1234
14.5 避坑指南:我踩过的那些坑
用 regex.h 做日志解析,有几个坑我不得不提:
- 转义字符的坑:C 语言字符串里,反斜杠本身需要转义。比如你想匹配
\[,在正则里写\\[。写多了容易眼花,我建议先写好正则,再手动加转义。 - 子表达式索引从 1 开始:
matches[0]是整个匹配,matches[1]才是第一个子表达式。我曾经犯过这个错,调试了半天才发现索引搞错了。 - 内存管理:每次
regcomp()之后,一定要记得regfree()。特别是在循环里反复编译正则时,忘记释放会导致内存泄漏。 - 性能问题:如果你要对大量日志行使用同一个正则,把
regcomp()放在循环外面。编译一次,匹配多次,效率高得多。
^ 和 $,结果匹配到了日志行中间的部分,导致提取的数据完全错位。从那以后,我写正则都会明确指定起始和结束位置。
14.6 知识体系总览
下面这张图帮你理清本章的核心逻辑:
说白了,整个流程就是:编译 → 匹配 → 提取 → 释放。循环往复,直到把所有日志都处理完。
我个人习惯把正则编译和释放封装成两个函数,这样主逻辑看起来干净很多。你想想看,如果每次都要写一堆 regcomp、regexec、regfree,代码得多臃肿。
好了,这一章的内容就到这里。正则表达式是个好东西,但别贪多,掌握常用的几个模式就够了。遇到复杂的场景,拆成多个小正则,比写一个超级长的正则要容易调试得多。
公众号:蓝海资料掘金营,微信deep3321