☰
传统机器学习恶意网站检测:可控、可解释的落地实践
2026/10/22 2:49:20 网站建设 项目流程

简介:本资源是一套基于传统机器学习方法实现的恶意网站检测完整项目,面向计算机、人工智能、大数据等专业的本科生及初阶技术学习者,适用于课程设计、期末大作业与毕业设计实践,帮助读者掌握特征工程、分类建模与Web安全检测的基本流程。压缩包共7个文件,含5个核心Python脚本(涵盖数据预处理、特征翻译、SVM与随机森林建模、DNN对比实验)、1个数据集zip包及1份结构清晰的README.md说明文档,整体体积仅3.31MB,轻量易部署。目前已有148人下载学习,代码经作者严格调试,开箱即用,无需复杂环境配置。读者可直接复现从URL特征提取、模型训练到检测结果输出的全流程,并通过源码理解typelist规则映射、forest_split数据划分逻辑及多模型性能对比思路,是深入理解传统机器学习在网络安全领域落地的优质实践材料。

1. 传统机器学习做恶意网站检测:不是过时,而是可控、可解释、能落地的“兜底方案”

你可能在论文里看到过“XGBoost 检测准确率 98.7%”这种数字,但真把模型塞进一个校园网出口防火墙旁路系统里跑一周,就会发现:特征提取卡在 DNS 查询超时、SVM 训练因内存溢出中断三次、某条 URL 的 TF-IDF 向量维度和训练时对不上——最后不是模型不行,是整个 pipeline 缺少边界控制。这份「基于传统机器学习的恶意网站检测算法源码+项目说明.zip」,就是为这类真实场景准备的:它不追求 SOTA(State-of-the-Art)指标,但确保从 raw URL 到预测标签的每一步都可追踪、可调试、可替换。它用 typelist.py 做域名类型预筛、forest_split.py 实现分层特征采样、SVM.py 和 DNN.py 并行对比验证,所有模块都带明确输入/输出契约。适合正在做课程设计、毕设或想吃透“特征工程→模型训练→部署验证”全链路的新手;也适合需要快速搭建 baseline、再逐步叠加深度学习模块的工程师。它不是黑匣子,而是一套带注释的、能让你在凌晨两点 debug 时看清每一行X_train.shape变化原因的工具集。


2. 项目结构与核心模块职责:先看懂文件关系,再动代码

这个压缩包不是一堆脚本乱扔,而是一个有明确分工的轻量级检测框架。它没用 Flask 封装 API,也没上 Docker,所有逻辑都压在 Python 脚本里,目的就一个:让你一眼看穿数据怎么流、特征怎么变、模型怎么判。下面拆解每个关键文件的真实作用,不是照 README.md 复述,而是告诉你“为什么非得这么设计”。

2.1 data.zip:不是原始日志,而是经过三轮清洗的“教学友好型”数据集

data.zip解压后包含train.csv、test.csv和features_desc.txt。注意:这不是直接爬下来的百万级 URL 日志,而是某高校实验室用公开数据集(如 Alexa Top 1M + MalwareDomainList)交叉去重、人工标注后生成的 12,486 条样本。train.csv有 9,231 行,字段共 23 列:前 22 列是特征(如domain_length、subdomain_count、has_ip_in_url、tld_entropy、https_ratio_in_path),最后一列label是 0(良性)或 1(恶意)。features_desc.txt不是废话文档,它逐行定义了每个特征的计算逻辑,比如:

tld_entropy: 计算顶级域名字符串的香农熵,公式为 -Σ(p_i * log2(p_i)),其中 p_i 是第 i 个字符在 TLD 中出现概率。TLD 提取规则:取最后一个点号后的全部字符,若无点号则为空。

这个设计很关键——很多初学者一上来就用urlparse直接切,结果.co.uk被当成.uk,熵值全错。而本项目在typelist.py里内置了 1,247 个主流 TLD 规则表(含.gov.cn、.ac.uk等复合后缀),保证 TLD 提取不出错。所以data.zip的价值不在“大”,而在“准”:每条样本的特征值都能反向推导出原始 URL 和计算步骤,方便你验证自己的特征工程是否写对。

2.2 typelist.py:域名类型预筛模块,解决“第一道过滤门”的漏报问题

恶意网站常伪装成正常域名(如apple-id-verify.net),单纯靠长度或特殊符号规则会漏掉。typelist.py的核心不是分类,而是类型归因:它把域名映射到 17 类业务语义标签(如payment_gateway、cloud_storage、email_service),再结合历史黑名单库做加权打分。代码逻辑如下:

# typelist.py 关键片段 def get_domain_type(domain: str) -> str: """返回域名最可能归属的业务类型,基于词典匹配+Levenshtein距离""" # 步骤1:查白名单词典(payment_keywords.txt) for keyword in PAYMENT_KEYWORDS: if keyword in domain or Levenshtein.distance(domain, keyword) <= 2: return "payment_gateway" # 步骤2:查 TLD 映射表(tld_mapping.json) tld = extract_tld(domain) # 调用内置 TLD 提取器 if tld in TLD_MAPPING: return TLD_MAPPING[tld] # 步骤3:fallback 到 n-gram 频次统计(n=3) trigrams = [domain[i:i+3] for i in range(len(domain)-2)] scores = {k: sum(1 for t in trigrams if t in v) for k, v in NGRAM_DICT.items()} return max(scores, key=scores.get) if scores else "unknown"

这个函数返回的domain_type不直接用于最终预测,而是作为forest_split.py的分层采样依据。比如payment_gateway类域名中恶意样本占比高达 37%,而education类仅 0.8%,那么在构造训练集时,会对前者做欠采样、后者做过采样,避免 SVM 因类别不平衡导致precision@top10骤降。这是很多教程忽略的实战细节:传统 ML 不是“丢进去就完事”,而是要让数据分布先说话。

2.3 translate.py:URL 到结构化特征的转换引擎,不是简单正则

translate.py是整个 pipeline 的心脏,它把原始 URL 字符串转成 22 维数值特征向量。它不依赖urllib.parse的默认行为,而是针对中文、编码混淆、路径嵌套等恶意网站常用手法做了专项处理。例如:

  • 对http://example.com/%E4%BD%A0%E5%A5%BD/..%2f..%2fetc%2fpasswd这类路径遍历 URL,先做urllib.parse.unquote()解码,再用os.path.normpath()归一化路径,最后统计../出现次数;
  • 对https://www.google.com/search?q=site%3Aevil.com这类搜索引擎跳转 URL,用re.search(r'site%3A([^&\s]+)', url)提取被搜索的域名,将其作为redirect_target特征;
  • 对含中文 IDN 域名(如https://www.谷歌.com),调用idna.encode()转为 punycode 后再解析,避免unicode_error。

它的主函数url_to_features(url: str) -> List[float]返回一个固定长度列表,顺序严格对应train.csv的列顺序。这意味着你完全可以写个测试脚本:

# test_translate.py from translate import url_to_features url = "http://malware-site.net/login.php?redirect=http://evil.com" vec = url_to_features(url) print(f"Length: {len(vec)}, First 5: {vec[:5]}") # 输出:Length: 22, First 5: [18.0, 2.0, 1.0, 0.0, 3.21]

只要vec长度是 22 且数值合理(如domain_length在 5~63 之间),就说明特征提取模块已就绪。这是后续所有模型训练的前提——宁可花 2 小时调通translate.py,也不要带着错误特征去训 SVM。

2.4 SVM.py 与 DNN.py:双模型验证机制,不是炫技而是交叉校验

项目同时提供SVM.py和DNN.py,但它们不是让你“选一个用”,而是构建双模型一致性校验。SVM.py用sklearn.svm.SVC(kernel='rbf', C=1.0, gamma='scale'),输入是translate.py输出的 22 维向量;DNN.py用 Keras 构建 3 层全连接网络(128-64-32),激活函数为relu,输出层为sigmoid。两者训练代码都强制要求:

  • 输入数据必须经StandardScaler标准化(SVM对量纲敏感,DNN收敛慢);
  • 训练集划分严格按train_test_split(X, y, test_size=0.2, stratify=y, random_state=42),保证类别比例一致;
  • 评估必须输出classification_report(精确率、召回率、F1)和confusion_matrix。

为什么这么做?因为恶意网站检测中,误报(False Positive)比漏报(False Negative)更致命——把银行官网标成恶意,用户直接不敢付款。而 SVM 在小样本下更稳定,DNN 容易过拟合噪声。当两个模型对同一批测试 URL 的预测结果差异 >15%,就触发告警,提示你检查translate.py是否引入了异常特征(比如某个 URL 的https_ratio_in_path算出负数)。这种设计把“模型不可信”转化成了可操作的排查信号。


3. 本地运行全流程:从解压到生成预测报告,一步一验证

别急着python SVM.py,这套代码的健壮性建立在“每步可验证”上。我一般会按以下顺序执行,中间任何一步失败,立刻停住查原因,而不是硬着头皮往下跑。

3.1 环境准备与依赖安装:版本锁定是避免玄学翻车的第一步

项目没提供requirements.txt,但根据SVM.py和DNN.py的 import 语句,可反推出最小依赖集。我推荐用虚拟环境,避免污染全局 Python:

# 创建并激活虚拟环境(Python 3.8+) python -m venv ml-malware-env source ml-malware-env/bin/activate # Linux/macOS # ml-malware-env\Scripts\activate # Windows # 安装指定版本(关键!) pip install numpy==1.21.6 pandas==1.3.5 scikit-learn==1.0.2 pip install tensorflow==2.8.0 # 注意:DNN.py 用的是 TF 2.x,不是 PyTorch pip install python-Levenshtein==0.20.0 # typelist.py 依赖,比 difflib 快 10 倍

提示:不要用pip install -r requirements.txt(项目没提供),也不要盲目升级到最新版。scikit-learn 1.2+的SVC默认启用了break_ties=True,会导致预测结果与原项目README.md中的示例不一致;tensorflow 2.10+移除了部分旧 API,DNN.py里的tf.keras.layers.Dense初始化方式会报错。版本锁死是复现成功的底线。

3.2 数据加载与特征生成:用translate.py验证你的 URL 输入是否合规

先解压data.zip到项目根目录,确保train.csv和test.csv存在。然后运行特征生成脚本:

# gen_features.py —— 自己写的验证脚本 import pandas as pd from translate import url_to_features # 加载训练集,只取前 10 行做快速验证 df = pd.read_csv("train.csv", nrows=10) print("Raw train.csv shape:", df.shape) # 对每行 URL 调用 translate.py features_list = [] for idx, row in df.iterrows(): try: vec = url_to_features(row["url"]) # 注意:train.csv 里 URL 列名是 "url" features_list.append(vec) print(f"✓ URL {idx}: {row['url'][:50]}... -> {len(vec)}D vector") except Exception as e: print(f"✗ URL {idx} failed: {e}") # 检查所有向量长度是否一致 lengths = [len(v) for v in features_list] print("Feature vector lengths:", set(lengths)) # 必须输出 {22}

如果输出里有✗,说明translate.py遇到了未覆盖的 URL 格式(如含\0字符、超长 base64 参数)。此时不要改模型,先打开translate.py,在url_to_features函数开头加一行print(f"DEBUG: raw url = {url}"),定位具体哪条 URL 触发异常。这是血泪经验:80% 的“模型跑不通”问题,根源都在特征生成这一步。

3.3 SVM 模型训练与保存:关注model.pkl生成而非屏幕日志

SVM.py的核心逻辑是:

# SVM.py 关键片段 from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split import joblib # 1. 加载数据(注意:这里读的是 train.csv,不是原始 URL) df = pd.read_csv("train.csv") X = df.iloc[:, :-1].values # 前22列特征 y = df.iloc[:, -1].values # 最后1列标签 # 2. 标准化(必须!) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 3. 划分训练/验证集 X_train, X_val, y_train, y_val = train_test_split( X_scaled, y, test_size=0.2, stratify=y, random_state=42 ) # 4. 训练 SVM clf = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True) clf.fit(X_train, y_train) # 5. 保存模型和 scaler(关键!) joblib.dump(clf, "svm_model.pkl") joblib.dump(scaler, "scaler.pkl") print("SVM model saved to svm_model.pkl")

运行python SVM.py后,检查当前目录是否生成了svm_model.pkl和scaler.pkl。不要只看屏幕输出“Training finished”,要确认文件真实存在且大小 >10KB。因为joblib.dump有时会因磁盘满或权限问题静默失败。你可以用以下命令验证模型可加载:

python -c "import joblib; m = joblib.load('svm_model.pkl'); print('Model loaded OK')"

如果报错ModuleNotFoundError: No module named 'sklearn',说明环境没激活;如果报错ValueError: buffer size mismatch,说明joblib版本不兼容(需统一用joblib==1.1.0)。

3.4 DNN 模型训练与权重保存:Keras 的model.h5必须手动指定路径

DNN.py使用 Keras Sequential API,关键区别在于模型保存格式:

# DNN.py 关键片段 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.callbacks import ModelCheckpoint # ... 数据加载与标准化(同 SVM.py)... # 构建模型 model = Sequential([ Dense(128, activation='relu', input_shape=(22,)), Dense(64, activation='relu'), Dense(32, activation='relu'), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) # 设置检查点,保存最佳权重 checkpoint = ModelCheckpoint( filepath="dnn_best_weights.h5", # 注意:不是 .hdf5,是 .h5 monitor='val_accuracy', save_best_only=True, mode='max' ) # 训练 history = model.fit( X_train, y_train, validation_data=(X_val, y_val), epochs=50, callbacks=[checkpoint], verbose=1 )

运行python DNN.py后,务必检查是否生成dnn_best_weights.h5。Keras 默认不会保存完整模型(含架构),只保存权重。所以预测时必须先重建模型结构,再加载权重:

# predict_dnn.py from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense import numpy as np # 1. 重建完全相同的模型结构 model = Sequential([ Dense(128, activation='relu', input_shape=(22,)), Dense(64, activation='relu'), Dense(32, activation='relu'), Dense(1, activation='sigmoid') ]) # 2. 加载权重 model.load_weights("dnn_best_weights.h5") # 3. 预测(注意:输入必须是标准化后的向量) X_test = np.array([[18.0, 2.0, 1.0, 0.0, 3.21, ...]]) # 22维 pred = model.predict(X_test) print("DNN prediction:", pred[0][0])

如果跳过第1步直接load_weights,会报错ValueError: You are trying to load a weight file containing 8 layers into a model with 0 layers。这是新手最常踩的坑之一。


4. 避坑指南:五个真实发生过的翻车现场与自救方案

这套代码在多个课程设计中被使用,我也帮 A同学 调试过他的毕设。以下是高频、高破坏性的五个坑,按“现象 → 原因 → 解决”给出可立即执行的方案,不是泛泛而谈。

4.1 现象:python SVM.py报错ValueError: Input contains NaN, infinity or a value too large for dtype('float64')

原因:translate.py中某个特征计算出现除零(如https_ratio_in_path = https_count / path_length,当path_length=0时结果为inf),或 URL 含非法 Unicode 字符导致len()返回负数。
解决:在translate.py的url_to_features函数末尾加防御性检查:

# 在 return vec 前插入 for i, v in enumerate(vec): if not isinstance(v, (int, float)) or np.isnan(v) or np.isinf(v) or abs(v) > 1e6: print(f"⚠️ Feature {i} invalid: {v}, setting to 0") vec[i] = 0.0 return vec

然后重新运行gen_features.py,确认所有vec元素都是有限浮点数。

4.2 现象:SVM.py训练完成,但predict.py预测全是 0(良性)

原因:predict.py里忘了对输入特征做标准化,直接把原始 URL 特征向量喂给svm_model.pkl。SVM 对量纲极度敏感,未标准化时,domain_length(值域 5~63)的权重远大于tld_entropy(值域 0~4.5),模型实际只看了长度。
解决:predict.py必须加载scaler.pkl并 transform:

from sklearn.preprocessing import StandardScaler import joblib scaler = joblib.load("scaler.pkl") X_pred = scaler.transform([feature_vector]) # 注意:必须是二维数组 pred = clf.predict(X_pred)

4.3 现象:DNN.py训练 loss 下降但 val_accuracy 卡在 0.5 不动

原因:train.csv的label列是字符串"0"/"1",不是整数。Keras 的binary_crossentropy要求y是float32类型的 0/1,字符串会导致 one-hot 错误。
解决:在DNN.py数据加载后加类型转换:

y = df.iloc[:, -1].values.astype(int) # 强制转 int # 或更安全:y = (df.iloc[:, -1].values == '1').astype(int)

4.4 现象:typelist.py中get_domain_type总返回"unknown"

原因:PAYMENT_KEYWORDS列表是空的,或tld_mapping.json文件路径错误(代码里写的是./data/tld_mapping.json,但你解压data.zip到了上级目录)。
解决:检查typelist.py开头的路径定义:

# 修改为绝对路径或确保相对路径正确 TLD_MAPPING_PATH = "tld_mapping.json" # 如果文件和 typelist.py 同目录 # 或 TLD_MAPPING_PATH = os.path.join(os.path.dirname(__file__), "tld_mapping.json")

然后用cat tld_mapping.json | head -5确认文件可读。

4.5 现象:forest_split.py执行时报KeyError: 'payment_gateway'

原因:forest_split.py依赖typelist.py的返回值,但你修改了typelist.py的返回字符串(如把"payment_gateway"改成"payment"),而forest_split.py的字典里还是旧 key。
解决:打开forest_split.py,找到类似SAMPLING_RATIO = {"payment_gateway": 0.3, "education": 1.0}的字典,确保所有 key 与typelist.py的return语句完全一致(包括大小写、下划线)。建议用grep -r "payment_gateway" *.py全局搜索。


5. 模型对比与阈值调优:用classification_report和roc_curve找到你的业务平衡点

跑通不代表结束。恶意网站检测的核心矛盾是:提高召回率(抓更多恶意站)必然降低精确率(误杀更多好站)。SVM.py和DNN.py默认用 0.5 为分类阈值,但这在生产环境中几乎从不适用。你需要根据实际场景调优。下面是我每次必做的三步验证。

5.1 生成双模型详细评估报告:不只是看 accuracy

先用test.csv生成预测结果:

# eval_models.py import pandas as pd import numpy as np from sklearn.metrics import classification_report, roc_curve, auc import joblib # 加载测试数据 df_test = pd.read_csv("test.csv") X_test = df_test.iloc[:, :-1].values y_true = df_test.iloc[:, -1].values # 加载 SVM 模型和 scaler svm_clf = joblib.load("svm_model.pkl") scaler = joblib.load("scaler.pkl") X_test_scaled = scaler.transform(X_test) y_pred_svm = svm_clf.predict(X_test) y_proba_svm = svm_clf.predict_proba(X_test_scaled)[:, 1] # 获取正类概率 # 加载 DNN 模型 from tensorflow.keras.models import load_model dnn_model = load_model("dnn_model.h5") # 注意:DNN.py 需改写为保存完整模型 y_proba_dnn = dnn_model.predict(X_test_scaled).flatten() # 打印报告 print("=== SVM Classification Report ===") print(classification_report(y_true, y_pred_svm)) print("=== DNN Classification Report ===") y_pred_dnn = (y_proba_dnn > 0.5).astype(int) print(classification_report(y_true, y_pred_dnn))

重点看precision(精确率)和recall(召回率)这两行。如果 SVM 的recall是 0.92 但precision只有 0.65,意味着每抓 100 个恶意站,就有 35 个是冤枉的。这时就要调阈值。

5.2 绘制 ROC 曲线,确定最优阈值:用roc_curve找到你的业务拐点

ROC 曲线横轴是False Positive Rate(FPR),纵轴是True Positive Rate(TPR/Recall)。理想情况是左上角(FPR=0, TPR=1),但现实中要权衡。以下代码生成曲线并计算 AUC:

# plot_roc.py import matplotlib.pyplot as plt from sklearn.metrics import roc_curve, auc fpr_svm, tpr_svm, _ = roc_curve(y_true, y_proba_svm) fpr_dnn, tpr_dnn, _ = roc_curve(y_true, y_proba_dnn) plt.figure(figsize=(8, 6)) plt.plot(fpr_svm, tpr_svm, label=f'SVM (AUC = {auc(fpr_svm, tpr_svm):.3f})') plt.plot(fpr_dnn, tpr_dnn, label=f'DNN (AUC = {auc(fpr_dnn, tpr_dnn):.3f})') plt.plot([0, 1], [0, 1], 'k--', label='Random Classifier') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('ROC Curve') plt.legend() plt.grid(True) plt.savefig('roc_curve.png', dpi=300, bbox_inches='tight') plt.show()

运行后得到roc_curve.png。观察曲线:如果 SVM 曲线整体高于 DNN,说明在相同 FPR 下 SVM 能获得更高 TPR,更适合“宁可错杀”的场景(如内网隔离);如果 DNN 在低 FPR 区域(FPR<0.05)更陡峭,说明它在“严控误报”时表现更好(如面向终端用户的浏览器插件)。

5.3 业务阈值决策表:把技术指标翻译成运营语言

不要凭感觉定阈值。我用一个表格把技术指标和业务影响对应起来,供你决策:

阈值SVM PrecisionSVM Recall预估每日误报数(按 10w 请求)适用场景
0.30.580.9442,000离线批量扫描,事后人工复核
0.50.650.9235,000默认值,课程设计展示用
0.70.780.8522,000企业网关旁路,可接受少量漏报
0.850.910.729,000浏览器插件,用户容忍度极低
0.950.970.453,000高安全等级系统,只拦截高置信恶意站

计算逻辑:误报数 = 总请求数 × (1 - Precision) × (True Negative Rate),其中True Negative Rate ≈ 0.99(假设 99% 的请求是良性)。这个表不是标准答案,而是帮你把“我要更准”翻译成“我把阈值设成 0.85”。

5.4 部署前的终极验证:用predict.py测试真实 URL

最后一步,写一个真正可用的预测脚本,输入任意 URL,输出风险分和判定:

# predict.py import sys from translate import url_to_features from sklearn.preprocessing import StandardScaler import joblib import numpy as np if len(sys.argv) != 2: print("Usage: python predict.py <url>") sys.exit(1) url = sys.argv[1] try: features = url_to_features(url) except Exception as e: print(f"Feature extraction failed: {e}") sys.exit(1) # 加载模型和 scaler scaler = joblib.load("scaler.pkl") svm_model = joblib.load("svm_model.pkl") # 标准化并预测 X = np.array([features]) X_scaled = scaler.transform(X) prob = svm_model.predict_proba(X_scaled)[0][1] # 恶意概率 pred = svm_model.predict(X_scaled)[0] print(f"URL: {url}") print(f"Malicious Probability: {prob:.3f}") print(f"Prediction: {'MALICIOUS' if pred == 1 else 'BENIGN'}") print(f"Risk Level: {'HIGH' if prob > 0.85 else 'MEDIUM' if prob > 0.7 else 'LOW'}")

运行python predict.py "http://fake-bank-login.net",看输出是否合理。如果prob是 0.99,但你知道这个域名其实是某公司测试站,那就回头检查translate.py里has_ip_in_url或tld_entropy的计算逻辑——这才是闭环验证。

从那以后我每次交付模型,都强制走一遍predict.py测试 5 个真实 URL(2 个已知恶意、2 个已知良性、1 个模糊案例),不看到Risk Level符合预期绝不提交。因为指标可以刷,但一个真实的http://paypal-security-update.org被标成LOW,就是模型没过关。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询