机器学习入门:日志采集 数据预处理
最近打算入门机器学习,跟着教程跑了一圈,发现数据集全是现成的:鸢尾花、MNIST、泰坦尼克,下个 csv 直接
pd.read_csv()就完事了。但总觉得这样学不踏实,正好手头有台服务器,nginx、ssh 天天在上面写日志,这不就是现成的数据吗... 于是决定从日志采集做起,把数据这条链路完整走一遍,踩坑记录如下。
日志从哪来
日志其实不用刻意“采集”,服务器上到处都是。我这里主要用两个来源:
/var/log/nginx/access.log:nginx 访问日志,量大,字段规整,适合练手/var/log/auth.log:ssh 登录记录,失败登录一大堆,后面想做异常检测正好用得上
如果是 Docker 部署的服务,也可以 docker logs --since 24h 容器名 直接导,或者去翻 /var/lib/docker/containers/ 下面的 json 文件,不过那个格式读起来更难受就是了。
一开始想过上 Filebeat + ELK 那套,配了半天感觉太重了,就为了拿点日志做实验,先整一套分布式采集纯属杀鸡用牛刀。最后决定简单粗暴,写个脚本定时把日志拷到工作目录:
mkdir -p ~/ml-logs/raw
cp /var/log/nginx/access.log ~/ml-logs/raw/access.$(date +%F).log
丢进 crontab 每天凌晨跑一次,数据就这么慢慢攒起来。对入门来说够用了。另外注意 logrotate 会把日志切成带日期的压缩包,抓的时候小心点,不然轮转那几天的数据就漏了。
先看看数据长什么样
拿到数据先别急着写代码,head -n 3 看一眼:
192.168.1.23 - - [10/Jun/2025:14:03:22 +0800] "GET /index.html HTTP/1.1" 200 5123 "https://example.com/" "Mozilla/5.0 ..."
一行日志里其实啥都有:IP、时间、方法、路径、状态码、UA。教程里常说“数据和特征决定上限”,这话到动手的时候才真正理解——现在这些原始字符串,模型是不认的,得先变成表格。
正则解析:脏活但绕不开
解析 nginx 日志的正则网上到处都是,我抄了一个稍微改了改:
import re
import pandas as pd
pattern = re.compile(
r'(?P<ip>\S+) - (?P<user>\S+) \[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<path>\S+) (?P<proto>[^"]+)" '
r'(?P<status>\d{3}) (?P<size>\S+)'
)
rows = []
with open('access.log', encoding='utf-8', errors='ignore') as f:
for line in f:
m = pattern.match(line)
if m:
rows.append(m.groupdict())
df = pd.DataFrame(rows)
注意那个 errors='ignore',日志里混进几个奇怪字节太正常了,不加这个参数直接给你抛异常,别问我怎么知道的。匹配不上的行(一般是扫描器发的畸形请求)会直接被丢掉,先记个数,丢得太多再回头查。
清洗:脏数据比想象中多
真开始清洗才发现,日志这东西看着规整,脏起来花样百出。- 这种占位符到处都是,得统一转成 NaN;状态码、字节数是字符串,要转成数字;再顺手去个重:
df['status'] = pd.to_numeric(df['status'], errors='coerce')
df['size'] = pd.to_numeric(df['size'], errors='coerce')
df = df.dropna(subset=['status']).drop_duplicates()
还有时间。nginx 的时间格式是 10/Jun/2025:14:03:22 +0800,pandas 直接 parse 会翻车,得手动指定格式:
df['time'] = pd.to_datetime(df['time'], format='%d/%b/%Y:%H:%M:%S %z')
这里卡了我一会儿,%b 是英文月份缩写,系统 locale 不对的时候会解析失败,报的错还特别迷惑。多提一嘴,省得有人跟我一样对着报错发呆。
特征处理:变成模型能吃的样子
原始字段还不能直接喂模型,简单做了几件事。
把时间拆成小时、星期几,后面的分析基本都是围绕“什么时间点不正常”来的:
df['hour'] = df['time'].dt.hour
df['weekday'] = df['time'].dt.weekday
类别编码方面,状态码、请求方法这类字段先做个简单映射就够用;至于 path 这种高基数字段,入门阶段就别硬上编码了,取个长度、或者按是否包含 .php 之类的规则变成 0/1,反而更实在。
归一化。字节数这个字段跨度太大,正常请求几百字节,下载接口可能几十兆,直接 MinMax 一把:
from sklearn.preprocessing import MinMaxScaler
df['size_scaled'] = MinMaxScaler().fit_transform(df[['size']])
最后存成 parquet,比 csv 快得多,类型也不会丢:
df.to_parquet('access_clean.parquet')
写在最后
走完这一趟,最大的感受是:教程里一笔带过的“数据预处理”,实际干起来占掉的时间比想象中多得多,但也就是这一步,决定后面模型有没有东西可学。
下一篇打算拿这份处理好的日志试试点简单的异常检测,比如用孤立森林找出可疑的访问。不出意外的话,到时候又是一堆新坑,踩完再来记录。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。