简介这份资源面向网络安全与机器学习入门及进阶学习者提供一套基于传统DNN算法的恶意网站检测完整实践方案可用于课程设计、毕业项目或安全检测方向的算法练手。压缩包共7个文件以5个Python脚本为核心辅以1个数据压缩包和1份项目说明文档整体约3.31MB体积轻便便于本地运行。数据部分整合了黑白名单网站特征脚本覆盖特征提取、特征可视化以及SVM、随机森林、DNN三种模型的训练与预测流程其中DNN采用分类算法完成检测任务便于横向对比不同模型的检测效果。项目结构清晰从数据处理到模型评估形成完整链路读者可借此理解恶意网站检测的特征工程思路与模型调参方法并在此基础上替换数据集或改进网络结构。目前已有373人学习适合希望快速上手安全检测实战的开发者参考。1. 恶意网站检测为什么用 DNN 而不是 XGBoost一份能跑通的 Python 源码与数据集拆解手里拿到一份「基于传统机器学习 DNN 算法的恶意网站检测 python 源码 数据集.zip」多数人第一反应是解压、装依赖、跑train.py然后看着准确率 98% 沾沾自喜。但真正上线过 URL 风控的人都知道恶意网站检测的难点从来不在模型结构而在特征怎么造、数据怎么切、线上分布怎么漂移。这份标题里的关键词——DNN、恶意网站检测、python、源码、数据集——其实对应了四个必须逐个拆开的问题DNN 在这里到底比树模型强在哪、URL 文本特征怎么变成网络能吃的向量、源码里的训练脚本哪些参数不能乱改、数据集切分时哪些坑会让离线指标虚高。这篇文章面向的是想把这套方案真正落地的人做安全风控的工程师、想拿它当毕业设计或课程项目的学生、以及手里已经有这份源码但跑出来结果不对劲的开发者。我会按「特征工程 → 模型结构 → 训练调参 → 避坑 → 进阶验证」的顺序把一份典型的 DNN 恶意网站检测工程从零讲透代码可以直接抄参数会告诉你为什么这么设。读完你应该能自己判断这套东西值不值得投入、能不能改成你自己的业务场景。2. 从 URL 到特征向量DNN 恶意网站检测的数据管线怎么搭DNN 不是万能的它对输入特征的表达方式极其敏感。恶意网站检测里原始输入就是一条 URL 字符串比如http://paypal-secure-login.xyz/verify?id123。你不可能把字符串直接喂给全连接网络中间必须有一层特征工程。这一步做得好不好直接决定 DNN 能不能打过 XGBoost。2.1 词法特征、统计特征、主机特征三类怎么选常见的做法是把 URL 拆成三类特征分别对应不同的攻击信号。第一类是词法特征lexical针对 URL 字符串本身。包括 URL 总长度、域名长度、路径长度、点的数量、连字符数量、数字占比、特殊字符、-、_、、?出现次数、是否包含 IP 地址、是否使用 HTTPS。这些特征背后是有安全含义的钓鱼网站为了伪装域名里经常塞大量连字符和数字比如paypal-secure-login-2024.xyz而正常站点域名通常短且语义清晰。第二类是主机特征host-based针对域名本身。包括域名年龄whois 注册时间、DNS 记录数量、是否有 MX 记录、Alexa/SimilarWeb 排名、域名是否在黑白名单里。域名年龄是最强的单特征之一——绝大多数恶意域名存活时间不超过几个月。但这类特征需要外部数据源离线数据集里往往已经预计算好了。第三类是内容特征content-based需要实际抓取页面。包括页面里外链数量、iframe 数量、表单 action 指向、JS 混淆程度、是否自动下载文件。这类特征最强但也最贵因为要真实请求目标站点线上做实时检测时延迟吃不消。我一般会先用词法 主机特征跑一版基线因为这两类特征可以纯离线计算不依赖网络请求。内容特征留到有充足算力时再加。2.2 用 Python 把一条 URL 拆成 30 维特征向量下面这段代码是特征提取的核心输入一条 URL输出一个定长向量。注意每个特征都要做归一化或截断否则长度几百的 URL 会把网络带偏。import re import math from urllib.parse import urlparse def extract_features(url: str) - list: 把一条 URL 拆成固定长度的数值特征向量 feats [] parsed urlparse(url) host parsed.netloc or path parsed.path or query parsed.query or # 1. 长度类特征做 log 压缩避免极端值 feats.append(min(len(url), 200) / 200.0) feats.append(min(len(host), 100) / 100.0) feats.append(min(len(path), 150) / 150.0) # 2. 字符统计类 feats.append(url.count(.)) feats.append(url.count(-)) feats.append(url.count(_)) feats.append(url.count(/)) feats.append(url.count(?)) feats.append(url.count()) feats.append(url.count()) feats.append(url.count(%)) # 3. 数字与特殊字符占比 digits sum(c.isdigit() for c in url) feats.append(digits / max(len(url), 1)) feats.append(sum(c.isalpha() for c in url) / max(len(url), 1)) # 4. 主机类特征 feats.append(1.0 if re.match(r^\d\.\d\.\d\.\d$, host) else 0.0) feats.append(1.0 if parsed.scheme https else 0.0) feats.append(host.count(.)) # 5. 敏感词命中钓鱼常用词 keywords [login, verify, secure, account, update, bank, pay] for kw in keywords: feats.append(1.0 if kw in url.lower() else 0.0) # 6. 熵值衡量域名随机程度 def entropy(s): if not s: return 0.0 prob [s.count(c) / len(s) for c in set(s)] return -sum(p * math.log2(p) for p in prob) feats.append(entropy(host)) return feats逻辑说明前三个长度特征做了归一化因为 URL 长度从 10 到 2000 都有直接输入会让网络对长 URL 过度敏感。字符统计类特征不做归一化因为它们的取值范围本身有限0 到几十。敏感词命中用 one-hot 形式7 个词对应 7 维。熵值特征用来捕捉 DGA域名生成算法产生的随机域名正常域名熵值通常在 2.5 到 3.5 之间DGA 域名往往超过 4。参数说明min(len(url), 200)里的 200 是截断阈值你可以根据自己数据集的 URL 长度分布调整一般取 95 分位数。敏感词列表要按你的业务场景定制做电商风控就加order、refund做银行就加card、transfer。2.3 数据集切分为什么随机切分会让指标虚高拿到数据集后最常见的翻车点是用train_test_split(random_state42)随机切分。这在恶意网站检测里是致命的因为同一个域名下的不同 URL 会被分到训练集和测试集两边模型实际上在「背答案」。正确的做法是按域名切分先把所有 URL 按主域名分组然后以域名为单位划分训练集和测试集保证同一个域名的 URL 只出现在一边。代码大概是这样import pandas as pd from sklearn.model_selection import GroupShuffleSplit df pd.read_csv(dataset.csv) # 列: url, label df[domain] df[url].apply(lambda u: urlparse(u).netloc) gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(df, groupsdf[domain])) train_df, test_df df.iloc[train_idx], df.iloc[test_idx]这样切分后你大概率会看到准确率从 98% 掉到 92% 左右。别慌92% 才是真实水平。我见过太多项目因为随机切分离线 99% 上线 70%血泪经验。提示如果你的数据集里域名信息缺失至少按 URL 的二级域名或 IP 段做分组绝不能纯随机切。3. DNN 模型结构设计几层、多少神经元、Dropout 放哪特征向量准备好之后接下来是网络结构。恶意网站检测的 DNN 不需要很深因为输入维度通常只有几十到几百维数据量也就几万到几十万条。堆到十几层只会过拟合。3.1 一个够用的 DNN 结构长什么样我一般用这样的结构输入层 → 全连接 128 → ReLU → Dropout 0.3 → 全连接 64 → ReLU → Dropout 0.3 → 全连接 1 → Sigmoid。三层全连接参数量在几万级别训练快推理也快。import torch import torch.nn as nn class MaliciousURLNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 1) ) def forward(self, x): return self.net(x).squeeze(-1) # 输出 logits配合 BCEWithLogitsLoss逻辑说明输出层不加 Sigmoid因为 PyTorch 的BCEWithLogitsLoss内部已经做了 Sigmoid数值更稳定。如果你用 Keras输出层加Dense(1, activationsigmoid)损失用binary_crossentropy。参数说明128 和 64 这两个宽度是经验值。输入维度小于 50 时可以降到 64/32输入维度超过 500 时可以升到 256/128。Dropout 0.3 是安全区间0.5 太激进会导致欠拟合0.1 又起不到正则作用。如果你的数据集正负样本比例超过 1:10Dropout 可以适当加大到 0.4。3.2 训练脚本里三个不能乱改的参数源码里通常有一个train.py里面有几个参数新手最爱乱调调完指标就崩。第一个是学习率。Adam 优化器默认 1e-3这个值对恶意网站检测任务通常偏大容易在后期震荡。我一般用 1e-3 配合ReduceLROnPlateau验证集 loss 连续 3 个 epoch 不降就乘 0.5。如果你用 SGD学习率要设到 0.01 到 0.1并且加 momentum 0.9。第二个是 batch size。64 或 128 都行但要注意和 batch norm 的配合。如果你的网络里加了 BatchNormbatch size 小于 32 会让统计量不准反而掉点。恶意网站检测数据量不大我一般用 64。第三个是类别权重。恶意网站数据集里正常站点通常远多于恶意站点比例可能到 10:1。这时候不加pos_weight模型会倾向于全预测为正常准确率看着高但召回率惨不忍睹。from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim # 假设 X_train, y_train 已经准备好 dataset TensorDataset(torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.float32)) loader DataLoader(dataset, batch_size64, shuffleTrue) model MaliciousURLNet(input_dimX_train.shape[1]) pos_weight torch.tensor([(y_train 0).sum() / max((y_train 1).sum(), 1)]) criterion nn.BCEWithLogitsLoss(pos_weightpos_weight) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience3) for epoch in range(50): model.train() for xb, yb in loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() # 验证集评估省略拿到 val_loss 后 scheduler.step(val_loss)逻辑说明pos_weight设为负样本数除以正样本数这样正样本的损失会被放大模型不敢忽略恶意样本。ReduceLROnPlateau的patience3表示容忍 3 个 epoch 不降factor0.5表示每次减半。参数说明pos_weight不要设得过大超过 20 会导致大量误报。如果正负比超过 50:1建议先做负采样而不是一味加大权重。训练轮数 50 是上限实际配合 early stopping通常 15 到 25 个 epoch 就收敛了。3.3 评估指标别只看 accuracy恶意网站检测的评估必须看 precision、recall 和 F1尤其是 recall。因为漏掉一个恶意网站的代价远大于误报一个正常网站。我一般要求 recall 不低于 0.95在这个前提下再看 precision。from sklearn.metrics import classification_report, roc_auc_score model.eval() with torch.no_grad(): logits model(torch.tensor(X_test, dtypetorch.float32)) probs torch.sigmoid(logits).numpy() preds (probs 0.5).astype(int) print(classification_report(y_test, preds, digits4)) print(AUC:, roc_auc_score(y_test, probs))阈值 0.5 不是固定的。如果业务上更怕漏报可以把阈值降到 0.3牺牲 precision 换 recall。这个阈值要在验证集上按业务目标调不能拍脑袋。4. 避坑与排查DNN 恶意网站检测最常见的 5 个翻车现场这一章是我踩过的坑合集每条都按「现象 → 原因 → 解决」写你对号入座。4.1 训练 loss 正常但验证 AUC 只有 0.5现象训练集 loss 一路下降准确率 99%但验证集 AUC 在 0.5 附近晃等于随机猜。原因特征里混入了标签泄漏。最常见的是数据集里带了domain_age或alexa_rank这类特征而恶意样本的这些字段大量缺失填了 -1 或 0模型直接学会了「这个字段是 -1 就是恶意」根本没学 URL 本身。解决检查所有特征和标签的相关性把相关性超过 0.9 的特征删掉。缺失值不要填 -1改成填中位数并加一个is_missing标志位。4.2 上线后误报率飙升离线指标完全对不上现象离线 recall 0.96上线一周被业务方投诉误报太多。原因训练集和线上流量的分布不一致。训练集里的正常站点多是知名大站线上却充斥着大量长尾小站它们的 URL 长度、域名结构和大站完全不同模型没见过。解决定期用线上真实流量做一次评估把误报样本捞出来重新标注加入训练集。同时检查特征归一化的统计量均值、方差是不是还在用训练集的老值线上推理必须用同一套归一化参数。4.3 GPU 上跑得好好的CPU 推理慢十倍现象训练用 GPU推理部署到 CPU 服务器单条延迟从 2ms 涨到 20ms。原因模型太小GPU 的并行优势发挥不出来反而 kernel launch 开销占大头。而且 PyTorch 默认的 CPU 推理没开 MKL 优化。解决小模型直接上 CPU用torch.jit.trace导出 TorchScript再开torch.set_num_threads(4)。如果还慢考虑把模型转成 ONNX RuntimeCPU 上通常能再快 2 到 3 倍。4.4 数据集里 URL 重复训练集和测试集互相污染现象测试集准确率异常高达到 99.5%但换个数据集就崩。原因数据集里同一条 URL 出现多次随机切分后训练集和测试集都有模型在测试时见到的是训练时背过的样本。解决切分前先df.drop_duplicates(subset[url])再按域名分组切分。如果同一 URL 有不同标签说明标注有问题要人工核对。4.5 模型对短 URL 全部预测为正常现象像http://t.cn/abc这种短链模型一律判正常但里面藏着恶意跳转。原因短 URL 的特征向量里长度、点数、特殊字符数全是低值和正常短链几乎一样模型没有区分信号。解决短链必须做展开follow redirect拿到最终落地 URL 再提特征。如果线上不允许实时展开就单独训练一个短链专用模型特征里加入跳转链长度、跳转次数、最终域名年龄。注意短链展开有 SSRF 风险展开服务要部署在隔离环境限制跳转次数和超时时间。5. 进阶用 SHAP 验证模型到底学到了什么以及一个提点技巧模型跑通只是开始真正让你敢上线的是「知道模型为什么这么判」。DNN 是黑匣子但 SHAP 可以把它撬开一条缝。5.1 用 SHAP 看每个特征的贡献方向SHAP 的DeepExplainer对 PyTorch 模型支持不错能给出每条样本每个特征的贡献值。我一般会抽 500 条测试样本看全局特征重要性再挑几条误报样本单独分析。import shap import numpy as np # 取 200 条背景样本500 条待解释样本 background torch.tensor(X_train[:200], dtypetorch.float32) explainer shap.DeepExplainer(model, background) test_samples torch.tensor(X_test[:500], dtypetorch.float32) shap_values explainer.shap_values(test_samples) # 全局重要性 shap.summary_plot(shap_values[0], X_test[:500], feature_namesfeature_names)逻辑说明background是参考基线一般从训练集随机抽200 条足够。shap_values的形状和输入一致每个值表示该特征把预测推高或推低了多少。参数说明样本量不要太大DeepExplainer 是 O(N) 复杂度500 条在 CPU 上大概几十秒。如果特征维度超过 200建议先做特征选择再解释否则图会糊成一团。看 SHAP 图时重点看两件事一是特征重要性排序是否符合安全直觉如果url_length排第一而domain_age排到十名开外说明数据可能有问题二是看误报样本里哪个特征贡献最大往往是某个特征在长尾分布上失效了。5.2 一个低成本提点技巧特征分桶 嵌入如果你的词法特征已经榨干可以试试把离散特征做分桶后接嵌入层。比如把 URL 长度分成 10 个桶每个桶学一个 4 维嵌入再和连续特征拼接。这样模型能学到「长度在某个区间时风险更高」的非线性关系比直接归一化喂进去更灵活。class BucketEmbeddingNet(nn.Module): def __init__(self, num_continuous, bucket_sizes, embed_dim4): super().__init__() self.embeddings nn.ModuleList([ nn.Embedding(n, embed_dim) for n in bucket_sizes ]) total_dim num_continuous len(bucket_sizes) * embed_dim self.fc nn.Sequential( nn.Linear(total_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 1) ) def forward(self, x_cont, x_bucket): emb [e(x_bucket[:, i]) for i, e in enumerate(self.embeddings)] x torch.cat([x_cont] emb, dim1) return self.fc(x).squeeze(-1)这个改动通常能带来 0.5 到 1.5 个点的 AUC 提升代价是模型稍微复杂一点。分桶边界要在训练集上按分位数确定别用等宽分桶否则长尾区间会没样本。我自己的习惯是任何 DNN 模型上线前必须跑一遍 SHAP确认 top 5 特征符合安全常识否则宁可不上。这套恶意网站检测的方案特征工程占七成模型结构占两成调参占一成。把第 2 章的特征管线和第 4 章的坑避开你手里的这份源码就能从「跑得通」变成「敢上线」。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?