简介:这是一套面向网络安全与机器学习教学场景的恶意网站检测系统实现,通过特征提取与多算法对比完成黑白名单网站识别,适合高校计算机、人工智能专业学生、网络安全研究者及入门开发者使用。打包为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, dtype=float) # 批量处理数据 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_size=0.2, random_state=42, stratify=y) # RBF 核的 SVM model = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True, random_state=42) 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,距离会被大数值特征完全主导,模型等于瞎了。kernel='rbf'是默认情况下最好用的核函数,它能映射到无限维空间,处理非线性边界;C=1.0是误分类惩罚系数,越大越不容忍错误、越容易过拟合,越小越平滑、越容易欠拟合;gamma='scale'让 sklearn 根据特征数量自动计算 gamma 初值,比写死一个数要稳。
训练集和测试集准确率如果出现"训练 99%、测试 82%"的差距,那就是过拟合了。下调 C 到 0.1 或者减小 gamma,是优先尝试的方向。
3.2 forest_split.py:随机森林与决策树的区别到底在哪
随机森林是 Bagging 思想和决策树结合的产物:训练多棵决策树,每棵树用不同的随机子样本和随机特征子集,最后投票决定样本类别。跟单棵决策树比,随机森林的最大区别是「多样性」——单棵树容易深度过深、把噪声都背下来,随机森林通过随机采样和特征随机选择,让每棵树各自犯错且错误不相关,投票时错误互相抵消。这也是为什么实践中随机森林很少需要精细调参,就能达到 95% 上下的准确率。
一个可用的训练片段:
from sklearn.ensemble import RandomForestClassifier # 随机森林不需要特征标准化,树模型对尺度不敏感 model = RandomForestClassifier( n_estimators=200, # 树的数量 max_depth=None, # 不限制深度,靠集成来控制方差 min_samples_split=5, # 内部节点再划分所需最小样本数 min_samples_leaf=2, # 叶子节点最少样本数 max_features='sqrt', # 每次分裂随机选 sqrt(n_features) 个特征 random_state=2024, 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(ascending=False))max_features='sqrt'是分类问题的标准取值——每棵树分裂时只看特征总数的平方根个候选特征,强制制造随机性。min_samples_leaf=2和min_samples_split=5一起限制树的复杂度,防止单棵树长到把每个样本都单独包起来。这两参数比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, activation='relu', input_shape=(X_train.shape[1],)), Dropout(0.3), Dense(32, activation='relu'), Dropout(0.3), Dense(1, activation='sigmoid') # 二分类输出 ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) # 训练早停:验证集 loss 连续 5 轮不降就停 early_stop = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True) history = model.fit( X_train, y_train, validation_split=0.2, epochs=100, batch_size=32, callbacks=[early_stop], verbose=1 )逻辑说明:第一层 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(data=df_viz, x='whois_days', y='url_len', hue='label', alpha=0.6, palette=['#4C72B0', '#C44E52']) plt.xscale('log') # whois_days 是长尾分布,取对数能看得更清楚 plt.xlabel('WHOIS 注册天数 (log)') plt.ylabel('URL 长度') plt.title('正常网站与恶意网站的特征分布对比') plt.savefig('feature_distribution.png', dpi=150)这段代码会生成一张散点图,横轴是域名注册天数(对数刻度),纵轴是 URL 长度,颜色区分标签。如果两类点明显聚集在不同的区域,这个特征组合就是可用的;如果你发现画出来的图两类点像撒了一把芝麻一样混在一起,就要重新回看 translate.py 的特征提取逻辑。
4.2 从可视化回推特征构造:一次典型的迭代过程
特征工程是迭代的。我的习惯是先跑出原始版本的可视化图,对着图挑几个「看起来能分开」的维度,再回到 translate.py 里加特征,重新提取、重新训练。这个项目的好处是特征对照表很清晰,加特征不需要改模型代码——模型读的是 features.npy,只要列数变了,模型结构会自动适配(DNN 除外,它要改 input_shape)。
实践中的常见扩展包括:TLS 证书剩余有效期、备案信息是否存在、页面内外部链接比例、表单提交协议。你在 2.xlsx 里追加列,在 translate.py 加提取逻辑,重新生成 features.npy 就能对比新旧特征集的效果。如果新特征让测试集准确率提升不到 0.5 个百分点,我建议丢掉它——多一个特征多一份维护成本,收益不匹配就不值得。
4.3 评价体系别只用准确率:精确率、召回率与 AUC
95% 准确率听起来很好,但在恶意网站检测这种正负样本不均衡的任务里,准确率有迷惑性——如果数据里 90% 是正常网站,模型把所有样本都判成正常就能拿到 90% 准确率。所以交叉验证时,每个模型都要额外输出精确率、召回率和 AUC。
如果随机森林的准确率是 96% 但恶意网站的召回率只有 80%,说明模型把不少恶意站点放过去了。优先调class_weight='balanced'让模型对少数类(恶意样本)给予更高误分类代价,或者用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 can't decode byte ...。
原因:项目代码是按 UTF-8 写的,Windows 中文环境默认用 GBK 读文件,遇到 UTF-8 编码的中文内容就崩了。
解决:所有open()和read_excel()调用明确指定编码:
df = pd.read_excel('2.xlsx') # pandas 读 Excel 一般没事 with open('typelist.py', 'r', encoding='utf-8') as f: # 读代码文件时指定 content = f.read()另外pd.read_csv()时加encoding='utf-8'或encoding='gbk',哪种不报错用哪种。这个问题的排查方向永远是先看编码,再看路径。
5.3 随机森林在恶意检测里的两个坑:类别不平衡和特征冗余
现象:随机森林准确率很高,但恶意网站检测的召回率却很低;或者树数量从 50 加到 500,性能毫无变化。
原因:恶意样本占比低时,随机森林偏向多数类;特征里有大量冗余列时,每棵树分裂时反复选到的是同一个强特征,多样性不足,集成效果打折。
解决:训练时开class_weight='balanced',让少数类样本获得更高权重;用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 文件」的完整流程,中间偷懒一步,后面预测阶段就必定出幺蛾子。希望这份流程能帮你少走这些弯路。
本文还有配套的精品资源,点击获取