3. 日志文件读取与解析:open()、readlines()、逐行读取大文件、编码处理
日志这东西,说白了就是程序的「黑匣子」。你想想看,生产环境出问题,第一反应肯定是翻日志。但日志文件动辄几百 MB 甚至几个 GB,怎么读、怎么解析,这里头门道不少。我刚开始写审计脚本时,直接 read() 整个文件,结果服务器内存差点爆了。嗯,从那以后我再也不敢轻视文件读取这件事了。
3.1 文件打开:open() 的正确姿势
Python 内置的 open() 是读取文件的入口。别小看这个函数,参数选不对,后面全是坑。
| 参数 | 作用 | 常用值 |
|---|---|---|
file | 文件路径(推荐路径) | /var/log/audit.log |
mode | 打开模式 | 'r'(只读文本)、'rb'(只读二进制) |
encoding | 文本编码(关键!) | 'utf-8'、'gbk' |
errors | 编码错误处理 | 'strict'(默认)、'ignore'、'replace' |
核心经验:始终指定 encoding 参数。不写的话,Windows 下会按系统默认编码(比如 GBK),Linux 下是 UTF-8,跨平台就跑偏了。我个人习惯写成 encoding='utf-8', errors='replace',遇到无法解码的字符用 � 替代,总比直接崩溃强。
小技巧:使用 with 语句自动关闭文件,别忘写 f.close()。
3.2 读取方式对比:readlines() vs 逐行读取
文件打开后,怎么读?三个方法各有场景,我整理了一张对比表。
| 方法 | 内存占用 | 速度 | 适用场景 |
|---|---|---|---|
f.read() | 极高(整个文件载入) | 快(一次读完) | 小文件(<10 MB) |
f.readlines() | 高(按行列表载入) | 中等 | 中等文件,需随机访问行 |
for line in f | 低(一次一行) | 稍慢但稳定 | 大文件(>100 MB) |
举个例子,审计日志往往持续增长,单文件可能到 2GB。用 readlines() 的话,2GB 的列表直接吃光所有可用内存。我经历过一次线上 OOM,就是同事写的脚本用了 readlines() 处理几个 G 的 web 日志。从那以后,我还审计脚本里加了内存预警。
# 逐行读取(推荐用于大文件)
with open('/var/log/audit.log', 'r', encoding='utf-8', errors='replace') as f:
for line in f:
# 处理每一行(低内存)
process(line)
注意:for line in f 是延迟迭代,文件句柄底层按缓冲区读取,不会一次性把全部行读进内存。这是处理大日志文件的正确姿势。
3.3 逐行读取大文件的进阶技巧
光用 for line in f 还不太够。生产环境里日志文件可能正在被写入(比如 rsyslog 持续追加),我们需要处理这种情况。
- 按块读取:用
f.read(chunk_size)读取固定大小字节,然后自行按换行符拆分。 - 使用
fileinput模块:适合同时处理多个日志文件。 - 实现进度反馈:大文件处理时间长,加个进度打印或日志桩。
# 按块读取大文件(兼顾速度和内存)
def read_large_log(file_path, chunk_size=65536):
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
buffer = ''
while True:
chunk = f.read(chunk_size)
if not chunk:
break
buffer += chunk
# 按换行符分割处理
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
yield line
# 最后可能剩一行不带换行符
if buffer:
yield buffer
个人实践:我在某次日志巡检项目里,日志文件以分钟级轮替,单个文件不到 50 MB,但一天生成上百个。用 fileinput 配合 glob 通配符,一行代码搞定多文件遍历,省事不少。
3.4 编码处理——日志解析的暗礁
编码问题是审计脚本最容易忽略的陷阱。日志来源多样:系统日志可能是 UTF-8,老旧应用可能是 GBK,还有混入乱码字节的。
常见的编码异常及处理方案:
| 异常 | 原因 | 解决办法 |
|---|---|---|
UnicodeDecodeError | 实际编码与指定编码不匹配 | 用 errors='replace' 或 errors='backslashreplace' |
乱码(如 锟斤拷) | 底层数据部分损坏或混合编码 | 记录行号跳过,或使用 chardet 自动检测 |
| 文件头带 BOM | Windows 记事本保存的 UTF-8 BOM | 指定 encoding='utf-8-sig' |
避坑提醒:我曾经遇到一个生产问题——脚本跑了两小时,最后因为第 100 万行有个 GBK 编码的字符直接崩溃,前面全部白干。所以,一定不要用 errors='strict'(默认值)。除非你确认日志 100% 纯 UTF-8。
3.5 本章知识结构全景图
下图为日志文件读取与解析的核心流程,我把关键节点和注意事项都画出来了。
3.6 实战代码片段:审计日志巡检初版
综合以上知识点,一个生产可用的日志巡检读取模板大概长这样:
import os
def safe_read_log(log_path):
try:
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
# 去除空白行
line = line.strip()
if not line:
continue
# 示例:判断是否包含 ERROR 或 audit
if 'ERROR' in line or 'audit' in line:
print(line) # 实际应写入结果集
except FileNotFoundError:
print(f"[警告] 日志文件不存在: {log_path}")
except PermissionError:
print(f"[警告] 无权限读取: {log_path}")
最后说一句:读取是基础,但往往就是基础最容易被忽视。我这几年审过不少同事的脚本,十有八九都在编码和内存上翻过车。你想想看,一个审计脚本如果因为编码崩溃,那还谈什么安全巡检?所以,把 open() 的编码参数写进你的脚本模板里,养成习惯,能省掉很多线上事故。