实战案例1:日志文件解析

日志解析,说白了就是跟字符串打交道。我刚开始做嵌入式开发那会儿,最头疼的就是看设备日志——几千行密密麻麻的文本,眼睛都快看瞎了。后来我学乖了,写个正则解析脚本,一分钟搞定别人半天的活。

今天咱们就拿一个真实的日志文件开刀。我会带着你一步步写代码,把IP地址、时间戳、错误级别这些关键信息提取出来。嗯,这里要注意:正则表达式不是万能的,但处理日志这种半结构化文本,它确实是最顺手的工具。

日志长什么样?

先看看我们要处理的日志格式。这是我以前项目里真实用过的格式,稍微简化了一下:

[2024-01-15 10:23:45] [ERROR] 192.168.1.100 - Failed to connect to database
[2024-01-15 10:23:46] [WARN] 10.0.0.5 - Connection pool is 80% full
[2024-01-15 10:23:47] [INFO] 172.16.0.1 - Health check passed
[2024-01-15 10:23:48] [ERROR] 192.168.1.200 - Timeout after 30 seconds

每一行包含三个关键部分:时间戳、错误级别、IP地址。我们的目标就是把这仨玩意儿分别拎出来。

第一步:读取日志文件

在C语言里读文件,我习惯用fgets一行行读。为什么不用fscanf?因为日志格式有时候不太规整,fgets配合正则更灵活。你想想看,万一某行日志中间多了个空格,fscanf直接就崩了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

#define MAX_LINE 1024

int main() {
    FILE *fp = fopen("server.log", "r");
    if (!fp) {
        perror("打开日志文件失败");
        return 1;
    }

    char line[MAX_LINE];
    while (fgets(line, sizeof(line), fp)) {
        // 去掉换行符
        line[strcspn(line, "\n")] = '\0';
        printf("原始行: %s\n", line);
    }

    fclose(fp);
    return 0;
}

这段代码很简单,但有个坑:fgets会把换行符也读进来。我刚开始写的时候经常忘记去掉它,结果正则匹配死活不对。后来养成了习惯,读进来第一件事就是干掉\n

第二步:提取时间戳

时间戳的格式是[2024-01-15 10:23:45]。方括号包着,年月日时分秒。对应的正则表达式怎么写?

\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]

我来拆解一下:

  • \[\]:匹配方括号本身,记得要转义
  • \d{4}:4位数字,匹配年份
  • -:连字符,直接匹配
  • \d{2}:2位数字,匹配月份、日期等
  • 中间的空格和冒号都是字面量,直接写就行
  • 括号():捕获组,把时间戳内容单独拎出来

小技巧:我在项目中遇到过时间格式不统一的情况,有的日志用/分隔日期,有的用-。这时候可以把正则写成\d{4}[-/]\d{2}[-/]\d{2},一次匹配两种格式。

第三步:提取错误级别

错误级别在第二个方括号里:[ERROR][WARN][INFO]。正则表达式:

\[(ERROR|WARN|INFO)\]

这里用了|表示「或」。我建议把常见的级别都列全,万一以后加了DEBUGFATAL,直接加进去就行。

注意:正则默认是区分大小写的。如果你的日志里出现了[error]小写,那就匹配不到了。可以在编译正则时加上REG_ICASE标志,忽略大小写。

第四步:提取IP地址

IP地址稍微复杂点。IPv4的格式是4组0-255的数字,用点分隔。正则表达式:

((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)

看着挺长,其实逻辑很简单:

  • 25[0-5]:匹配250-255
  • 2[0-4]\d:匹配200-249
  • 1\d{2}:匹配100-199
  • [1-9]?\d:匹配0-99
  • 以上四种情况用|连接,外面套个括号
  • 重复3次加个点,最后再来一次不带点的

说实话,这个正则我每次写都要查一下。后来我干脆封装成一个宏,用到的地方直接调用。

完整代码示例

把上面三部分合起来,就是完整的日志解析器:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <regex.h>

#define MAX_LINE 1024
#define MAX_MATCHES 10

int main() {
    // 编译三个正则表达式
    regex_t time_regex, level_regex, ip_regex;
    regmatch_t matches[MAX_MATCHES];

    // 时间戳正则
    regcomp(&time_regex, 
            "\\[([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})\\]", 
            REG_EXTENDED);

    // 错误级别正则
    regcomp(&level_regex, 
            "\\[(ERROR|WARN|INFO)\\]", 
            REG_EXTENDED);

    // IP地址正则
    regcomp(&ip_regex, 
            "((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])", 
            REG_EXTENDED);

    FILE *fp = fopen("server.log", "r");
    if (!fp) {
        perror("打开文件失败");
        return 1;
    }

    char line[MAX_LINE];
    int line_num = 0;

    printf("=== 日志解析结果 ===\n\n");

    while (fgets(line, sizeof(line), fp)) {
        line_num++;
        line[strcspn(line, "\n")] = '\0';

        char time_str[64] = "";
        char level_str[16] = "";
        char ip_str[32] = "";

        // 提取时间戳
        if (regexec(&time_regex, line, 1, matches, 0) == 0) {
            int start = matches[1].rm_so;
            int end = matches[1].rm_eo;
            strncpy(time_str, line + start, end - start);
            time_str[end - start] = '\0';
        }

        // 提取错误级别
        if (regexec(&level_regex, line, 1, matches, 0) == 0) {
            int start = matches[1].rm_so;
            int end = matches[1].rm_eo;
            strncpy(level_str, line + start, end - start);
            level_str[end - start] = '\0';
        }

        // 提取IP地址
        if (regexec(&ip_regex, line, 1, matches, 0) == 0) {
            int start = matches[0].rm_so;
            int end = matches[0].rm_eo;
            strncpy(ip_str, line + start, end - start);
            ip_str[end - start] = '\0';
        }

        printf("第%d行 | 时间: %s | 级别: %s | IP: %s\n",
               line_num, time_str, level_str, ip_str);
    }

    // 释放正则资源
    regfree(&time_regex);
    regfree(&level_regex);
    regfree(&ip_regex);

    fclose(fp);
    return 0;
}

运行结果

编译运行后,输出如下:

=== 日志解析结果 ===

第1行 | 时间: 2024-01-15 10:23:45 | 级别: ERROR | IP: 192.168.1.100
第2行 | 时间: 2024-01-15 10:23:46 | 级别: WARN  | IP: 10.0.0.5
第3行 | 时间: 2024-01-15 10:23:47 | 级别: INFO  | IP: 172.16.0.1
第4行 | 时间: 2024-01-15 10:23:48 | 级别: ERROR | IP: 192.168.1.200

你看,整整齐齐。有了这个基础,后面想做统计、过滤、报警都方便了。

核心逻辑流程图

我把整个解析流程画成了图,方便你理解:

日志解析核心流程 打开日志文件 逐行读取 (fgets) 正则匹配时间戳 正则匹配错误级别 正则匹配IP地址 输出解析结果 读取操作 正则处理 输出结果

避坑指南

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

  • 正则编译失败:记得检查regcomp的返回值。我曾经写错了一个转义符,结果正则编译失败,程序直接崩了。加个错误处理,能省很多调试时间。
  • 内存泄漏:每次regcomp之后,一定要对应调用regfree。尤其是循环里编译正则的情况,忘了释放内存,跑几个小时就OOM了。
  • 缓冲区溢出:用strncpy代替strcpy,指定最大拷贝长度。日志里万一出现超长IP地址(比如IPv6),不至于把栈搞崩。
  • 空行处理:日志文件末尾经常有空行。正则匹配空行会失败,但程序不能崩。我习惯在匹配前先判断一下行长度,太短的就跳过。

好了,这一章的内容就到这儿。代码你可以直接拿去用,改改正则就能适配自己的日志格式。记住一点:正则表达式是工具,不是目的。我们的目标是快速、准确地从日志里拿到想要的信息。

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