简介基于传统机器学习DNN算法的恶意网站检测项目面向学习机器学习与网络安全交叉应用的开发者提供可直接运行的Python源码和配套数据集。数据集中包含黑白名单样本项目流程覆盖样本整理、网站特征提取、特征可视化以及SVM、随机森林、DNN等多种分类器的训练与预测尤其以DNN分类算法为核心便于理解从数据预处理到模型评估的完整链路。资源共7个文件含5个Python功能脚本、1个zip数据集和1份Markdown说明文档压缩包整体仅3.31MB各脚本分别承担特征提取、可视化展示、模型训练等任务目录结构简单清晰适合课程设计、毕业设计或入门实践直接参考。已有373人学习下载适合希望快速上手恶意网站检测建模流程并动手复现的读者可作为从零搭建同类检测任务的起点。1. 为什么这份恶意网站检测源码值得跑从特征到模型一次完整落地提到恶意网站检测很多人第一反应是上深度学习、上大模型。但实际做安全分析的时候传统机器学习DNN算法在 Python 环境里依然是性价比很高的方案样本量不大、特征可控、训练快、结果可解释。这份源码包覆盖的正是完整链路——从 data.zip 里的黑名单和白名单数据出发经过 translate.py 做特征提取再分别用 SVM、随机森林和 DNN 三类分类器完成训练与预测。它适合两类人一类是做安全运营的工程师想快速搭一个 URL 分类基线另一类是学机器学习的开发者想找一个带真实数据、能跑通的 python 恶意网站检测练手项目。整份资源的核心价值不是某个算法多高级而是数据、特征、模型、评估这四个环节全都齐了。2. 特征提取与数据预览跑 translate.py 之前先搞清楚数据和特征长什么样恶意网站检测的难点不在算法而在特征。原始 URL 字符串不能直接丢给分类器必须先转成数值特征。整个源码包的数据流是data.zip 里的原始数据 → translate.py 提取特征 → typelist.py 可视化 → 2.xlsx 汇总特征 → 三个训练脚本读取并建模。如果你跳过了特征工程环节直接去看 DNN.py后面调参时大概率会无从下手。2.1 先解压 data.zip 看数据结构拿到源码包第一件事不是打开 IDE 就“下一步”而是解压 data.zip看看原始数据到底长什么样。我一般会解压后把数据文件重命名成好认的格式并且建议把目录固定在项目根目录下否则后续脚本的相对路径会出问题。在项目根目录下执行解压假设解压后的数据和脚本在同一目录unzip data.zip -d ./data ls -lh ./data这一步做完你会看到白名单和黑名单的原始数据文件。常见情况是 Excel 格式.xlsx可能是两个工作簿也可能是一个工作簿里的不同 Sheet。根据源码包的说明data.zip 中的 2.xlsx 是白名单和黑名单特征的整合可以理解为 2.xlsx 就是 translate.py 的输出结果也是后面三个模型脚本的统一入口。关于数据结构注意两点。第一黑名单样本占总样本的比例。通常恶意网址远少于正常网址这一点直接决定后续要不要做类别平衡处理。第二原始数据是否包含 URL 字符串之外的列比如 label 标签列。这两个信息决定了你在 translate.py 里怎么指定读取列名。提示写死相对路径是这类课程源码的通病。建议所有运行命令都在项目根目录执行不要分到子目录否则 FileNotFoundError 会很早找上门。2.2 translate.py 在做什么URL 字符串到特征向量的转换translate.py 的核心任务是把原始的 URL 字符串转成分类器能读的数值特征矩阵。URL 特征提取的常见做法是先拆 URL把协议、域名、路径、查询参数分开再对每个部分做统计比如域名长度、路径层级数、参数个数、特殊字符个数最后生成统计特征比如是否包含 IP、是否包含 、数字占比等。一个典型的 translate.py 的逻辑import pandas as pd from urllib.parse import urlparse def extract_features(url): parsed urlparse(url) domain parsed.netloc path parsed.path query parsed.query features { url_length: len(url), domain_length: len(domain), path_depth: len(path.split(/)) - 1, query_len: len(query), has_ip: 1 if domain.replace(., ).isdigit() else 0, has_at: 1 if in url else 0, special_chars: url.count(.) url.count(-) url.count(_), digit_ratio: sum(c.isdigit() for c in url) / max(len(url), 1), } return features df pd.read_excel(./data/raw_data.xlsx) # 文件名按实际解压结果为准 feature_df df[url].apply(lambda u: pd.Series(extract_features(u))) feature_df[label] df[label] feature_df.to_excel(./data/2.xlsx, indexFalse)逻辑说明urlparse 来自 Python 标准库 urllib.parse它把 URL 拆成 scheme、netloc、path、query 等部分。对于恶意 URL 检测这些字段是主要的信息来源因为大部分恶意特征是否伪装、是否纯 IP、路径深度异常大都体现在这些部分中。path_depth 计算路径层数。恶意网址经常用多层路径来隐藏真实目标比如一个钓鱼链接可以伪装成三层子目录。digit_ratio 是数字在 URL 中的占比。域名或路径中数字过多是可疑域名的一个典型信号。特征提取完成后输出到 2.xlsx后面三个训练脚本共用一个文件这样避免重复生成、重复读取。参数说明has_ip当域名是如 192.168.1.1 这样的纯数字字符串时置为 1。恶意站点直连 IP 的情况较多这个特征在分类器里通常权重较大。如果你想验证哪个特征贡献最大可以在随机森林的 feature_importances_ 里直接看。special_chars统计 .、-、_ 三个字符的总数。恶意域名喜欢用连字符拼凑长串字符这部分特征在钓鱼网站里往往偏高。digit_ratio分母用 max(len(url), 1) 避免除零。正常网站的 URL 数字占比通常不高但有些业务系统 URL 本身就带大量数字参数这个特征在具体场景下可能产生噪音。translate.py 生成的 2.xlsx 本质是特征矩阵。这一环节最值得花时间的地方是特征设计增加一条特征的成本很低但对分类结果的贡献可能很大。我一般会把 URL 长度、域名长度、路径深度、是否 IP、是否有 这几个维度先固定下来再根据实际数据分布逐步增加特征。如果后面发现 DNN 过拟合优先回到这一环节调整特征集而不是贸然加网络层数。2.3 typelist.py 的作用训练前把特征分布看清楚typelist.py 的作用是可视化展示特征数据简单说就是把 2.xlsx 中的特征列类型和分布打印出来。这个脚本本身不参与训练但很值得在训练前跑一遍原因有两个。第一查看 label 分布。如果黑名单与白名单的比例悬殊例如 1:10那么后面 SVM 和 DNN 的预测结果都会偏向多数类需要提前做类别平衡处理或选用加权损失。第二查看特征的量纲差异。URL 长度可能是几百has_ip 是 0 或 1digit_ratio 是 0 到 1 之间的小数。SVM 对特征量纲敏感如果直接输入不做归一化距离计算会被量纲大的特征主导。这个问题在 DNN 中相对弱一些因为网络自己能学权重但 SVM 中非常致命。一个典型的 typelist.pyimport pandas as pd df pd.read_excel(./data/2.xlsx) print(columns:, df.columns.tolist()) print(label distribution:) print(df[label].value_counts()) print(feature describe:) print(df.describe())从 typelist.py 的输出里你还能看到极端值特征比如有些 URL 长度超过 1000 的样本。这类样本往往非常少但对 SVM 的决策边界影响很大后面排错章节会展开讲。简单来说运行这三个步骤的顺序决定了你是不是在“盲盒”状态下跑模型先看数据再生成特征并验证分布最后训练模型。3. 三种分类器横向对比SVM、随机森林与 DNN 的关键参数和效果差异进入训练环节也就是源码包的核心部分SVM.py、forest_split.py、DNN.py。这三个脚本对应三种不同的算法思想放在一起跑本身就是一个很好的对照实验同一个特征矩阵在不同分类器上表现如何以及各自的适用边界在哪里。3.1 SVM.py对特征量纲最敏感的基线模型SVM 是一种经典的有监督分类模型核心思想是找一个超平面使不同类别之间的间隔最大化。它很适合恶意网站检测这个场景因为样本特征维度不高几十个数值特征但特征之间往往存在清晰的非线性边界。SVM.py 中典型的训练骨架import pandas as pd from sklearn.model_selection import train_test_split from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report df pd.read_excel(./data/2.xlsx) X df.drop(label, axis1) y df[label] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) clf SVC(kernelrbf, C1.0, gammascale, probabilityTrue, random_state42) clf.fit(X_train_scaled, y_train) y_pred clf.predict(X_test_scaled) print(classification_report(y_test, y_pred))逻辑说明train_test_split 中加 stratifyy是为了保证划分后的训练集和测试集中正负样本的比例与原数据一致。对于恶意网站检测这类类别不平衡数据这一点比乱序划分重要得多。StandardScaler 在训练集上用 fit_transform在测试集上只用 transform。很多入门代码写成全量数据 fit 后再切分这会让测试结果引入“未来信息”正确做法是先切分再单独计算均值和方差。SVC 的 kernelrbf 是默认核函数适合非线性可分的数据probabilityTrue 是为了后续能拿到 predict_proba 的概率输出方便调整判定阈值。SVM 的三个关键参数参数含义典型取值范围对结果的影响C错误惩罚系数0.1 ~ 10C 越大对误分类惩罚越重训练集拟合越充分但也越容易过拟合gammaRBF 核的宽度系数0.001 ~ 1 或 scalegamma 越大决策边界越复杂容易过拟合kernel核函数rbf / linear / poly数据线性可分时用 linear 更快非线性数据选 rbf 更稳对这份源码的场景我一般会把 C 设为 1.0、gamma 设为 scale 作为起点。如果分类结果出现大量假阳把正常网站判为恶意优先减小 C如果假阴多恶意网站漏掉优先增大 C。SVM 在类别不平衡时默认阈值 0.5 偏向多数类需要结合后面的阈值调整来处理。3.2 forest_split.py随机森林的解释性和抗过拟合能力随机森林是基于 Bagging 的决策树集成算法通过随机抽取样本和特征来训练多棵独立的决策树最终投票。恶意网站检测的特征往往是离散和偏态分布的比如 has_ip 这种二值特征、path_depth 这种小整数决策树在这些特征上的切分非常自然所以随机森林在不少同类数据上表现好于 SVM。forest_split.py 常见的长这样from sklearn.ensemble import RandomForestClassifier clf RandomForestClassifier( n_estimators200, max_depth10, min_samples_split4, min_samples_leaf2, class_weightbalanced, random_state42, n_jobs-1 ) clf.fit(X_train, y_train) print(clf.feature_importances_)逻辑说明n_estimators 是树的棵数一般 100 到 300 之间即可再往上提升有限反而增加训练时间。max_depth 限制单棵树深度用来抑制过拟合。恶意网站检测特征少深度 10 左右基本够用。class_weightbalanced 是处理类别不平衡最直接的方式让少数类的错误分类权重更高。feature_importances_ 是随机森林最值钱的输出直接告诉你哪些特征在区分恶意和正常网站时贡献最大。上一章特征工程里设计的特征有没有用在这里一眼见分晓。随机森林对原始特征没有缩放要求因为它不看距离只看分裂点所以这里的 X_train 可以直接用原始特征。如果你同时跑 SVM 和随机森林不要忘记把标准化后的版本留给 SVM而不是一个特征矩阵走到底。在实际的恶意网站检测里随机森林还有一个优势它对漏值容忍度高。如果某个样本的 URL 解析不出 host 字段可以用默认值代替不会像 SVM 那样因为距离计算直接产生 NaN。这也是为什么我平时做安全分类喜欢先用随机森林作为基线再与 SVM 和 DNN 的结果做对比。3.3 DNN.py用 Keras 搭一个可复用的多层感知机DNN 在源码包中是深度学习那一路的解法本质上是一个多层感知机MLP也就是全连接网络。与 CNN、LSTM 这些适合处理图像和序列的结构不同MLP 适合处理表格型特征正好对应这里的 URL 特征向量。DNN.py 的典型结构import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from keras.models import Sequential from keras.layers import Dense, Dropout from keras.optimizers import Adam df pd.read_excel(./data/2.xlsx) X df.drop(label, axis1).values.astype(np.float32) y df[label].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) model Sequential([ Dense(64, activationrelu, input_dimX_train.shape[1]), Dropout(0.3), Dense(32, activationrelu), Dropout(0.3), Dense(1, activationsigmoid) ]) model.compile(optimizerAdam(learning_rate0.001), lossbinary_crossentropy, metrics[accuracy]) history model.fit( X_train, y_train, epochs50, batch_size64, validation_split0.2, verbose1 ) loss, acc model.evaluate(X_test, y_test) print(ftest acc: {acc:.4f})逻辑说明第一层 Dense(64, activationrelu) 将特征从输入维度映射到 64 维隐藏空间。relu 是为了避免梯度消失也是表格型数据上最常用的激活函数。Dropout(0.3) 随机让 30% 的神经元失活这是 DNN 在样本量不大时防止过拟合的最有效手段。恶意网站检测数据集通常只有几千到几万条如果不加 Dropout训练集准确率很容易冲到 99%测试集却只有 85%。输出层是 1 个神经元加 sigmoid 激活把输出压缩到 0 到 1 之间作为恶意网站的概率。binary_crossentropy 对应二分类场景。epochs50、batch_size64 是基于常见规模数据集的起点。如果训练集很小epochs 减到 20 左右配合 early stopping。DNN 的参数调整习惯参数起点值调整方向神经元数量64→32过拟合就砍半欠拟合就加倍Dropout0.3训练集和验证集差距大时加大到 0.5epochs50看验证集 loss 是否还在降batch_size64数据量小时调到 16~32 更稳定learning_rate0.001训练震荡时降到 0.0001这里最容易忽略的一步是 StandardScaler因为 DNN 用梯度下降优化输入特征的量纲不同会导致 loss 曲面被拉长训练不稳定。即便 DNN 理论上有较强的尺度自适应能力实际体验里不标准化照样难收敛尤其是特征里既有几百的 URL 长度又有 0/1 的 has_ip 时这种情况特别明显。3.4 从评估指标看三种模型的取舍三个脚本跑完不能只看 print 出来的 accuracy。在恶意网站检测场景里accuracy 经常是骗人的如果黑名单只占 10%模型把全部分类为白名单就已经有 90% 的准确率但这显然不能用。正确做法是三份模型都打印分类报告重点看 recall查全率和 precision查准率。对于恶意网站检测recall 更重要一些漏判一个恶意网站的风险比误杀一个正常网站的代价更高。当然如果场景是拦截策略precision 太低也会导致大量正常用户被拦所以通常会在两者之间取一个平衡。三个模型的取舍结论如下SVM适合特征维度不高、样本量不大时做快速基线训练快但需要标准化且对类别不平衡敏感。随机森林与 SVM 相比不需要标准化对离散特征更友好class_weight 调整方便还自带特征重要性输出。在同等数据规模下很多时候是三者中综合表现最好的。DNN训练最慢参数最多但在特征设计合理、样本量充足时上限通常更高。适合你想把模型部署为在线服务时的最终选型。在实际项目里我通常先跑随机森林出一个基线再用 SVM 验证特征的线性边界最后才调 DNN。DNN 不是任何时候都比传统模型好在小样本场景下随机森林往往赢面更大。4. 避坑指南跑这份源码最容易翻车的五个位置这章多花点笔墨因为这些坑都是实际跑这份源码时几乎一定遇到的而且大部分是共性问题。4.1 读取 data.zip 时路径对不上导致 FileNotFoundError现象直接运行 translate.py报错 FileNotFoundError 或 IOError提示找不到 data 目录中的文件。原因源码写的读取路径是相对路径而当前终端的工作目录不在项目根目录。例如在 PyCharm 里设置了子目录为工作目录或者在命令中先行进入了 data 子目录数据文件便无法被找到。解决所有脚本统一在项目根目录运行并把路径统一到一个变量里。在 translate.py 头部这样处理import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_DIR os.path.join(BASE_DIR, data) df pd.read_excel(os.path.join(DATA_DIR, raw_data.xlsx))这样无论脚本在哪里被执行路径都与脚本本身绑定消除了运行位置的影响。4.2 read_excel 缺少 openpyxl 引擎现象pandas 读取 xlsx 时抛出 ImportError 或 ValueError提示缺少 openpyxl 引擎。原因pandas 读取 .xlsx 需要额外安装 openpyxl而读取 .xls 则需要 xlrd。很多课程源码的 requirements.txt 中没有列出这两个依赖导致新环境里第一次跑必然失败。解决安装依赖pip install openpyxl如果你拿到的数据文件有多个 Sheet建议把 Sheet 名称打印出来并明确指定df pd.read_excel(./data/2.xlsx, sheet_nameSheet1)这里 sheet_name 的具体名称以实际为准用 pd.ExcelFile 先查看 sheet 名能避免一上来就踩空格或中文名称的坑。4.3 全量特征标准化后再切分测试结果虚高现象用全量数据的均值和方差去标准化然后切分训练集和测试集训练出的模型在测试集上结果很好但后续部署到新数据上表现骤降。原因这是数据泄露的一种常见形式。标准化时全量数据参与计算相当于测试集的信息已经被“看见”测试集不再独立模型自然会在测试集上表现得更好。更隐蔽的情况是如果不先区分训练集和测试集直接对全量特征做缺失值填充、相关性筛选等也都存在同样的数据泄露风险。解决先切分再对训练集使用 fit_transform对测试集只使用 transform。上面 SVM 与 DNN 的代码中已给出的写法就是标准做法。X_train, X_test, y_train, y_test train_test_split(X, y, random_state42, stratifyy) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)fit_transform 会计算均值和方差并完成转换transform 只是套用已有的均值和方差。每次模型评估时这个顺序都要保持一致否则评估结果没有参考意义。4.4 类别不平衡导致 DNN 全预测为白名单现象DNN 训练完成后模型对测试集几乎全部预测为 0白名单准确率看起来还行但恶意网站全部漏网。原因黑名单占比太小DNN 学到的策略是把所有样本塞进多数类就能获得很低的 loss。网络本身没有“恶意网站很重要”的先验知识默认阈值 0.5 对不平衡数据并不友好。解决在 DNN 中给少数类加权重或者调整预测阈值。调整预测阈值的做法是proba model.predict(X_test) y_pred (proba 0.3).astype(int)阈值从 0.5 降到 0.3意味着模型更倾向于把不确定样本判为恶意。你可以在 0.1 到 0.5 之间搜索用测试集上的 recall 和 precision 曲线来找到合适阈值。另一个办法是给 loss 函数加权Keras 中可以在 fit 时传入 class_weight 参数model.fit(X_train, y_train, class_weight{0: 1.0, 1: 5.0}, epochs50, batch_size64)class_weight 的作用是给少数类的错误预测更大的惩罚。权重的具体取值需要根据实际的类别比例调整常见做法是设置成多数类样本数除以少数类样本数例如多数类有 4000 条、少数类有 1000 条权重约取 4 到 5。4.5 中文路径与编码问题导致读取失败现象在 Windows 环境下数据文件路径包含中文字符导致 pandas 读取报错 UnicodeDecodeError 或 OSError。原因pandas 读取 Excel 文件时不涉及文本编码但路径本身包含中文在某些系统区域设置下可能引发兼容问题而在输出 CSV 时出现编码问题的概率更大Windows 默认 GBK脚本默认 UTF-8 写入会导致 Excel 打开乱码。解决项目路径统一改成英文字母命名数据输出 CSV 时指定编码格式df.to_csv(./output.csv, indexFalse, encodingutf-8-sig)utf-8-sig 会在文件头部写入 BOM 标记Excel 打开时才能正确识别 UTF-8 编码。如果后续还有转码需求可以额外用 codecs 库统一文件的编码规则。5. 把模型用起来保存模型并批量预测新 URL 的几个验证技巧模型训练完成后最常用的下一步是保存模型文件然后在新的 URL 列表上做批量预测。这一步做得好前面的训练成果才能真正落地。5.1 同时保存模型与标准化器三种模型保存方式不同。sklearn 的 SVM 和随机森林统一用 joblib 保存DNN 用 model.save 保存成 H5 格式import joblib from keras.models import load_model joblib.dump(clf_rf, ./save/rf_model.pkl) joblib.dump(scaler, ./save/scaler.pkl) model.save(./save/dnn_model.h5) dnn_model load_model(./save/dnn_model.h5)注意保存模型时一定要把 scaler 一起保存因为预测时需要对新的输入特征做同样的标准化处理。这个细节很容易被遗漏。如果只保存模型不保存 scaler新数据进来后直接 predict输入特征的量纲完全不对结果基本不可用。5.2 在新 URL 列表上做批量预测假设你有一个新 URL 列表存放在 urls.txt每行一条预测流程是import joblib from translate import extract_features model joblib.load(./save/rf_model.pkl) scaler joblib.load(./save/scaler.pkl) with open(./urls.txt, r, encodingutf-8) as f: urls [line.strip() for line in f if line.strip()] X_new [extract_features(u) for u in urls] proba model.predict_proba(scaler.transform(X_new))[:, 1] for url, p in zip(urls, proba): print(f{p:.3f} {url})这里有一个习惯值得多说一句拿到预测结果后不要只看概率大于 0.5 的样本而是把概率值打出来排序从 0.3 到 0.7 之间逐条目测一遍。这个区间里的样本通常是模型最不确定的区域也往往是特征提取环节存在盲区的地方。比如某些在线文档站由于 URL 路径全部是随机长字符串很容易被误判为恶意手动检查后你会根据这些误判调整特征或阈值而不是盲目相信模型。5.3 验证预测结果分布的三个技巧第一个技巧是查看预测结果的类别比例。恶意网站通常只占全部流量的 5% 到 10%如果你的预测结果显示 60% 的 URL 都是恶意的那大概率是阈值或特征出了问题。概率分布的 describe 输出可以清晰反映这一情况。第二个技巧是把模型对已知正常网站的预测结果保存下来作为持续回归的对照样本。每次修改特征或重新训练后跑一遍这批样本对比预测结果的漂移及时发现问题。第三个技巧是交叉验证。除了源码给的 train_test_split 切分交叉验证更能反映模型的稳定性from sklearn.model_selection import cross_val_score scores cross_val_score(clf_rf, X, y, cv5, scoringrecall) print(scores.mean(), scores.std())cv5 意味着将数据分成 5 份轮流拿出其中一份做验证计算 5 次的平均指标和标准差。标准差大说明模型在不同数据划分上表现不稳定这时候优先检查特征是否存在大幅波动。这些都是我实际跑这类课程源码总结出来的习惯。曾经有一次我在部署这样训练得到的模型到线上环境时因为没有同步保存 scaler导致线上预测结果全乱追查了半天才意识到是量纲问题。从那以后我每次接手这类机器学习检测项目都会强制走一遍先看数据分布再确认特征提取脚本的输出接着跑基线模型然后保存模型加 scaler最后对预测结果做分布检查。这个固定流程帮我避掉了很多坑也希望能帮到你。本文还有配套的精品资源点击获取