恶意网站检测系统实战:SVM、随机森林与DNN三模型融合
简介这份资源面向计算机、人工智能与电子信息工程等专业的学生及机器学习初学者提供一套完整的恶意网站检测实践方案可用于网络安全、信息过滤等场景的教学与实验。压缩包共11个文件约3.32MB以Python脚本为主体辅以数据压缩包、备份文件与说明文档覆盖特征提取、可视化与多算法建模的完整流程。数据经translate.py完成特征向量提取typlot.py生成特征分布图表标注依据整合于对照表中。算法层面同时实现支持向量机、随机森林与深度神经网络三类方法分别从超平面分类、多决策树集成与多层感知机非线性学习角度展开验证交叉验证准确率均超过95%。模块结构清晰便于读者理解特征工程与模型调优思路也支持在此基础上扩展特征或引入增量学习机制。目前已有50人学习适合作为课程实践与算法对比研究的参考需遵守学术规范禁止商业用途。1. 恶意网站检测系统从特征工程到三模型融合的落地路线恶意网站检测系统要解决的核心问题很具体给定一个 URL 或页面内容判断它是不是钓鱼、挂马、诈骗站点。传统黑名单方案只能拦住已知域名攻击者换个域名就绕过去了所以现在主流做法是走机器学习路线——用 SVM、随机森林和 DNN 三个模型分别建模再做融合决策。这套方案适合有基础 Python 和 sklearn 经验的安全工程师、后端开发也适合想拿一个完整项目练手的学生。它不需要 GPU 集群一台 16GB 内存的机器就能跑通全流程。下面从数据构造、特征工程、三个模型的训练细节、融合策略到线上部署把每个环节的参数和踩坑点讲清楚。2. 数据从哪来、特征怎么造恶意网站检测的地基2.1 公开数据集的选择与标签清洗恶意网站检测的数据集不像 MNIST 那样现成好用。常见做法是组合几个来源PhishTank 提供钓鱼 URL 黑名单Alexa Top 1M 提供正常站点样本再配合一些开源安全社区整理的恶意 URL 数据集。我一般会按 1:1 到 1:1.5 的比例采样正负样本避免类别极度不平衡导致模型全预测成多数类。拿到原始数据后第一件事是去重和标签校验。PhishTank 里有些 URL 已经失效Alexa 里也可能混入被挂马的站点。清洗逻辑如下import pandas as pd from urllib.parse import urlparse def clean_url_dataset(df, url_colurl, label_collabel): # 去掉空值和重复 df df.dropna(subset[url_col]) df df.drop_duplicates(subset[url_col]) # 只保留 http/https 且域名合法的记录 def is_valid(url): try: p urlparse(url) return p.scheme in (http, https) and . in p.netloc except: return False df df[df[url_col].apply(is_valid)] # 标签统一成 0/1 df[label_col] df[label_col].astype(int) return df.reset_index(dropTrue)这段代码做了三件事去重、URL 合法性校验、标签类型统一。参数上注意url_col和label_col要跟你实际数据的列名对齐。清洗完建议打印一下正负样本比例如果偏差超过 1:3就要考虑过采样或调整损失函数权重。2.2 URL 特征与页面特征的提取方法特征工程是这套系统的胜负手。SVM 和随机森林对特征质量极其敏感DNN 虽然能自动学一些表征但输入特征太烂也救不回来。我一般从三个维度构造特征URL 文本特征URL 长度、域名长度、路径深度、是否含 IP 地址、特殊字符、-、//数量、数字占比、是否使用短链接服务。这些特征提取快对钓鱼检测特别有效。域名特征域名年龄通过 WHOIS 查但线上推理时缓存、是否含可疑关键词login、verify、account、secure 等、子域名层数、TLD 类型.com、.tk、.xyz 风险不同。页面内容特征如果能抓到 HTML外部链接与内部链接比例、表单数量、是否含 iframe、JS 混淆程度、页面标题与域名是否匹配。import re from urllib.parse import urlparse def extract_url_features(url): p urlparse(url) domain p.netloc path p.path feats {} feats[url_len] len(url) feats[domain_len] len(domain) feats[path_depth] path.count(/) feats[has_ip] 1 if re.match(r\d\.\d\.\d\.\d, domain) else 0 feats[special_char_cnt] len(re.findall(r[\-//], url)) feats[digit_ratio] sum(c.isdigit() for c in url) / max(len(url), 1) feats[subdomain_cnt] domain.count(.) - 1 suspicious_kw [login, verify, account, secure, update] feats[suspicious_kw] sum(1 for kw in suspicious_kw if kw in url.lower()) return feats这段函数返回一个字典每个 key 是一个特征。实际项目中我会把它包成一个FeatureExtractor类方便批量处理和后续扩展。注意digit_ratio用max(len(url), 1)防止除零。特征提取完后用pd.DataFrame拼成矩阵列名就是特征名方便后面看特征重要性。提示域名年龄特征在训练集里好拿但线上推理时 WHOIS 查询有延迟建议做成异步缓存不要阻塞主流程。3. 三个模型怎么训SVM、随机森林、DNN 的参数与边界3.1 SVM 在小样本高维特征下的调参要点SVM 在恶意网站检测里适合特征维度高、样本量中等几万到几十万的场景。它的优势是决策边界清晰对噪声有一定容忍度劣势是样本量超过百万后训练时间急剧上升核函数选择也影响很大。我一般用 RBF 核因为 URL 特征之间往往不是线性可分的。关键参数两个C和gamma。C越大对误分类惩罚越重容易过拟合gamma越大决策边界越弯曲也容易过拟合。用网格搜索找from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (svm, SVC(kernelrbf, probabilityTrue)) ]) param_grid { svm__C: [0.1, 1, 10], svm__gamma: [scale, 0.01, 0.001] } grid GridSearchCV(pipe, param_grid, cv5, scoringf1, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_)注意 SVM 对特征尺度敏感必须加StandardScaler。probabilityTrue会启用概率输出但会拖慢训练速度如果只做硬分类可以关掉。scoringf1是因为恶意网站检测里漏报和误报代价不同F1 比准确率更能反映实际效果。3.2 随机森林的树数量、深度与训练时间预估随机森林是这套系统里最稳的模型对特征尺度不敏感能输出特征重要性训练速度也可控。热搜里有人问“随机森林需要跑多长时间”这取决于树的数量、最大深度和样本量。我的经验是10 万样本、50 棵树、深度不限单机 8 核大约 2 到 5 分钟如果树加到 200 棵时间线性增长到 10 到 20 分钟。关键参数n_estimators树的数量一般 50 到 200 够用再多边际收益递减。max_depth不设限让树自由生长但容易过拟合设 15 到 25 之间比较平衡。min_samples_leaf叶子最小样本数设 1 到 5越大越保守。class_weight样本不平衡时设balanced。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators100, max_depth20, min_samples_leaf2, class_weightbalanced, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) # 看特征重要性 import pandas as pd feat_imp pd.Series(rf.feature_importances_, indexfeature_names) print(feat_imp.sort_values(ascendingFalse).head(10))n_jobs-1用满所有 CPU 核。random_state固定后结果可复现。训练完一定要看特征重要性如果url_len或suspicious_kw排在前列说明特征构造方向对了如果某个特征重要性接近零考虑删掉减少维度。3.3 DNN 的网络结构设计与过拟合抑制DNN 适合样本量更大百万级以上、特征更复杂的场景。它的优势是能自动学特征交叉劣势是需要调的东西多容易过拟合。我的经验结构是输入层 → 全连接 256 → ReLU → Dropout 0.3 → 全连接 128 → ReLU → Dropout 0.3 → 全连接 64 → ReLU → 输出层 1 个神经元 Sigmoid。import tensorflow as tf from tensorflow.keras import layers, models def build_dnn(input_dim): model models.Sequential([ layers.Input(shape(input_dim,)), layers.Dense(256, activationrelu), layers.Dropout(0.3), layers.Dense(128, activationrelu), layers.Dropout(0.3), layers.Dense(64, activationrelu), layers.Dense(1, activationsigmoid) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), lossbinary_crossentropy, metrics[accuracy, tf.keras.metrics.AUC()] ) return model model build_dnn(X_train.shape[1]) history model.fit( X_train, y_train, validation_split0.2, epochs30, batch_size256, class_weight{0: 1, 1: 2}, # 恶意样本权重更高 callbacks[tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue)] )Dropout 设 0.3 是经验值太高欠拟合太低过拟合。EarlyStopping耐心值 5 表示验证集损失连续 5 轮不降就停restore_best_weights保证拿回最优权重。class_weight里给恶意样本更高权重因为漏报一个恶意网站比误报一个正常网站代价大。注意DNN 对特征尺度也敏感输入前要做标准化。如果特征里有类别型变量先做 one-hot 或 embedding。4. 三模型融合与线上部署投票、加权与推理加速4.1 硬投票、软投票与加权融合的取舍三个模型各自训练完后融合策略决定最终效果。常见做法有三种硬投票每个模型输出 0 或 1多数决定最终结果。简单但丢失了概率信息。软投票每个模型输出概率取平均后卡阈值。比硬投票稳但要求每个模型都能输出概率SVM 需要probabilityTrue。加权融合根据每个模型在验证集上的 F1 或 AUC 分配权重。我一般给随机森林 0.4、DNN 0.35、SVM 0.25因为随机森林最稳DNN 次之SVM 在小样本下波动大。import numpy as np def ensemble_predict(svm_prob, rf_prob, dnn_prob, weights(0.25, 0.4, 0.35), threshold0.5): # 加权平均 final_prob (weights[0] * svm_prob weights[1] * rf_prob weights[2] * dnn_prob) return (final_prob threshold).astype(int), final_prob权重不是拍脑袋定的要在验证集上网格搜索。阈值 0.5 也可以调如果业务上更怕漏报降到 0.4更怕误报升到 0.6。4.2 模型序列化与 API 推理服务搭建训练完的模型要落盘。SVM 和随机森林用joblibDNN 用model.saveimport joblib joblib.dump(grid.best_estimator_, svm_model.pkl) joblib.dump(rf, rf_model.pkl) model.save(dnn_model.h5)线上服务用 FastAPI 包一层接收 URL 字符串提取特征三个模型分别推理融合后返回结果from fastapi import FastAPI import joblib import numpy as np from tensorflow.keras.models import load_model app FastAPI() svm joblib.load(svm_model.pkl) rf joblib.load(rf_model.pkl) dnn load_model(dnn_model.h5) app.post(/predict) def predict(url: str): feats extract_url_features(url) x np.array([list(feats.values())]) svm_p svm.predict_proba(x)[0][1] rf_p rf.predict_proba(x)[0][1] dnn_p dnn.predict(x)[0][0] label, prob ensemble_predict(svm_p, rf_p, dnn_p) return {label: int(label), probability: float(prob)}注意特征顺序必须和训练时一致建议把特征名列表也存下来推理时按名取值。DNN 的predict返回的是 numpy 数组取[0][0]拿标量。提示线上推理延迟主要花在特征提取尤其是 WHOIS 查询上模型本身推理都在毫秒级。建议把域名年龄等慢特征做成缓存。5. 避坑与排查恶意网站检测系统最常见的 5 个翻车点现象一验证集 AUC 0.95线上一跑全是误报。原因通常是训练集和线上数据的分布不一致——训练集里正常样本多来自 Alexa 头部站点线上却遇到大量长尾正常站点。解决办法是定期用线上流量做无监督漂移检测发现分布偏移后重新采样训练。现象二SVM 训练到一半内存爆了。RBF 核 SVM 的时间复杂度是 O(n²) 到 O(n³)样本超过 50 万就非常吃力。解决办法是先用随机森林做特征选择把维度从几百降到几十再喂给 SVM或者改用LinearSVC做线性近似。现象三随机森林特征重要性里url_len排第一但业务上觉得不合理。这是因为训练集里恶意 URL 普遍偏长模型学到了虚假相关。解决办法是检查数据构造过程确保正负样本在 URL 长度上分布接近或者做分层采样。现象四DNN 训练 loss 震荡不收敛。多半是学习率太大或 batch size 太小。把学习率从 1e-3 降到 1e-4batch size 从 64 升到 256通常能稳下来。另外检查输入特征有没有做标准化。现象五融合后效果还不如单模型。常见原因是某个模型严重过拟合它的概率输出全是 0.99 或 0.01把加权平均带偏了。解决办法是先看每个模型在验证集上的校准曲线对过拟合的模型做概率校准Platt Scaling 或 Isotonic Regression再融合。6. 进阶技巧用 SHAP 做特征归因与模型可信度验证模型上线后安全运营团队会问“为什么这个 URL 被判成恶意”。这时候光给一个概率不够得给出解释。SHAP 是目前最实用的特征归因工具对随机森林和 DNN 都支持。import shap # 随机森林的 SHAP 解释 explainer shap.TreeExplainer(rf) shap_values explainer.shap_values(X_sample) shap.summary_plot(shap_values[1], X_sample, feature_namesfeature_names)shap_values[1]取的是恶意类的贡献值。summary_plot会画出每个特征对预测的推动方向红色向右表示推高恶意概率蓝色向左表示拉低。如果发现suspicious_kw和has_ip是主要推动力说明模型学到了合理模式如果url_len一枝独秀就要警惕虚假相关。对 DNN 用shap.DeepExplainer但要注意它比树模型慢很多建议只对少量样本做解释。实际部署时我会把 SHAP 值缓存下来运营人员点击某个告警时直接展示 top 5 贡献特征。验证模型可信度还有一个笨办法但很有效手动构造一批边界样本。比如把正常 URL 里的域名换成 IP、把路径里插入login关键词、把短链接展开后再测。如果模型对这些扰动敏感且方向正确说明它学的是真信号如果纹丝不动说明特征工程有漏洞。我自己踩过最深的坑是早期版本里用了 URL 的字符级 n-gram 做特征结果模型把训练集里某个特定域名片段当成了强特征换一批数据后 F1 从 0.92 掉到 0.61。后来改成手工特征加随机森林特征选择泛化才稳下来。做安全检测特征的可解释性和泛化性比刷高验证集指标重要得多。希望帮到你。本文还有配套的精品资源点击获取