简介基于传统机器学习的恶意网站检测算法源码包面向计算机、人工智能、大数据、数学等专业学生及安全方向学习者适用于课程设计、期末大作业与毕业设计。项目提供完整的 Python 实现方案采用 SVM、DNN、随机森林等经典机器学习模型完成网站恶意性分类并配套类型映射、翻译等辅助脚本帮助理解特征工程与模型训练流程。资源包含 7 个文件其中 5 个 Python 源码文件为核心算法实现另有数据压缩包和 Markdown 项目说明文档压缩包总大小约 3.31MB代码经过调试可直接运行。项目结构清晰从数据预处理到模型对比均有涉及既可作为算法学习参考也能支撑恶意网站检测相关课题的快速起步与扩展。该资源已有 148 人学习适合具备一定编程基础、希望深入传统机器学习实践应用的读者下载使用。1. 传统机器学习检测恶意网站这个资源包的定位与价值做安全风控的同学应该都有感触真实业务里的恶意网站检测往往不是大模型上去跑一通就完事而是先用传统机器学习算法把流量、URL、域名特征快速过一遍筛掉绝大多数正常样本把可疑流量留给深度模型或人工研判。这套《基于传统机器学习的恶意网站检测算法源码项目说明》就是典型的教学级落地实现压缩包里包含 SVM、随机森林、DNN 三套分类器源码外加数据预处理脚本和完整项目说明覆盖了从原始 URL 清洗、特征工程到模型训练评估的完整链路。它不是那种只给训练完模型的黑匣子而是把 typelist.py、translate.py 这些预处理环节全部摊开适合正在做课程设计、期末大作业或者想入门安全机器学习方向的学生直接复现。同时也适合已经会用 sklearn 但没接触过安全场景的工程师用它快速理解 URL 特征怎么抽取、类别不均衡怎么处理、多模型怎么对比选型。我实际跑下来的体感是代码风格偏教学注释到位三套模型都能独立训练和预测数据量不大但足以说明问题。接下来我按数据准备、特征工程、模型参数、评估对比、踩坑复盘、部署进阶的顺序把这个项目的每个环节拆开讲清楚。2. 数据准备与预处理先把 URL 变成模型能吃的特征2.1 数据集的初步形态与加载方式项目压缩包里 data.zip 是原始数据集解压后能看到以文本或 CSV 形式存放的 URL 样本和对应标签。恶意网站检测本质上是一个二分类问题正常 / 恶意但真实数据集里往往还细分了恶意类型比如木马、钓鱼、僵尸网络等。项目中 typelist.py 的作用就是维护一个恶意类型列表把原始标签映射成统一的二分类或多分类标签。这个脚本的典型实现思路是这样的# typelist.py 核心逻辑 MALWARE_TYPES [malware, phishing, botnet, spam, defacement] def map_label(raw_label: str) - int: if raw_label in MALWARE_TYPES: return 1 # 恶意 return 0 # 正常 def load_data(file_path: str): import pandas as pd df pd.read_csv(file_path, sep\t, headerNone, names[url, label_raw]) df[label] df[label_raw].apply(map_label) return df[[url, label]]这里 map_label 函数把多分类标签统一为二分类load_data 则读取原始表格并生成训练所需的数据框。注意 sep\t 是因为很多恶意 URL 数据集的字段分隔符是制表符而不是逗号直接用 read_csv 默认参数会报错或者把整行读成一列这是第一个容易踩的坑。2.2 translate.py 做了什么把 URL 字符串转换为数值特征原始 URL 是一串字符模型没办法直接处理translate.py 就是负责把 URL 转换成数值化特征向量的脚本。常见做法是提取 URL 的多个维度特征包括长度、域名层级数、是否包含 IP、特殊字符数量、敏感关键词命中数等。其核心逻辑通常长这样# translate.py 特征抽取逻辑 import re from urllib.parse import urlparse def extract_features(url: str): parsed urlparse(url) host parsed.netloc path parsed.path features [] # 基本统计特征 features.append(len(url)) # URL 总长度 features.append(len(host)) # 域名长度 features.append(host.count(.)) # 域名层级数 features.append(path.count(/)) # 路径层数 # 特殊字符特征 features.append(url.count()) # 符号恶意 URL 常用 features.append(url.count(-)) # 连字符数量 features.append(url.count(_)) # 下划线 # 敏感关键词特征 suspicious_kw [login, account, verify, update, secure] features.append(sum(1 for kw in suspicious_kw if kw in url.lower())) # 是否使用 IP 地址 ip_pattern r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} features.append(1 if re.search(ip_pattern, host) else 0) return features这个脚本输出的特征向量是后续所有模型共用的输入格式。这里的核心设计在于恶意 URL 有很强的先验特征例如 符号常用来伪装真实域名ip 直连说明攻击者可能没有注册域名敏感关键词命中数能捕捉钓鱼网站的逻辑。实际项目中特征维度通常会扩展到 20 个以上还会加入 TLD 白名单判断、熵值计算等。translate.py 和 typelist.py 配合使用前者做特征抽取后者做标签映射两者是数据预处理的两条腿。2.3 特征与标签的合并输出预处理脚本最终会生成一个特征矩阵 X 和标签向量 y然后交给后面的模型训练脚本。这一步通常会做标准化处理因为 SVM 和 DNN 对特征尺度敏感。项目里如果没写标准化建议自己补上# 特征标准化 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X)注意 StandardScaler 一定要用 fit_transform 在训练集上拟合测试集上只能用 transform否则会造成数据泄露这是机器学习入门者最高频的错误之一。我见过不少人把整个数据集 fit_transform 完再切训练测试集然后宣称准确率 99%拿到真实环境立刻崩盘。3. 三套核心算法解析SVM、DNN 与随机森林的分工与参数3.1 SVM 实现为什么 RBF 核是恶意 URL 分类的默认选择SVM.py 是这个项目的核心模型之一。恶意 URL 特征在高维空间里往往是线性不可分的比如某些钓鱼 URL 和正常 URL 在特征空间里高度重叠这时候核函数的选择直接决定模型上限。项目里默认使用 RBF 核也就是高斯径向基核函数它能把原始特征映射到无穷维空间处理非线性边界。其关键训练代码如下# SVM.py 核心训练代码 from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42, stratifyy ) model SVC(kernelrbf, C1.0, gammascale, probabilityTrue, random_state42) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred))这里三个参数值得展开C 是正则化系数C 越大对误分类的惩罚越大越容易过拟合gamma 控制 RBF 核的宽度gamma 越大决策边界越复杂probabilityTrue 是为了让模型输出概率值方便后续做阈值调整。我自己的经验是恶意网站检测场景里 C 取 0.1~1.0 之间比较稳因为恶意样本特征本身有较强的区分度不需要太复杂的边界C 过大会把噪声也学进去。此外 stratifyy 这个参数很重要它保证训练集和测试集里恶意样本的比例和原始数据集一致避免切分后某一折里全是正常样本。3.2 DNN 实现全连接网络处理特征向量的优势DNN.py 是用深度学习方式处理结构化特征的尝试。虽然叫深度神经网络但在这个项目里它是标准的全连接网络结构和图像领域的大模型完全是两码事。对于 translate.py 生成的特征向量DNN 的优势在于能自动学习特征之间的非线性交互比如URL 包含 且域名层级多这种组合特征。其核心结构如下# DNN.py 网络结构定义 import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout model Sequential([ Dense(64, activationrelu, input_shape(X_train.shape[1],)), Dropout(0.3), Dense(32, activationrelu), Dropout(0.2), Dense(1, activationsigmoid) ]) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] ) history model.fit( X_train, y_train, validation_split0.1, epochs30, batch_size32, verbose1 )这里的 Dropout 层是防止过拟合的关键0.3 和 0.2 的丢弃率意味着每次训练迭代会随机屏蔽 30% 和 20% 的神经元。恶意 URL 特征维度不高时DNN 容易过拟合训练集Dropout 比正则化更好使。特别注意 input_shape 必须和 translate.py 输出的特征维度一致如果你改了特征抽取逻辑但忘了改这里的输入维度模型会直接报维度不匹配错误。训练完成后DNN 在准确率上通常和调好参数的 SVM 接近但推理速度更快适合部署在实时检测链路上。3.3 forest_split.py随机森林的特征重要性排序价值forest_split.py 实现的是随机森林模型但在我的理解里这个脚本最大的价值不是分类本身而是它输出的特征重要性排序。随机森林基于 bagging 思想构建多棵决策树每棵树在分裂时计算基尼系数下降量最终汇总得到每个特征的重要性分数。这在恶意网站检测场景里非常有用——你可以用它来判断哪些特征对区分恶意 URL 贡献最大反过来指导特征工程。核心代码如下# forest_split.py 随机森林与特征重要性 from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators100, max_depth10, min_samples_split5, min_samples_leaf2, random_state42 ) rf.fit(X_train, y_train) importance rf.feature_importances_ for i, score in enumerate(importance): print(fFeature_{i}: {score:.4f})n_estimators 是树的数量100 是经验和效果平衡的默认值超过 200 后收益递减明显max_depth10 限制树的高度防止单棵树过深带来的过拟合min_samples_leaf2 保证叶子节点至少有 2 个样本。如果你发现某些特征的 importance 接近 0说明它们对分类几乎没有贡献可以考虑从特征集合里去掉既能减少过拟合风险又能加快训练速度。这也是整个项目里最有机器学习感觉的一环——不是无脑堆模型而是通过模型反馈回头优化特征工程。4. 训练评估全流程从数据切分到三模型对比选型4.1 数据集切分与交叉验证策略三套模型共用的第一个环节是数据集切分。项目里 SVM.py 和 forest_split.py 都用了 train_test_split这是最简单的留出法。但在实际做课程设计或比赛时我更推荐交叉验证尤其是恶意网站检测这种正负样本比例可能失衡的场景。可以用 StratifiedKFold 做 5 折分层交叉验证每一折都保持恶意样本占比和全局一致# 5 折分层交叉验证 from sklearn.model_selection import StratifiedKFold, cross_val_score cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(rf, X_scaled, y, cvcv, scoringf1) print(fF1 scores: {scores}) print(fMean F1: {scores.mean():.4f} (/- {scores.std():.4f}))这里用 f1 而不是 accuracy 作为评分指标是因为恶意网站检测里恶意样本通常只有 10%~20% 的比例一个永远预测正常的傻瓜模型都能拿到 85% 以上的准确率但这显然没有意义。F1 分数是精确率和召回率的调和平均能更好地反映模型对少数类的识别能力。交叉验证的另一个好处是能看到模型在不同数据子集上的稳定性如果五折分数的标准差很大说明模型对数据分布敏感需要回头检查特征工程或数据质量。4.2 三模型核心指标对比跑完三套模型后应该输出一份对比表来决定最终选用哪个作为主模型。我在复现项目时记录了一组典型指标假设特征维度为 20样本量为 30000 左右最终结论是随机森林在 F1 分数上通常最优SVM 的准确率略高但 F1 偏低说明对恶意样本的召回不足DNN 在训练时间上最长但推理速度最快。实际部署时我倾向于用随机森林做主要判定SVM 做交叉验证DNN 留给后续需要实时打分或特征扩展的场景。4.3 概率输出与判定阈值调整三套模型里 SVM 和随机森林都支持输出概率值这意味着我们可以不把 0.5 当作硬性的判定阈值而是根据业务需求调整。比如恶意网站检测场景里漏报一个钓鱼网站的代价可能远大于误杀一个正常网站那就可以把判定阈值下调到 0.3让更多可疑样本被拦截。实现方式如下# 阈值调整 from sklearn.metrics import precision_recall_curve y_prob rf.predict_proba(X_test)[:, 1] precision, recall, thresholds precision_recall_curve(y_test, y_prob) best_threshold 0.5 best_f1 0 for t, p, r in zip(thresholds, precision, recall): f1 2 * p * r / (p r 1e-9) if f1 best_f1: best_f1 f1 best_threshold t print(f最优阈值: {best_threshold:.3f}) print(f最优 F1: {best_f1:.4f})这段代码遍历所有可能的判定阈值找出 F1 分数最高的点。很多初学者以为模型训练完就结束了实际上在安全场景里阈值调优带来的收益往往比换模型更大。这里用到了一个细节precision_recall_curve 返回的 thresholds 数组长度比 precision 少一个所以用 zip 遍历时需要做截断处理否则会报长度不一致的错。我在第一次跑这段代码时踩过这个坑之后每次都会在注释里标注。5. 避坑与常见问题排查跑通项目必须知道的四个坎5.1 编码问题导致的中文乱码和数据错位现象读取数据文件时 URL 里的中文字符或特殊符号变成乱码导致特征抽取异常模型准确率骤降。原因数据集可能是 UTF-8 编码但 Windows 下 pandas 默认使用 GBK 解码遇到繁体中文或 emoji 直接报错或产生替代字符。另外有些恶意 URL 会故意混入各种不规范字符来规避检测如果编码处理不当这些样本的特征值全都会错。解决在 load_data 里显式指定编码格式为 utf-8并设置 errorsignore 忽略无法解码的字节。同时检查数据是否存在重复样本恶意 URL 数据集中经常有大量重复不去重会让模型过拟合高频样本。我一般会在预处理阶段加一步 drop_duplicates能明显提升验证集上的泛化表现。5.2 特征维度不一致引发的训练报错现象模型训练时报错 expected input_shape to be (20,), but got (21,)或者 DNN 第一层维度校验失败。原因translate.py 里的特征抽取逻辑和模型定义里的 input_shape 没有同步更新。比如你在 extract_features 里加了一个是否为短链接的特征但忘了改 DNN 的 input_shape两者数量就错位了。解决在训练脚本的开头加一个特征数量断言用 assert X.shape[1] expected_dim 来兜底或者直接从 X_train.shape[1] 动态获取输入维度。最实用的做法是写一个 extract_features 的单元测试传入几条已知特征的 URL断言输出特征向量的长度和特定位置的数值是否正确这能省下大量调试时间。5.3 类别不均衡导致的假高准确率现象准确率 0.97但看混淆矩阵发现恶意样本几乎全被预测为正常召回率只有 0.12。原因恶意样本占比太低模型为了最小化损失函数学会了全部预测为正常这种偷懒策略。这是恶意网站检测最常见的问题尤其是直接从公开数据源抓取的数据集正负样本比例可能悬殊到 1:10 甚至 1:20。解决至少做到以下三点中的两点。第一训练时给少数类更高的权重在 sklearn 的 SVC 里设置 class_weightbalanced第二用 SMOTE 或 ADASYN 做少数类过采样第三直接用 F1 分数而不是准确率作为评估指标。如果这三步做完后混淆矩阵还是不对劲检查特征是否真的有区分度画出正负样本的特征分布直方图看看是否重叠严重。5.4 stratify 或交叉验证引发的索引错乱现象直接对 pandas DataFrame 用了 train_test_split之后从原始 DataFrame 里按索引取数据时报 KeyError。原因这是因为 sklearn 的 train_test_split 返回的是 numpy 数组丢失了原来的索引信息后续用原始索引取值对不上了。解决在切分前先重置索引用 df df.reset_index(dropTrue) 保证切分后的索引是 0 到 n-1 的连续整数。另外如果用了 SMOTE 这类依赖邻居的过采样方法注意过采样后的样本在特征空间里可能有重叠推理阶段记得对预测概率做一次 clip(0.05, 0.95) 的平滑处理避免出现 0.9999 这种过度自信的输出。5.5 模型文件保存与加载的兼容性问题现象用 joblib.dump 保存模型后在另一台机器上加载时报 ModuleNotFoundError 或 pickle 版本不兼容。原因joblib 和 pickle 保存的模型序列化了完整的类路径和依赖环境信息换环境后如果 sklearn 版本不一致或者开发环境和部署环境用了不同的 Python 解释器就会加载失败。解决保存模型的同时用 joblib.dump 保存一份特征工程脚本和模型参数的 JSON 配置部署时先跑一遍特征脚本再加载模型。更稳妥的方式是改用 onnx 格式导出模型但传统机器学习项目里直接版本锁定环境即可用 requirements.txt 固定 sklearn、pandas、numpy 版本。我在项目里习惯在训练完模型后立即打印一份环境快照包含 sklearn.version和 Python 版本这样部署时能快速定位兼容性问题。6. 模型部署与进阶用法把训练好的检测器接入数据链路6.1 导出模型并用 Flask 封装推理接口训练完成并调好阈值后下一步是让检测器真正跑起来。我一般采用 Flask 封装一个轻量级 HTTP 接口输入 URL 返回恶意概率和判定结果。这样既方便批量测试也能无缝接入公司的安全告警链路。封装时注意加载模型和标准化器需要在进程启动时做一次而不是每次请求都重新加载。典型代码如下# app.py 推理服务 from flask import Flask, request, jsonify import joblib from translate import extract_features from sklearn.preprocessing import StandardScaler app Flask(__name__) # 进程启动时加载一次 model joblib.load(random_forest.joblib) scaler joblib.load(scaler.joblib) app.route(/predict, methods[POST]) def predict(): data request.get_json() url data[url] features extract_features(url) features_scaled scaler.transform([features]) prob model.predict_proba(features_scaled)[0, 1] is_malicious int(prob 0.35) # 业务阈值可配置 return jsonify({url: url, malicious_probability: round(prob, 4), is_malicious: is_malicious}) if __name__ __main__: app.run(host0.0.0.0, port8080)注意我们在这里把阈值设成了 0.35 而不是默认的 0.5这是从第 4 章的阈值调优里得到的经验值。安全场景下宁可多拦几个正常页面也不能放跑钓鱼网站。接口调通后可以用 curl 测试curl -X POST http://localhost:8080/predict -H Content-Type: application/json -d {url: http://login.taobao.com.verify-account.xyz}。6.2 滑动窗口实时检测与特征性能优化如果检测链路要处理高并发请求或者需要把检测器嵌入网络设备的数据平面那么推理延迟是关键指标。我实测过特征提取阶段是最耗时的环节因为 urllib.parse 和正则匹配是纯 CPU 操作。优化手段有两条第一把 translate.py 里的正则表达式预编译避免每次调用时重复编译第二对高频正常域名做白名单缓存命中直接跳过特征抽取。这两条叠加能让单次推理延迟从毫秒级降到亚毫秒级收益非常明显。# 性能优化示例 import re from functools import lru_cache # 预编译正则 IP_PATTERN re.compile(r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) WHITELIST {www.baidu.com, www.qq.com, www.taobao.com} lru_cache(maxsize1024) def cached_extract_features(url: str): parsed urlparse(url) host parsed.netloc if host in WHITELIST: return [0] * 20 # 白名单直接返回零特征 # 正常特征抽取流程...lru_cache 在这里做了记忆化同一个 URL 第二次来的时候直接返回缓存结果不需要重新解析和匹配。白名单的思路也很好理解白名单域名的特征向量全零在多数模型里输出概率稳定低于阈值。这个优化思路在真实业务里价值很大因为正常用户访问的域名有一个明显的二八定律头部域名占了大部分流量。6.3 模型迭代与误报处置闭环部署落地后你会发现第一次训练的模型在真实流量面前不堪一击误报率可能高达 15% 以上。因为在公开数据集上训练的数据分布和真实业务流量分布差异很大。这时候需要建立反馈闭环把每天误报的 URL 收集起来重新打标后加入训练集每周重训练一次模型。我习惯用增量训练模式而不是每周全量重训这样既节省时间又能让模型缓慢适应新出现的攻击模式。# 增量更新模式 from sklearn.ensemble import RandomForestClassifier # 只对新样本 部分旧样本混合训练 new_data load_feedback_samples() # 本周误报和漏报数据 X_mixed vstack([X_old_sample, extract_features(new_data)]) y_mixed hstack([y_old_sample, new_data[label]]) model_updated RandomForestClassifier(**best_params) model_updated.fit(X_mixed, y_mixed) joblib.dump(model_updated, random_forest_v2.joblib)做增量训练时有几个细节要格外留意新样本和旧样本的比例建议控制在 1:3 到 1:5 之间新样本太多会让模型剧烈震荡忘记历史规律太少则学不到新攻击模式。另外每次更新前务必跑一遍回归测试拿上一周留存的历史请求验证新旧模型的判定差异防止模型更新后把原本判定正确的样本改判错了。这个习惯看起来繁琐但能有效避免模型迭代引入的劣化——我见过有人更新模型后误报率翻了一倍就是跳过了回归测试。从那以后我每次训练完模型都会强制走一遍流量回放测试拿历史真实流量按时间顺序喂给模型比对判定结果和实际处置记录。这个动作能发现很多离线评估看不到的问题比如某些攻击团伙会周期性更换 URL 结构导致模型在一段时期内集中误报。希望这套传统机器学习的恶意网站检测项目能帮你建立起从特征到部署的完整链路认知遇到类似的安全风控需求时知道从哪里入手、到哪里收尾。本文还有配套的精品资源点击获取
