恶意网站检测工程:SVM、随机森林与DNN特征工程实战

发布时间:2026/10/3 3:33:19
恶意网站检测工程:SVM、随机森林与DNN特征工程实战
简介这是一套面向网络安全与机器学习教学场景的恶意网站检测系统实现通过特征提取与多算法对比完成黑白名单网站识别适合高校计算机、人工智能专业学生、网络安全研究者及入门开发者使用。打包为zip格式共11个文件含5个Python脚本特征提取、可视化与模型调用、3个备份文件、2个压缩数据集和1个Markdown说明文档整体仅3.32MB结构清晰便于快速上手。核心流程由特征提取脚本完成特征向量提取可视化脚本生成分布图并配有特征对照表算法涵盖支持向量机、随机森林与深度神经网络三种模型均经过交叉验证分类准确率均达95%以上。已有49人学习包内含完整可运行代码、备份文件与说明文档支持特征工程扩展和参数调优可直接用于课程实验、毕业设计或恶意网站过滤等场景同时需遵守学术规范勿用于商业用途。1. 恶意网站检测到底难在哪这份工程把特征、数据、算法一次给齐了恶意网站检测这个方向理论上人人都能说几句——提取 URL 特征、训练分类器、判别黑白名单听起来很顺。但真动手做过的人都会卡在同一个地方数据集从哪来特征怎么对齐为什么自己搭的 SVM 跑出来准确率只有七成论文里却都是 95% 以上这个基于 SVM、随机森林与 DNN 的恶意网站检测系统把最麻烦的部分提前解决了。压缩包里的 data.zip 是整理好的训练数据2.xlsx 提供了特征对照表translate.py 负责把原始网站信息转成特征向量三个算法文件分别实现了 SVM、随机森林、DNN交叉验证下准确率都在 95% 以上。适合正在做机器学习课程设计、毕业设计或者想快速跑通一个「从数据到模型」完整流程的人——你不用先吭哧吭哧爬几万条数据拿到就能训练、能对比、能改参数。2. 数据准备与特征工程translate.py 怎么把网站变成模型能吃的向量2.1 先看清数据集结构data.zip 与 2.xlsx 的分工打开压缩包你会发现这个项目的核心不是算法代码而是数据。data.zip 里是原始样本2.xlsx 是一张特征对照表。这两者的关系是数据样本负责提供原始信息特征对照表负责告诉你怎么把这些信息转成数值。做过机器学习的人都知道模型不认字符串它只认矩阵——每一行是一个样本每一列是一个特征值必须是数字。2.xlsx 干的就是这件事它把 URL 长度、特殊字符数量、域名是否包含数字、Alexa 排名区间、WHOIS 注册天数这类信息映射成固定维度的数值列。你在跑 translate.py 之前务必先把 2.xlsx 打开翻一遍确认列顺序和特征含义。这个动作花不了五分钟但能避免后面特征顺序对不上训练出来的模型全乱套的尴尬。常见的做法是拿带标注的样本集做二分类正常网站标 0恶意网站标 1。恶意网站的范畴很广钓鱼、挂马、色情、赌博站点都算但在实践项目里通常先做粗粒度二分类把恶意整体当作正样本。这样标注成本低模型也更容易收敛。后续想细化到恶意类型识别再在类别标签上做扩展这是后话。2.2 translate.py 的提取逻辑一行样本对应一个向量translate.py 是这个系统的特征提取入口。它的核心任务是把每一条网站记录转成一个定长的数值向量。典型实现会是这样import pandas as pd import numpy as np import re from urllib.parse import urlparse def extract_features(url, whois_days, alexa_rank): 把一条网站记录转成特征向量特征顺序必须与 2.xlsx 对齐 parsed urlparse(url) features [] # 1. URL 基础特征 features.append(len(url)) # URL 总长度 features.append(len(parsed.netloc)) # 域名长度 features.append(url.count(.)) # 点的数量 features.append(url.count(-)) # 连字符数量钓鱼站常见 features.append(1 if re.search(r\d, parsed.netloc) else 0) # 域名是否含数字 # 2. 域名与注册信息特征 features.append(whois_days) # 域名注册天数恶意站点通常短命 features.append(alexa_rank if alexa_rank else 999999) # Alexa 排名无排名给大数 # 3. 关键词命中特征 suspicious_words [secure, account, verify, login, update] features.append(sum(1 for w in suspicious_words if w in url.lower())) return np.array(features, dtypefloat) # 批量处理数据 df pd.read_excel(2.xlsx) # 读取特征对照表 X np.array([extract_features(row[url], row[whois_days], row[alexa_rank]) for _, row in df.iterrows()]) np.save(features.npy, X)这段代码展示了特征提取的标准姿势。三个特征块分别对应 URL 结构特征、域名档案特征、关键词命中特征——这三类特征是恶意网站检测里最常用的特征维度。参数说明URL 总长度和特殊字符数量反映的是URL 伪装复杂度恶意站点为了模仿正规网站经常把 URL 拉得很长域名注册天数是最有区分度的特征之一正经站点不会每天换域名钓鱼站却常使用刚注册的域名Alexa 排名无值时给 999999 大数是为了让模型识别出查无排名的冷门域名。运行完这段每一条样本就变成一个等长的 numpy 向量SVM、随机森林、DNN 三个模型才能往下吃数据。需要注意真实项目里特征数量远不止这几个。你可以在 2.xlsx 基础上加特征列比如 TLS 证书是否有效、页面内链接占比、表单提交是否走 HTTPS 等。这个项目的结构允许你直接扩展特征维度translate.py 里加一段提取逻辑就行。2.3 跑通流程先特征提取再可视化最后训练项目里的执行顺序是固定的先跑 translate.py 得到特征向量再跑 typlist.py 生成分布图最后分别跑三个算法文件。我用 pyplot 跑这段流程时最容易踩的坑是没装依赖库——pandas、scikit-learn、tensorflow 一个都不能少。建议用 Anaconda 建一个 Python 3.7 或 3.8 的环境一次性装齐。pip install pandas numpy scikit-learn tensorflow matplotlib openpyxl python translate.py python typelist.py python SVM.py如果是第一次跑先别改任何参数把原始流程原样跑通。跑通后再去改特征和参数否则你分不清报错是环境问题还是代码问题。我习惯每跑完一步检查一下输出文件是否生成——features.npy 有没有大小、分布图能不能打开都是很好的 sanity check。3. 三种算法同台竞技SVM、随机森林与 DNN 的实现与参数解读3.1 SVM.py从线性超平面到 RBF 核的升级支持向量机的核心思想是在特征空间中找一个超平面把两类样本分开并且让这个超平面到两侧样本的间隔最大。恶意网站检测的场景里特征维度通常不高几十维样本量几千到几万条SVM 完全吃得消而且在小样本下泛化能力比深度学习强。训练 SVM 的典型代码from sklearn.svm import SVC from sklearn.model_selection import cross_val_score, train_test_split from sklearn.preprocessing import StandardScaler # 加载特征与标签 X np.load(features.npy) y np.load(labels.npy) # 0 正常1 恶意 # SVM 对特征尺度敏感必须先标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 划分训练集与测试集 X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42, stratifyy) # RBF 核的 SVM model SVC(kernelrbf, C1.0, gammascale, probabilityTrue, random_state42) model.fit(X_train, y_train) print(训练集准确率:, model.score(X_train, y_train)) print(测试集准确率:, model.score(X_test, y_test))逻辑说明StandardScaler这一步是 SVM 的血泪经验——SVM 依赖距离计算如果某个特征的数值范围是 0~999999比如 Alexa 排名而另一个是 0~1距离会被大数值特征完全主导模型等于瞎了。kernelrbf是默认情况下最好用的核函数它能映射到无限维空间处理非线性边界C1.0是误分类惩罚系数越大越不容忍错误、越容易过拟合越小越平滑、越容易欠拟合gammascale让 sklearn 根据特征数量自动计算 gamma 初值比写死一个数要稳。训练集和测试集准确率如果出现训练 99%、测试 82%的差距那就是过拟合了。下调 C 到 0.1 或者减小 gamma是优先尝试的方向。3.2 forest_split.py随机森林与决策树的区别到底在哪随机森林是 Bagging 思想和决策树结合的产物训练多棵决策树每棵树用不同的随机子样本和随机特征子集最后投票决定样本类别。跟单棵决策树比随机森林的最大区别是「多样性」——单棵树容易深度过深、把噪声都背下来随机森林通过随机采样和特征随机选择让每棵树各自犯错且错误不相关投票时错误互相抵消。这也是为什么实践中随机森林很少需要精细调参就能达到 95% 上下的准确率。一个可用的训练片段from sklearn.ensemble import RandomForestClassifier # 随机森林不需要特征标准化树模型对尺度不敏感 model RandomForestClassifier( n_estimators200, # 树的数量 max_depthNone, # 不限制深度靠集成来控制方差 min_samples_split5, # 内部节点再划分所需最小样本数 min_samples_leaf2, # 叶子节点最少样本数 max_featuressqrt, # 每次分裂随机选 sqrt(n_features) 个特征 random_state2024, n_jobs-1 # 用满所有 CPU 核心 ) model.fit(X_train, y_train) # 特征重要性输出这个在恶意检测里特别有用 importance pd.Series(model.feature_importances_, index[url_len, domain_len, dot_count, hyphen_count, has_digit, whois_days, alexa_rank, keyword_hits]) print(importance.sort_values(ascendingFalse))max_featuressqrt是分类问题的标准取值——每棵树分裂时只看特征总数的平方根个候选特征强制制造随机性。min_samples_leaf2和min_samples_split5一起限制树的复杂度防止单棵树长到把每个样本都单独包起来。这两参数比max_depth更平滑我一般优先调它们而不是硬切深度。随机森林跑完会输出feature_importances_它能直接告诉你哪些特征对恶意网站检测贡献最大。我跑类似项目时域名注册天数几乎永远是第一或第二重要的特征这个结论跟安全行业的直觉一致——恶意域名生命周期短。3.3 DNN.py多层感知机做非线性特征学习深度神经网络在这里用的是多层感知机MLP结构堆几个全连接层做非线性变换。它的优势是不需要手工设计特征交叉中间层会自动学出高阶组合特征劣势是数据量大才划算小样本下容易过拟合。这个项目里 DNN 用的是典型的全连接结构import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping # DNN 对特征尺度敏感同样需要先标准化与 SVM 共用 scaler 即可 model Sequential([ Dense(64, activationrelu, input_shape(X_train.shape[1],)), Dropout(0.3), Dense(32, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) # 二分类输出 ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) # 训练早停验证集 loss 连续 5 轮不降就停 early_stop EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_split0.2, epochs100, batch_size32, callbacks[early_stop], verbose1 )逻辑说明第一层 64 个神经元接relu激活relu能缓解梯度消失问题是目前全连接网络的标准选择每个relu层后接一个Dropout(0.3)随机让 30% 的神经元不参与本次迭代这是深度网络在中小数据集上防止过拟合最有效的手段输出层是单个神经元加sigmoid输出值落在 0~1 之间解释为正样本概率大于 0.5 判恶意。EarlyStopping是必须加的——如果不加跑满 100 轮验证集 loss 早在第 15 轮就开始反弹了最后保存的模型反而是过拟合最严重的那个。加了这个回调训练会在验证集指标不再改善时自动停止并回滚到最佳权重。恶意网站检测里 DNN 的准确率通常跟随机森林差不多但调参成本高得多你的时间应该主要花在 SVM 和随机森林上。3.4 三个模型怎么对比准确率只是起点建议你把三个模型的准确率、精确率、召回率、F1 和训练时间放到一张表里对比。恶意网站检测场景里召回率比准确率重要——漏放一个恶意网站比误杀一个正常网站后果严重得多。三个模型交叉验证的结果在 95% 以上但分布可能不同随机森林可能精确率高、SVM 召回率高、DNN 在某个特征子集上更强。4. 特征可视化与实验设计typelist.py 帮你判断特征有没有效4.1 可视化到底在看什么类别重叠与特征区分度typelist.py 的作用是生成特征分布可视化图表。它的价值在于在你把数据扔给模型之前先用人眼确认一下特征到底有没有区分度。如果两类样本在某个特征维度上完全重叠那这个特征就是噪声喂给模型只会增加过拟合风险如果明显分开那模型训练的效果大概率不会差。通常你会在柱状图或散点图里看到正常样本的 URL 长度集中在 20~40 个字符区间恶意样本则拉出长尾域名注册天数这个特征差异更明显——正常网站的注册天数分布很宽从几百天到十几年都有而恶意站点的域名注册天数普遍短于一两年。这就是 2.xlsx 标注质量的直观验证。import matplotlib.pyplot as plt import seaborn as sns import pandas as pd # 假设 features.npy 和标签 npy 都已生成 df_viz pd.DataFrame(X, columns[url_len, domain_len, dot_count, hyphen_count, has_digit, whois_days, alexa_rank, keyword_hits]) df_viz[label] y # 挑选两个区分度高的特征画二维分布 plt.figure(figsize(8, 6)) sns.scatterplot(datadf_viz, xwhois_days, yurl_len, huelabel, alpha0.6, palette[#4C72B0, #C44E52]) plt.xscale(log) # whois_days 是长尾分布取对数能看得更清楚 plt.xlabel(WHOIS 注册天数 (log)) plt.ylabel(URL 长度) plt.title(正常网站与恶意网站的特征分布对比) plt.savefig(feature_distribution.png, dpi150)这段代码会生成一张散点图横轴是域名注册天数对数刻度纵轴是 URL 长度颜色区分标签。如果两类点明显聚集在不同的区域这个特征组合就是可用的如果你发现画出来的图两类点像撒了一把芝麻一样混在一起就要重新回看 translate.py 的特征提取逻辑。4.2 从可视化回推特征构造一次典型的迭代过程特征工程是迭代的。我的习惯是先跑出原始版本的可视化图对着图挑几个「看起来能分开」的维度再回到 translate.py 里加特征重新提取、重新训练。这个项目的好处是特征对照表很清晰加特征不需要改模型代码——模型读的是 features.npy只要列数变了模型结构会自动适配DNN 除外它要改 input_shape。实践中的常见扩展包括TLS 证书剩余有效期、备案信息是否存在、页面内外部链接比例、表单提交协议。你在 2.xlsx 里追加列在 translate.py 加提取逻辑重新生成 features.npy 就能对比新旧特征集的效果。如果新特征让测试集准确率提升不到 0.5 个百分点我建议丢掉它——多一个特征多一份维护成本收益不匹配就不值得。4.3 评价体系别只用准确率精确率、召回率与 AUC95% 准确率听起来很好但在恶意网站检测这种正负样本不均衡的任务里准确率有迷惑性——如果数据里 90% 是正常网站模型把所有样本都判成正常就能拿到 90% 准确率。所以交叉验证时每个模型都要额外输出精确率、召回率和 AUC。如果随机森林的准确率是 96% 但恶意网站的召回率只有 80%说明模型把不少恶意站点放过去了。优先调class_weightbalanced让模型对少数类恶意样本给予更高误分类代价或者用predict_proba输出概率后手动调阈值而不是用默认的 0.5。5. 避坑指南数据对齐、编码乱码与「假高准确率」5.1 特征列顺序错位训练好的模型全乱套现象train_test_split 之前准确率很高一旦换新数据预测结果完全随机甚至所有样本被判成同一类。原因测试阶段用的特征列顺序与训练阶段不一致。训练时你可能用了 2.xlsx 里第 1、3、5 列换数据时把 1、2、3 列喂了进去SVM 拿到的是完全不同的输入分布。解决统一用 translate.py 输出 features.npy不要手工拼接特征。在代码入口处固定特征列清单用列表定义一个常量训练和预测共用同一个列表。每次改特征都重新跑一遍完整流程。5.2 Windows 下编码报错UnicodeDecodeError 是常态现象跑 translate.py 或读 2.xlsx 时报UnicodeDecodeError: gbk codec cant decode byte ...。原因项目代码是按 UTF-8 写的Windows 中文环境默认用 GBK 读文件遇到 UTF-8 编码的中文内容就崩了。解决所有open()和read_excel()调用明确指定编码df pd.read_excel(2.xlsx) # pandas 读 Excel 一般没事 with open(typelist.py, r, encodingutf-8) as f: # 读代码文件时指定 content f.read()另外pd.read_csv()时加encodingutf-8或encodinggbk哪种不报错用哪种。这个问题的排查方向永远是先看编码再看路径。5.3 随机森林在恶意检测里的两个坑类别不平衡和特征冗余现象随机森林准确率很高但恶意网站检测的召回率却很低或者树数量从 50 加到 500性能毫无变化。原因恶意样本占比低时随机森林偏向多数类特征里有大量冗余列时每棵树分裂时反复选到的是同一个强特征多样性不足集成效果打折。解决训练时开class_weightbalanced让少数类样本获得更高权重用feature_importances_筛掉重要性基本为零的特征列重新提取后再训练。树数量超过 200 后收益递减优先调min_samples_leaf而不是继续堆树。5.4 DNN 训练不收敛或 loss 一直是 NaN现象DNN 训练时 loss 从第一轮就是 NaN或者准确率一直停留在 50% 左右不涨。原因最常见的是特征没有标准化就喂进网络数值范围过大的列导致梯度爆炸其次是学习率过大adam 默认学习率在特征尺度不一致时会震荡。解决确认 SVM 用过的StandardScaler在 DNN 训练前也 fit 了一遍不能用 SVM 的 scaler 偷懒复用是不行的其实可以复用但必须确保是同一次 fit_transform 的结果。训练前打印一下X_train.mean()和X_train.std()标准化后的均值应该在 0 附近、标准差在 1 附近。5.5 备份文件与 .zbak 文件别删那是后悔药现象压缩包里有SVM.py.zbak、forest_split.py.zbak和备份文件.zip看起来多余。原因.zbak是项目作者在改动代码前留下的备份防止改坏后回不去。这是很多工程项目的惯例不是垃圾文件。解决保留它们直到你确认自己的代码能稳定跑通。如果你自己改坏了某段代码拿.zbak对照着找差异比重新看文档快得多。全部跑通后再决定是否清理我的建议是不删——占不了多少空间关键时刻能救急。6. 把系统接出去从三个 py 文件到可复用的检测接口模型训练完只是第一步真正能在实际里用的是「拿新 URL 预测」的接口。我的习惯是写一个独立的predict.py把三种模型封装成统一的预测函数而不是每次都在训练脚本里复制粘贴。核心逻辑是先调 translate.py 里的extract_features提取特征再走与训练时完全相同的标准化流程最后分别预测并投票。import numpy as np import joblib from sklearn.preprocessing import StandardScaler from translate import extract_features # 复用特征提取逻辑 def predict_url(url, whois_days, alexa_rank): 单条 URL 的集成预测返回恶意概率和模型投票结果 # 1. 特征提取必须与训练时保持完全一致 x extract_features(url, whois_days, alexa_rank).reshape(1, -1) # 2. 复用训练时保存的 scaler对特征做同样的标准化 scaler joblib.load(models/scaler.joblib) x_scaled scaler.transform(x) # 3. 三模型分别预测概率 svm joblib.load(models/svm.joblib) rf joblib.load(models/rf.joblib) dnn joblib.load(models/dnn.joblib) # Keras 模型可用 load_model p_svm svm.predict_proba(x_scaled)[0][1] p_rf rf.predict_proba(x_scaled)[0][1] p_dnn dnn.predict(x_scaled)[0][0] avg_prob np.mean([p_svm, p_rf, p_dnn]) votes sum([p_svm 0.5, p_rf 0.5, p_dnn 0.5]) return { malicious_probability: round(float(avg_prob), 4), model_votes: int(votes), final_verdict: malicious if votes 2 else benign }调用方式很简单result predict_url(http://example.com, 30, None)返回的字典里能看到三个模型的平均恶意概率、几个模型投了恶意票以及最终判定结果。以大于等于 2 票作为恶意判定是集成模型的保守策略——最少有两个模型认为它是恶意才拦减少误伤。从这里还能引出两个实际化方向一是把predict_url包装成 FastAPI 接口接收网页传来的 URL 数据返回 JSON 判定结果二是对avg_prob做阈值调节默认 0.5 可能太紧或太松在实际流量里观察误报率再调。另外提一句如果你要做增量学习随机森林可以 partial_fit但它本身不支持实际上要重新训练或换增量版本更实际的做法是定期重跑训练脚本把新增标注并回 features.npy 和 labels.npy。我从第一次跑通这个项目到现在每次做特征改动都强制走一遍「改 extract_features → 重新生成 features.npy → 重训三个模型 → 重存 joblib 文件」的完整流程中间偷懒一步后面预测阶段就必定出幺蛾子。希望这份流程能帮你少走这些弯路。本文还有配套的精品资源点击获取