应使用 open() 流式逐行读取 Nginx 日志,配合预编译正则提取 $remote_addr 和 $time_local 等字段,用 datetime.strptime() 解析时间,defaultdict 实现高效小时级聚合,跳过空行与注释行并妥善处理异常。

用 open() 逐行读取日志文件,别一次性 readlines()
大流量 Nginx 日志动辄几 GB,用 readlines() 或 read() 会直接 OOM。必须流式处理:用 open() 返回的文件对象配合 for line in f: 迭代,内存只保留当前行。
注意点:
- 确保文件以
encoding='utf-8'打开(Nginx 默认不带 BOM,但若日志含中文或特殊符号,不指定编码可能抛UnicodeDecodeError) - 跳过空行和注释行(Nginx 日志本身不含注释,但运维可能手动加
#行,建议用line.strip()判断 - 不要用
file.read().split('\n')—— 这等价于全加载,毫无意义
用正则提取字段时,预编译 re.compile() 并匹配 $remote_addr 和 $time_local
Nginx 默认日志格式是 log_format combined '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';,字段顺序固定但空格/引号嵌套复杂,不能靠 split() 可靠切分。
推荐做法:
立即学习“Python免费学习笔记(深入)”;
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
- 用
re.compile(r'^(\S+) - (\S+) \[([^\]]+)\] "([^"]+)" (\d+) (\S+) "([^"]*)" "([^"]*)"$')提前编译,避免每次循环都编译 - 关键字段如
$remote_addr(IP)、$time_local(时间字符串)必须提取准确,后续做 IP 归属、QPS 统计都依赖它们 - 如果日志启用了
$request_time或$upstream_response_time,需在正则里额外捕获,注意这些字段可能是-(短横线)表示缺失,需用if group else None处理
解析 $time_local 用 datetime.strptime() 而非 dateutil.parser
$time_local 格式固定为 10/Jan/2024:15:32:01 +0800,用第三方库 dateutil.parser.parse() 看似省事,但在高吞吐下性能差 3–5 倍,且对异常格式容错过强(比如把乱码也转成某个时间),反而掩盖日志格式变更问题。
正确方式:
- 写死格式串:
datetime.strptime(ts, '%d/%b/%Y:%H:%M:%S %z') - 捕获
ValueError单独记录异常行(常见于日志轮转瞬间的不完整行或 Nginx 异常 crash 写入) - 注意
%b依赖系统 locale,Linux 服务器多为Clocale,月份缩写是英文;若 locale 不一致,改用映射字典手动替换Jan→01等更稳妥
按小时聚合统计时,用 collections.defaultdict(dict) 而非嵌套 if not in
比如要统计每小时的 4xx/5xx 数量,容易写出这样的逻辑:
if hour not in stats:
stats[hour] = {}
if status not in stats[hour]:
stats[hour][status] = 0
stats[hour][status] += 1更简洁安全的做法是:
- 用
from collections import defaultdict,初始化为defaultdict(lambda: defaultdict(int)) - 直接写
stats[hour][status] += 1,避免重复键判断 - 若需最终导出 JSON 或写入数据库,再用
dict(stats)转回普通 dict —— 中间过程别过早固化结构
真正麻烦的是日志时间跨天、跨月甚至跨年,且 Nginx 没有严格按秒刷盘,同一秒内多条日志顺序可能和写入顺序不一致。这时候“按小时聚合”本质是离散桶操作,只要桶边界清晰(如 2024-01-01-14),就别纠结毫秒级排序。

















