简介:一套面向计算机、信息安全等专业毕业设计场景的网络入侵检测系统完整项目资料,基于Python实现,包含完整源码、训练数据集与技术文档,适用于本科毕业设计、研究生课程实践及安全工程二次开发参考。资料包共48个文件,以C源码、HTML说明页、PDF技术手册及GZ源码包等类型为主,整体大小16.84MB,目录结构清晰便于按模块查阅。已有72人浏览学习。内容涵盖数据采集层、特征处理引擎、异常检测核心算法与可视化报告模块,数据集含多种典型攻击流量样本;各模块均经多轮功能测试,可直接部署,并支持调整算法参数、扩展特征或集成新型检测模型增强功能。文档详细说明系统配置流程、算法原理与性能评估方法,另附Git协同开发指南、libpcap/Snort/libnids源码包及《Linux网络入侵检测系统》《Snort入侵检测系统源码分析》等资料,可支撑源码阅读与二次开发完整链路。
1. 拿到“基于Python的网络入侵检测系统”这个题目,先别急着写代码
很多同学拿到“基于Python的网络入侵检测系统”这个毕业设计题目,第一反应是去扒一份现成的NIDS源码,改改界面就交差。我的建议正好相反:先放下代码,把数据集分析做明白。这个题目的分数差距,基本不在模型多高级,而在你有没有把“流量怎么变成特征、特征怎么变成标签”讲清楚。本文按我做毕设时的路线展开:先选数据与特征方向,再处理NSL-KDD和CICIDS2017两个数据集,然后训练并评估随机森林/XGBoost分类器,最后把模型封装成一个可实时读取pcap的检测工具,并列出我在这个过程中踩过的四个大坑。适合正在做毕设、或者准备接网络安全方向实战项目的你参考。
2. 数据集选型与特征方向:NIDS 的设计基线
2.1 两个方向:基于数据包特征与基于流量特征
网络入侵检测系统的常见做法可以分两个方向。
基于数据包特征:拿到一个 packet 直接抽字段,比如 IP 源/目的地址、端口、协议号、TCP 标志位、包长、窗口大小,组成一个向量丢给分类器。这种方案抓包轻量、实时性好,代码写起来也直接,但缺点很明显:每个包只有几十维特征,像 SYN Flood、端口扫描这类特征明显的攻击容易抓,慢速隐蔽攻击很难表达出来,实际部署时误报率偏高。
基于流量特征:先定义“流”(Flow),把一段时间内五元组相同的双向包聚合起来,统计出流时长、包数量、字节数、平均包长、每秒包数、各标志位计数等特征,再交给分类器。这种方案表达能力强,CICIDS2017官方就自带80多个流特征,绝大多数研究论文和开源工具都走这条路。缺点是有聚合延迟,不适合需要逐包判断的超低时延场景。
| 方向 | 特征维度 | 实时性 | 落地难度 | 典型场景 |
|---|---|---|---|---|
| 数据包特征 | 每包 20~50 维 | 高,逐包判断 | 低 | 轻量边缘设备、在线拦截 |
| 流量特征 | 每流 80+ 维 | 中,有聚合延迟 | 中 | 旁路检测、离线分析、论文实验 |
我一般建议毕设走流量特征路线:它和数据集天然匹配,能写出特征分析过程,模型可解释性也强,演示时还能用现成流文件做输入。所以本文主线就定为:抓包 → 提取流特征 → 建模 → 输出检测结果。
2.2 数据集选型:NSL-KDD、CICIDS2017 与 CICIDS2019
定了方向之后,下一个问题是数据从哪来。常见 NIDS 数据集有 NSL-KDD、CICIDS2017、CICIDS2019、UNSW-NB15 等。
NSL-KDD 是 KDD-CUP99 的改进版本,样本量从数百万削到约 12.5 万条,去掉了大量冗余记录。标签分成 Normal、DoS、Probe、R2L、U2R 五类,加载速度快,适合一切“先跑通再说”的前期工作。缺点是数据年代久远,很多攻击场景和现在内网流量差异很大。
CICIDS2017 是加拿大 CIC 用真实流量抓了 5 天得到的,包含 80 多维流特征,标签有 BENIGN 和 14 种攻击(DDoS、PortScan、Patator、WebAttack 等)。这是目前中文毕设最常见的选择,官方提供了完整 CSV 可以直接载入。缺点是文件很大,且 CSV 有表头重复、缺失值、Inf 混入等一堆问题,必须预处理。
UNSW-NB15 和 CICIDS2019 更新、更大,适合做扩展章节,不适合从零快速跑通。我的建议是:用 NSL-KDD 跑通第 1 版流程,用 CICIDS2017 做主实验和答辩数据,有余力再补 UNSW-NB15。这样节奏合理,文档里还能写一段“新旧数据集的泛化对比”,反而是加分项。
2.3 环境准备:Python 版本、依赖与目录结构
环境上先装好 Python 3.8 或 3.10,依赖列在一个 requirements.txt 里,避免做到一半换机器后环境崩掉。
numpy pandas scipy scikit-learn matplotlib seaborn scapy xgboost说明:scikit-learn 用于切分数据集、训练和评估;scapy 用于手动读 pcap 演示流聚合;xgboost 是比随机森林更强的梯度提升模型,论文里写对比实验时会用到。scapy 在 Windows 下需要安装 Npcap 驱动才能抓包,但如果只读 pcap 文件则不需要。
目录结构我习惯这样组织:
nids_project/ ├── data/ # 原始数据集 ├── features/ # 处理好的特征矩阵 ├── models/ # 训练好的模型和特征列清单 ├── src/ │ ├── load_data.py # 数据集读取与清洗 │ ├── build_features.py# 特征工程 │ ├── train_model.py # 模型训练与评估 │ └── detect.py # 检测工具入口 └── requirements.txt毕设往往有多个迭代版本,把数据、特征、模型分开放,重跑某个实验时不用重新读原始 CSV,能省不少时间。
3. 数据集分析落地:把 CSV 和 pcap 变成能训练的特征矩阵
3.1 NSL-KDD 快速建档:手动补列名与标签分布
NSL-KDD 原始文件是 CSV 格式,但没有表头,需要手动补 41 个特征列名加最后 1 个标签列。很多网上教程直接 pd.read_csv 不指定列名,结果第一行数据被当成表头,后面全部错位,这是最早会遇到的坑。
import pandas as pd from collections import Counter col_names = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'logged_in', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login', 'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count', 'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate', 'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate', 'label' ] train_df = pd.read_csv('KDDTrain+.txt', header=None, names=col_names) print(train_df.shape) print(Counter(train_df['label'])) print(Counter(train_df['protocol_type']))逻辑说明:这段代码的核心动作有三个。第一是 header=None 告诉 pandas 数据文件没有表头,然后通过 names 参数一次性把列名挂上去,避免后续每次访问列名都写错。第二是用 Counter 统计标签分布,先看清数据长什么样再谈建模。第三是打印 protocol_type 的取值,因为后面要做哑变量编码时必须知道一共有几个枚举值。
参数说明:col_names 顺序必须和原始 KDDTrain+.txt 的列顺序一致,不能凭感觉前后调换;label 列在不同版本里可能写的是 attack 类型名,也可能是已经归好的五类,要先打印出来确认。
3.2 CICIDS2017 读取与清洗:表头空格、Inf、重复行
CICIDS2017 的官方 CSV 和 NSL-KDD 是完全不同的画风:列名带空格(比如 Flow ID),某些版本表头重复出现两行,特征列里混着 Infinity 和空值。直接 pd.read_csv 读进来,整个 DataFrame 全是 object 类型,模型根本没法训练。
import pandas as pd import numpy as np df = pd.read_csv('Friday-WorkingHours-Afternoon-DDos.pcap_ISCX.csv', skiprows=1) # 列名去掉首尾空格,并把空格替换成下划线,方便后续用点号访问 df.columns = [c.strip().replace(' ', '_') for c in df.columns] # 全空行丢弃,Infinity 转成 NaN 后统一丢弃 df.replace([np.inf, -np.inf], np.nan, inplace=True) df.dropna(how='all', inplace=True) df.dropna(inplace=True) # 标签列去空格,再看分布 df['Label'] = df['Label'].str.strip() print(df['Label'].value_counts())逻辑说明:skiprows=1 是针对某些版本表头重复的补救手段,如果你的文件第一行就是列名,这句会误删表头,所以读取前先打开文件看一眼。列名清洗是必须的,否则 read_csv 生成的列名可能带前后空格,正常时看不出来,一用 df['Flow ID'] 就报错。Inf 转 NaN 再 dropna 是为了保证模型输入稳定,sklearn 遇到 Inf 会直接报 ValueError,而且定位很费时间。
参数说明:Friday-WorkingHours-Afternoon-DDos 只是其中一个文件,CICIDS2017 按天拆成多个 CSV,比如 Monday-WorkingHours.pcap_ISCX.csv。如果做全量数据,需要把多个 CSV concat 起来,或者分别处理后再合并;同时注意 Label 列的值分布极不均匀,DDoS 类样本可能占了一半,后面训练时必须分层抽样或做类别平衡处理。
3.3 从 pcap 到流特征:scapy 的五元组聚合示例
流特征的核心思想是“把包变成流”。官方 CICIDS2017 的 pcap 是经过 CICFlowMeter 工具处理后才生成 CSV 的,为了讲清楚这个过程,我会在毕设里先用 scapy 写一个最小版本:按五元组聚合,统计每个流的包数、总字节数和时长。
from scapy.all import rdpcap from collections import defaultdict import pandas as pd pcap = rdpcap('sample.pcap') flows = defaultdict(list) for pkt in pcap: # 只处理 IP over TCP 的包,UDP、ICMP 另行统计 if not (pkt.haslayer('IP') and pkt.haslayer('TCP')): continue src = pkt['IP'].src dst = pkt['IP'].dst sport = pkt['TCP'].sport dport = pkt['TCP'].dport key = (src, dst, sport, dport) flows[key].append(pkt) rows = [] for key, pkts in flows.items(): total_len = sum(len(p) for p in pkts) rows.append({ 'src_ip': key[0], 'dst_ip': key[1], 'src_port': key[2], 'dst_port': key[3], 'pkt_count': len(pkts), 'total_bytes': total_len, 'duration_sec': max(p.time for p in pkts) - min(p.time for p in pkts) }) flow_df = pd.DataFrame(rows) print(flow_df.head())逻辑说明:把同一个五元组的所有包归到一个列表,然后对每个列表做聚合计算,这就是流特征的最小实现。src_ip、dst_ip、src_port、dst_port 是键的一部分,但训练时不能直接用 IP 地址做特征——IP 到具体用户是可识别的,而且训练集和测试集的 IP 不同会导致模型学废。实际特征是 pkt_count、total_bytes、duration_sec 这类统计值。
参数说明:rdpcap 在文件超过几百 MB 时内存消耗很夸张,毕设演示用的小 pcap 没事,处理 CICIDS2017 官方 pcap 时不要直接 rdpcap,建议用 CICFlowMeter 或 tshark 先转成 CSV。这就是为什么上一节强调“先用官方 CSV 跑通,pcap 只做演示”。五元组还可以加上协议号,TCP 和 UDP 要分开统计,否则聚合出来的流语义会混。
3.4 特征缩放与时序窗口:两个让模型精度稳步上升的处理
特征矩阵做好后,数值范围差距非常大:src_bytes 可能是几千,serror_rate 是 0 到 1 的小数,如果不缩放,树模型还好,但如果你后面用到 SVM、MLP,梯度更新会被大数值特征的梯度淹没。常见做法是用 StandardScaler 对数值列做标准化,公式是 (x - mean) / std,让特征均值为 0、方差为 1。
第二个处理是时间窗口。CICIDS2017 是连续几天抓的流量,官方流特征没有“当前流离上一流多久”这个概念,但很多攻击有爆破性质:短时间内同一源 IP 发起大量请求。手工加一列 group by src_ip 的流数量计数,会显著提升检测效果。这种手工特征在答辩时也比较好讲,是数据分析和业务理解结合的体现。
from sklearn.preprocessing import StandardScaler feature_cols = ['pkt_count', 'total_bytes', 'duration_sec', 'src_port'] scaler = StandardScaler() X_scaled = scaler.fit_transform(flow_df[feature_cols]) print(X_scaled[:3])逻辑说明:StandardScaler 必须在训练集上 fit,然后用同一个 scaler 去 transform 测试集,不能对全量数据先 fit 再切分,否则会造成数据泄露,测试集信息渗入训练过程,评估结果虚高。这个细节在答辩时经常被追问,提前处理掉能省很多麻烦。
4. 模型训练的源码实现:从特征矩阵到随机森林与 XGBoost
4.1 分层抽样与基线随机森林:先跑通再谈调参
模型部分我建议从随机森林起步,因为它对不平衡数据天生有一定容忍度,不需要复杂调参就能得到可用的结果,而且特征重要性可以直接输出,论文里很有用。先做分层抽样保证训练集和测试集里攻击比例一致。
from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report # X: 特征矩阵, y: 二分类标签(1=攻击, 0=正常) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) clf = RandomForestClassifier( n_estimators=200, max_depth=15, min_samples_leaf=2, n_jobs=-1, random_state=42, class_weight='balanced' ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred, digits=3))参数说明:stratify=y 是核心,不做分层抽样时,如果原始数据攻击类占 5%,测试集的攻击占比可能只有 2%,少数类评估结果就失真。max_depth=15 和 min_samples_leaf=2 是控制过拟合的,NIDS 特征多,全生长的树在训练集上表现很好但泛化差。class_weight='balanced' 让少数类获得更高权重,是一种你不改代码就能缓解不平衡的手段。
跑完打印 classification_report,先看加权 F1,再看攻击类的 recall。如果攻击类 recall 很低,说明模型把太多攻击放成了正常流量,这是误报之外更危险的事。
4.2 类别不平衡的三个处理方向:过采样、阈值移动、异常检测
NIDS 数据集的类别不平衡是天然的:正常流量占比极高,攻击流量只占一小部分。处理方向有三个。
第一是类权重法,上面代码里的 class_weight='balanced' 就是。原理是让少数类的损失函数权重变大,惩罚更重。缺点是如果少数类样本太少或噪声多,容易过拟合到少数的几个样本上。
第二是阈值移动法,这是我最常用也最快见效的。分类器输出的其实是一个概率,predict 默认按 0.5 判定,但 0.5 对少数类不够友好。训练时用正常的 class_weight='balanced',预测时手动把阈值提到 0.7 或 0.8,宁可让正常流量多报几个,也不放过攻击。
prob = clf.predict_proba(X_test)[:, 1] y_pred_th = (prob >= 0.7).astype(int) print(classification_report(y_test, y_pred_th, digits=3))逻辑说明:这个操作是纯推理阶段的调整,不重训模型,改一行就可以做阈值扫描实验,画出 precision/recall 随阈值变化的曲线,是毕设里很好写的一个对比实验。
第三是 SMOTE 过采样。imblearn 库里有现成的 SMOTE,原理是在少数类样本之间插值合成新样本。注意:SMOTE 在训练集上做,绝不能在测试集上做;而且合成样本可能制造人为的数据分布,真实流量的新攻击仍然测不出,所以只把它当作实验对比项,不作为主方案。
4.3 评价指标:为什么不看整体准确率
很多初学者训练完只看 accuracy,这在 NIDS 场景是致命的。假设数据里 95% 是正常、5% 是攻击,模型把所有流量全判成正常,整体准确率也能到 95%,但这个模型没有任何检测价值。
| 指标 | 计算方式 | NIDS 场景含义 |
|---|---|---|
| Accuracy | 全部预测正确 / 全部样本 | 数据不平衡时极易虚高,不推荐作为主指标 |
| Precision | TP / (TP + FP) | 报警中有多少是真的攻击,高则误报少 |
| Recall | TP / (TP + FN) | 攻击中有多少被抓住,高则漏报少 |
| Macro-F1 | 各类 F1 的平均 | 对少数类更敏感,不平衡场景推荐 |
| Weighted-F1 | 按各类占比加权 | 更贴近整体,但会掩盖少数类问题 |
我的习惯是盯着攻击类的 Recall 和整体 Macro-F1,报警宁可多一些也不能漏。误报可以靠阈值调,漏报才是真正的问题。把混淆矩阵画出来,能直接看到漏报集中在哪个类别。
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(y_test, y_pred) ConfusionMatrixDisplay(cm, display_labels=['Normal', 'Attack']).plot(cmap='Blues') plt.savefig('confusion_matrix.png', dpi=150)逻辑说明:ConfusionMatrixDisplay 把结果可视化成 2x2 矩阵,左上角是正常判正常,右下角是攻击判攻击,右上角是误报,左下角是漏报。NIDS 场景我更关注左下角,数量多说明模型把攻击当成了正常流量,实际部署时防线被绕过。
5. 搭建 NIDS 时踩过的坑与排查清单
5.1 NSL-KDD 训练集 99% 准确率,一换数据就废
现象:在 NSL-KDD 的 KDDTest+ 上测试准确率高达 99%,我以为模型已经完美,换到 CICIDS2017 的小样本上一测,攻击类召回率直接掉到 40% 以下。
原因:NSL-KDD 是 2000 年左右的仿真流量,攻击类型和当前现实流量差异非常大。高准确率说明模型记住了 NSL-KDD 的特征组合,而不是学到了通用的攻击检测模式。
解决:把 NSL-KDD 当作流程验证,主实验改用 CICIDS2017。答辩时主动说清楚两个数据集的差异,反而加分。
5.2 CICIDS2017 的 CSV 读进来全是 object 类型
现象:pd.read_csv 直接读取官方 CSV,特征列的数据类型全是 object,train_test_split 一执行就报 ValueError,错误提示还特别长。
原因:官方文件列名带空格和特殊字符,某些属性列混入了省略号和字符串形式的数值,pandas 自动推断类型失败,干脆全部按字符串处理。
解决:读取后用 df.columns 清洗列名,replace 把 Inf 转 NaN,再手动用 pd.to_numeric 转换特征列。排查时先打印 df.dtypes,看到 object 就立刻定位到异常列,不要带着未知类型直接训练。
5.3 少数类在分类报告里 Recall 为 0
现象:训练完,classification_report 显示某些攻击类的 recall 是 0.00,precision 也是 0.00,模型完全没发现这类攻击。
原因:类别严重不平衡,且少数类样本量太少,随机森林的决策边界全偏向多数类。默认阈值 0.5 对少数类要求太苛刻。
解决:先加 class_weight='balanced' 重训,再把预测阈值调到 0.3~0.4 重新判,最后尝试 SMOTE 做对比实验。注意每次修改阈值后都要重新看混淆矩阵,确认漏报降下来时误报是否还能接受。
5.4 实时抓包预测时正常流量一直在报警
现象:训练时测试集表现很好,但把模型接上实时流量后,正常访问网页的流量也被判定为攻击,报警刷屏。
原因:训练时用的是完整流的统计特征,一条流结束后才能算出 duration、总包数这些值。实时检测时流还在进行中,特征分布和完整流完全不同,相当于拿训练集的分布去预测一个完全不同分布的数据。
解决:实时场景必须采用“固定时间窗口”或“固定包数窗口”聚合。每 10 秒切一个窗口,窗口内统计特征,模型预测完清空窗口重新统计。这样做需要保证训练时也用同样的窗口语义构建特征,不能训练时用整条流、预测时用窗口。
6. 跑起来:把模型封装成一个命令行检测工具
最后一步是让这套东西从 Jupyter Notebook 里走出来,变成一个可执行的检测工具。我一般会把训练好的模型和特征列清单用 pickle 保存,再写一个 detect.py 接收 pcap 文件路径,输出预测结果。
import sys import pickle import pandas as pd from scapy.all import rdpcap # 加载训练时保存的模型和特征列 model = pickle.load(open('models/model.pkl', 'rb')) feature_cols = pickle.load(open('models/feature_cols.pkl', 'rb')) def build_features(pcap_path): # 调用本文 3.3 的聚合逻辑,输出和训练时相同的特征列 df = flow_aggregate(rdpcap(pcap_path)) return df[feature_cols] if __name__ == '__main__': X = build_features(sys.argv[1]) prob = model.predict_proba(X)[:, 1] pred = (prob >= 0.7).astype(int) print('攻击流量比例: {:.2%}'.format(pred.mean()))说明:build_features 必须复用训练时的聚合函数,保证特征语义一致,这是整个工具最容易出 bug 的地方。输出攻击比例是为了演示效果,实际部署时应该输出具体哪条流被判为攻击,以及命中概率。
验证方法是我一直坚持的习惯:步骤一,拿测试集里的 pcap 和对应标签做回放,看工具输出和测试集标签一致;步骤二,用正常流量的 pcap 跑一遍,看误报数量能不能接受;步骤三,去下载一个和训练集不同类型的攻击流量试试,观察模型对新攻击的泛化能力,这比任何指标都真实。
这整套做下来,毕业设计的核心闭环就通了:数据有分析,特征有来源,模型有评估,工具能演示。最后分享一个我的习惯:任何模型先打印基线的混淆矩阵再开始调参,先有基线,再谈优化——这一条帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取