实战案例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)\]
这里用了|表示「或」。我建议把常见的级别都列全,万一以后加了DEBUG或FATAL,直接加进去就行。
注意:正则默认是区分大小写的。如果你的日志里出现了[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-2552[0-4]\d:匹配200-2491\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
你看,整整齐齐。有了这个基础,后面想做统计、过滤、报警都方便了。
核心逻辑流程图
我把整个解析流程画成了图,方便你理解:
避坑指南
最后分享几个我踩过的坑:
- 正则编译失败:记得检查
regcomp的返回值。我曾经写错了一个转义符,结果正则编译失败,程序直接崩了。加个错误处理,能省很多调试时间。 - 内存泄漏:每次
regcomp之后,一定要对应调用regfree。尤其是循环里编译正则的情况,忘了释放内存,跑几个小时就OOM了。 - 缓冲区溢出:用
strncpy代替strcpy,指定最大拷贝长度。日志里万一出现超长IP地址(比如IPv6),不至于把栈搞崩。 - 空行处理:日志文件末尾经常有空行。正则匹配空行会失败,但程序不能崩。我习惯在匹配前先判断一下行长度,太短的就跳过。
好了,这一章的内容就到这儿。代码你可以直接拿去用,改改正则就能适配自己的日志格式。记住一点:正则表达式是工具,不是目的。我们的目标是快速、准确地从日志里拿到想要的信息。