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_] 来代替。

注意:POSIX 正则默认是贪婪匹配。如果你想用非贪婪模式,抱歉,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(&regex, "^[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(&regex);
    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:待匹配的字符串
  • nmatchpmatch 数组的大小
  • 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(&regex, pattern, REG_EXTENDED) != 0) {
        printf("正则编译失败\n");
        return 1;
    }

    if (regexec(&regex, 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(&regex);
    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(&regex, pattern, REG_EXTENDED) != 0) {
        printf("正则编译失败\n");
        return 1;
    }

    if (regexec(&regex, 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(&regex);
    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 知识体系总览

下面这张图帮你理清本章的核心逻辑:

正则表达式在C语言中的使用流程 regcomp() 编译正则表达式 regexec() 执行匹配与捕获 提取数据 处理匹配结果 regfree() 释放正则资源 日志文本 待解析的原始数据 结构化数据 IP、时间、状态码等 日志分析 / 监控告警 / 数据统计

说白了,整个流程就是:编译 → 匹配 → 提取 → 释放。循环往复,直到把所有日志都处理完。

我个人习惯把正则编译和释放封装成两个函数,这样主逻辑看起来干净很多。你想想看,如果每次都要写一堆 regcompregexecregfree,代码得多臃肿。

好了,这一章的内容就到这里。正则表达式是个好东西,但别贪多,掌握常用的几个模式就够了。遇到复杂的场景,拆成多个小正则,比写一个超级长的正则要容易调试得多。


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