ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

恶意网站检测实战:DNN模型Python源码与数据集特征工程详解

恶意网站检测实战:DNN模型Python源码与数据集特征工程详解

简介:这份资源面向网络安全与机器学习入门及进阶学习者,提供一套基于传统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?id=123。你不可能把字符串直接喂给全连接网络,中间必须有一层特征工程。这一步做得好不好,直接决定 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_state=42)随机切分。这在恶意网站检测里是致命的,因为同一个域名下的不同 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_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(df, groups=df['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, activation='sigmoid'),损失用binary_crossentropy。

参数说明:128 和 64 这两个宽度是经验值。输入维度小于 50 时,可以降到 64/32;输入维度超过 500 时,可以升到 256/128。Dropout 0.3 是安全区间,0.5 太激进会导致欠拟合,0.1 又起不到正则作用。如果你的数据集正负样本比例超过 1:10,Dropout 可以适当加大到 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 的配合。如果你的网络里加了 BatchNorm,batch 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, dtype=torch.float32), torch.tensor(y_train, dtype=torch.float32)) loader = DataLoader(dataset, batch_size=64, shuffle=True) model = MaliciousURLNet(input_dim=X_train.shape[1]) pos_weight = torch.tensor([(y_train == 0).sum() / max((y_train == 1).sum(), 1)]) criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3) 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的patience=3表示容忍 3 个 epoch 不降,factor=0.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, dtype=torch.float32)) probs = torch.sigmoid(logits).numpy() preds = (probs > 0.5).astype(int) print(classification_report(y_test, preds, digits=4)) 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 Runtime,CPU 上通常能再快 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], dtype=torch.float32) explainer = shap.DeepExplainer(model, background) test_samples = torch.tensor(X_test[:500], dtype=torch.float32) shap_values = explainer.shap_values(test_samples) # 全局重要性 shap.summary_plot(shap_values[0], X_test[:500], feature_names=feature_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_dim=4): 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, dim=1) return self.fc(x).squeeze(-1)

这个改动通常能带来 0.5 到 1.5 个点的 AUC 提升,代价是模型稍微复杂一点。分桶边界要在训练集上按分位数确定,别用等宽分桶,否则长尾区间会没样本。

我自己的习惯是:任何 DNN 模型上线前,必须跑一遍 SHAP,确认 top 5 特征符合安全常识,否则宁可不上。这套恶意网站检测的方案,特征工程占七成,模型结构占两成,调参占一成。把第 2 章的特征管线和第 4 章的坑避开,你手里的这份源码就能从「跑得通」变成「敢上线」。希望帮到你。

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

返回列表