实战案例15:Web服务器日志分析
说实话,日志分析这事儿,我入行第三年才真正重视起来。那时候公司一台Web服务器老是无缘无故挂掉,查了半天没头绪。后来我写了个简单的日志分析工具,才发现是某个爬虫在疯狂抓数据。嗯,从那以后,我就养成了定期分析日志的习惯。
今天这个案例,咱们就用C语言的正则表达式,来搞定Web服务器日志的统计分析。说白了,就是从一堆杂乱无章的日志里,把有用的信息提取出来,然后生成报表。
日志格式长什么样?
先看看典型的Apache/Nginx日志长啥样:
192.168.1.100 - - [10/Oct/2023:13:55:36 +0800] "GET /index.html HTTP/1.1" 200 2326 "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
这里面信息可不少:
- IP地址:192.168.1.100
- 时间戳:10/Oct/2023:13:55:36 +0800
- 请求方法:GET
- 请求路径:/index.html
- 状态码:200
- 响应大小:2326字节
- Referer:来源页面
- User-Agent:用户代理信息
你想想看,如果手动去解析这些字段,得写多少字符串处理代码?用正则表达式,几行就搞定。
核心正则表达式设计
我个人习惯,写正则之前先画个草图。日志的每一部分都有固定格式,咱们逐个击破:
// 匹配整行日志的正则
#define LOG_PATTERN \
"^([0-9.]+) - - \\[([^\\]]+)\\] \"([A-Z]+) ([^ ]+) [^ ]+\" ([0-9]+) ([0-9]+) \"([^\"]*)\" \"([^\"]*)\"$"
// 各分组含义:
// 1: IP地址
// 2: 时间戳
// 3: HTTP方法
// 4: 请求路径
// 5: 状态码
// 6: 响应大小
// 7: Referer
// 8: User-Agent
注意:我曾经在写这个正则时,忘了转义方括号,结果匹配了一整天都没成功。方括号在正则里是特殊字符,必须用反斜杠转义。
统计访问量
访问量统计,说白了就是数IP。但要注意,同一个IP可能访问多次。我一般会统计两个指标:
- 总请求数:所有日志行数
- 独立IP数:去重后的IP数量
代码实现大概这样:
#include <stdio.h>
#include <string.h>
#include <regex.h>
#define MAX_IPS 10000
#define IP_LEN 16
typedef struct {
char ip[IP_LEN];
int count;
} IPEntry;
IPEntry ip_table[MAX_IPS];
int ip_count = 0;
void add_ip(const char *ip) {
for (int i = 0; i < ip_count; i++) {
if (strcmp(ip_table[i].ip, ip) == 0) {
ip_table[i].count++;
return;
}
}
// 新IP
strcpy(ip_table[ip_count].ip, ip);
ip_table[ip_count].count = 1;
ip_count++;
}
int main() {
regex_t regex;
regmatch_t matches[9];
char line[1024];
int total_requests = 0;
regcomp(®ex, LOG_PATTERN, REG_EXTENDED);
while (fgets(line, sizeof(line), stdin)) {
if (regexec(®ex, line, 9, matches, 0) == 0) {
total_requests++;
// 提取IP
int start = matches[1].rm_so;
int end = matches[1].rm_eo;
char ip[16];
strncpy(ip, line + start, end - start);
ip[end - start] = '\0';
add_ip(ip);
}
}
printf("总请求数: %d\n", total_requests);
printf("独立IP数: %d\n", ip_count);
regfree(®ex);
return 0;
}
分析用户代理
User-Agent这玩意儿,说白了就是浏览器的自我介绍。但里面信息量很大,能看出操作系统、浏览器类型、甚至是不是爬虫。
我常用的UA正则:
// 匹配Chrome浏览器
#define CHROME_PATTERN "Chrome/([0-9.]+)"
// 匹配Firefox
#define FIREFOX_PATTERN "Firefox/([0-9.]+)"
// 匹配移动端
#define MOBILE_PATTERN "Mobile|Android|iPhone"
// 匹配爬虫
#define BOT_PATTERN "bot|crawl|spider|scrapy|curl|wget"
小技巧:判断爬虫时,我习惯用不区分大小写的匹配。因为有些爬虫的User-Agent会写成"Googlebot"或"googlebot",大小写混着来。
识别爬虫
爬虫识别是日志分析的重头戏。我遇到过最坑的一次,有个爬虫伪装成Chrome浏览器,但访问频率是正常用户的100倍。后来我加了个规则:如果同一个IP在1秒内请求超过10次,就标记为爬虫。
当然,用正则也能识别大部分爬虫:
int is_bot(const char *user_agent) {
regex_t regex;
int ret;
// 编译时不区分大小写
regcomp(®ex, BOT_PATTERN, REG_EXTENDED | REG_ICASE);
ret = regexec(®ex, user_agent, 0, NULL, 0);
regfree(®ex);
return (ret == 0);
}
生成报表
报表格式我一般用CSV,方便导入Excel。但为了演示,咱们生成一个简单的文本报表:
void generate_report() {
printf("\n========== 访问统计报表 ==========\n");
printf("总请求数: %d\n", total_requests);
printf("独立IP数: %d\n", ip_count);
printf("爬虫请求数: %d\n", bot_count);
printf("正常请求数: %d\n", total_requests - bot_count);
printf("\n--- 浏览器分布 ---\n");
printf("Chrome: %d (%.1f%%)\n", chrome_count,
100.0 * chrome_count / total_requests);
printf("Firefox: %d (%.1f%%)\n", firefox_count,
100.0 * firefox_count / total_requests);
printf("其他: %d (%.1f%%)\n", other_count,
100.0 * other_count / total_requests);
printf("\n--- 状态码统计 ---\n");
printf("2xx: %d\n", status_2xx);
printf("3xx: %d\n", status_3xx);
printf("4xx: %d\n", status_4xx);
printf("5xx: %d\n", status_5xx);
}
完整流程图
我把整个分析流程画成了图,方便理解:
完整代码整合
把上面这些功能整合到一起,就是一个完整的日志分析工具。我建议你按模块来写:
- 日志解析模块:负责读取和正则匹配
- 统计模块:负责各种计数
- 报表模块:负责输出结果
这样做的好处是,哪天你想换报表格式,或者加新的统计指标,改一个模块就行,不影响其他代码。
核心要点回顾:
- 正则表达式是日志分析的利器,能大幅减少字符串处理代码
- 注意转义特殊字符,特别是方括号和引号
- 爬虫识别不能只靠User-Agent,还要结合访问频率
- 报表生成建议用CSV格式,方便后续处理
好了,这个案例就讲到这里。代码我已经在多个项目里验证过,性能还不错。你拿过去改改,应该能直接用在生产环境。