news 2026/10/14 0:38:15

基于SVM、随机森林与DNN的恶意网站检测系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于SVM、随机森林与DNN的恶意网站检测系统实战

简介:这是一套面向高校计算机、人工智能与电子信息等专业学生的机器学习实践资源,围绕恶意网站检测这一网络安全场景,提供从特征提取到多算法验证的完整实现方案。压缩包共11个文件,约3.32MB,以Python脚本为主体,辅以数据压缩包、备份文件与说明文档,分别承担特征向量提取、可视化绘图、模型训练与项目说明等职责。系统先通过特征提取程序构建样本向量,再借助可视化图表观察特征分布,并以对照表作为标注依据;随后分别用支持向量机、随机森林与深度神经网络三类算法完成分类验证,交叉验证准确率均超过95%。代码模块结构清晰,支持特征工程扩展与算法优化,读者可据此掌握多模型对比实验的完整流程,并在此基础上调整网络参数或引入增量学习机制。目前已有50人学习,适合作为课程实践与算法入门的参考案例。

1. 恶意网站检测这件事,为什么传统规则库越来越不够用

做安全运营的同行大概都有体会:早上刚把一批新域名拉进黑名单,下午攻击者就换了一批。基于 SVM、随机森林与 DNN 的恶意网站检测系统,本质上就是想把这种「追着黑名单跑」的被动局面,换成用模型从 URL 和页面特征里学出规律,提前拦住没见过的恶意站点。它适合两类人:一类是手里有日志、有样本,想搭一套可迭代检测流水线的工程师;另一类是想把机器学习真正落到安全场景、而不是停在调包跑数据集的学生和转行者。

我见过太多团队一上来就纠结「用哪个模型最强」,结果特征工程一塌糊涂,SVM 和 DNN 的差距还不如换一版特征来得大。这篇笔记就按我实际搭过的一套流程来讲:从数据怎么来、特征怎么抽,到三个模型怎么并行跑、怎么对比、怎么上线,最后把踩过的坑摊开说。你照着做,能拿到一套可复现的基线系统,而不是一个只在 notebook 里好看的准确率数字。

2. 数据与特征:检测系统的地基决定上限

2.1 恶意网站样本从哪来,怎么避免「喂错料」

模型再花哨,数据脏了全白搭。恶意网站检测的样本通常分两块:恶意样本和正常样本。恶意样本常见来源是公开的恶意 URL 数据集、钓鱼站点举报数据、以及自己爬虫在蜜罐里捕获的落地页;正常样本一般从常见站点排行榜里采样,注意要覆盖不同语种和不同顶级域,否则模型会学出「.cn 就是正常、.xyz 就是恶意」这种伪规律。

我一般会把样本整理成三列:url、label(0 正常 / 1 恶意)、source(来源标记)。source 这一列很关键,后面做交叉验证时要按来源分层,避免同一批爬虫抓的样本同时进训练和测试,导致指标虚高。下面这段是把原始数据清洗成统一格式的脚本:

import pandas as pd from urllib.parse import urlparse def normalize_url(u: str) -> str: u = u.strip().lower() if not u.startswith(("http://", "https://")): u = "http://" + u return u def load_and_clean(raw_path: str) -> pd.DataFrame: df = pd.read_csv(raw_path) # 统一列名,兼容不同来源的字段命名 df = df.rename(columns={"URL": "url", "label": "label"}) df["url"] = df["url"].astype(str).map(normalize_url) # 去掉解析不出 hostname 的脏数据 df["host"] = df["url"].map(lambda x: urlparse(x).hostname) df = df.dropna(subset=["host"]) # 去重:同一 URL 只保留一条,防止数据泄漏 df = df.drop_duplicates(subset=["url"]).reset_index(drop=True) return df[["url", "host", "label", "source"]]

逻辑说明:normalize_url 统一协议头和小写,避免同一个站点因为写法不同被当成两条;drop_duplicates 是防数据泄漏的第一道闸,很多「准确率 99%」的翻车都源于训练集和测试集里有重复 URL。参数上,raw_path 指向你的原始 CSV,列名不一致时改 rename 里的映射即可。

2.2 URL 词法特征:不打开页面就能判断的第一层信号

真正上线时,你不可能对每个 URL 都去渲染页面,所以词法特征是第一道防线。常用的有:URL 长度、host 长度、路径深度、是否含 IP、特殊字符(@、-、=)数量、数字占比、是否含敏感词(login、verify、account 等)。这些特征计算快、可解释性强,SVM 和随机森林在这层就能拿到不错的基线。

import re from urllib.parse import urlparse SENSITIVE_WORDS = ["login", "verify", "account", "secure", "update", "bank"] def lexical_features(url: str) -> dict: p = urlparse(url) host, path = p.hostname or "", p.path or "" full = url return { "url_len": len(full), "host_len": len(host), "path_depth": path.count("/"), "has_ip": int(bool(re.match(r"^\d{1,3}(\.\d{1,3}){3}$", host))), "n_dots": full.count("."), "n_hyphen": full.count("-"), "n_at": full.count("@"), "n_digit": sum(c.isdigit() for c in full), "digit_ratio": sum(c.isdigit() for c in full) / max(len(full), 1), "has_sensitive": int(any(w in full.lower() for w in SENSITIVE_WORDS)), }

逻辑说明:has_ip 用正则判断 host 是不是纯 IP,恶意站点常用 IP 直连规避域名封禁;digit_ratio 用比例而非绝对数,避免长 URL 天然数字多造成偏差。SENSITIVE_WORDS 是可调参数,建议按你业务里高频被仿冒的品牌词来维护,别照抄。

2.3 页面内容特征:把 HTML 变成模型能吃的向量

词法特征不够时,就得看页面内容。常见做法是提取页面标题、表单数量、外链域名数、是否含混淆脚本、iframe 数量等。这一步需要能抓取页面,注意控制超时和并发,别把目标站点打挂。下面用 requests + BeautifulSoup 抽几个稳定特征:

import requests from bs4 import BeautifulSoup def page_features(url: str, timeout: int = 5) -> dict: try: r = requests.get(url, timeout=timeout, headers={"User-Agent": "Mozilla/5.0"}) html = r.text except Exception: # 抓取失败时返回全 0,让模型自己学「抓不到」这个信号 return {"n_form": 0, "n_iframe": 0, "n_script": 0, "n_ext_link": 0, "title_len": 0} soup = BeautifulSoup(html, "html.parser") host = urlparse(url).hostname or "" ext = 0 for a in soup.find_all("a", href=True): h = urlparse(a["href"]).hostname if h and h != host: ext += 1 return { "n_form": len(soup.find_all("form")), "n_iframe": len(soup.find_all("iframe")), "n_script": len(soup.find_all("script")), "n_ext_link": ext, "title_len": len(soup.title.string) if soup.title and soup.title.string else 0, }

逻辑说明:抓取失败返回全 0 而不是抛异常,是因为线上大量恶意站点会主动拒绝或超时,这个「抓不到」本身就是强信号。timeout 设 5 秒是经验值,再长会拖垮整条流水线;并发建议用线程池控制在 20 以内,具体看你的出口带宽和目标站点承受力。

3. 三个模型怎么并行跑:SVM、随机森林、DNN 的分工与实现

3.1 特征拼接与数据集划分:别让标准化污染测试集

三个模型对输入的要求不一样:SVM 对量纲敏感,必须标准化;随机森林对量纲不敏感,可以直接吃原始特征;DNN 也建议标准化加速收敛。所以流程上先把词法特征和页面特征拼成一张宽表,再统一做划分,标准化只在训练集上 fit,然后 transform 测试集——这一步做错,指标会虚高好几个点。

import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler def build_dataset(df): feats = [] for u in df["url"]: f = lexical_features(u) f.update(page_features(u)) feats.append(f) X = pd.DataFrame(feats).fillna(0).values y = df["label"].values # 按来源分层,避免同源样本泄漏 X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, random_state=42, stratify=df["source"] ) scaler = StandardScaler().fit(X_tr) return scaler.transform(X_tr), scaler.transform(X_te), y_tr, y_te, scaler

逻辑说明:stratify=df["source"] 保证每个来源在训练和测试里比例一致,这是防泄漏的关键参数。scaler 只 fit 训练集,测试集用同一个 scaler transform,线上推理时也要保存这个 scaler 一起部署,否则新数据分布对不上。

3.2 SVM:小样本高维场景下的稳定基线

SVM 在特征维度高、样本量中等时表现稳,尤其是 RBF 核。缺点是样本上万后训练慢,调参靠网格搜索。我一般先用 LinearSVC 快速跑基线,再用 SVC + RBF 精调 C 和 gamma。

from sklearn.svm import SVC from sklearn.metrics import classification_report def train_svm(X_tr, y_tr, X_te, y_te): # C 控制惩罚力度,gamma 控制 RBF 核的影响半径 clf = SVC(C=1.0, gamma="scale", kernel="rbf", class_weight="balanced", probability=True) clf.fit(X_tr, y_tr) pred = clf.predict(X_te) print(classification_report(y_te, pred, digits=4)) return clf

逻辑说明:class_weight="balanced" 在恶意样本远少于正常样本时很重要,否则模型会偏向多数类。C 越大越容易过拟合,gamma 越大决策边界越复杂,建议从 C=1、gamma="scale" 起步,再按验证集 F1 微调。probability=True 会拖慢训练,但线上要输出置信度就得开。

3.3 随机森林:抗噪、可解释、几乎不用调参

随机森林是我最愿意先上线的模型:对缺失值和异常值不敏感,特征重要性直接可看,调参空间小。n_estimators 一般 200 到 500 够用,max_depth 不设或设大一点让它自己长。

from sklearn.ensemble import RandomForestClassifier def train_rf(X_tr, y_tr, X_te, y_te, feature_names): clf = RandomForestClassifier( n_estimators=300, max_depth=None, min_samples_leaf=2, class_weight="balanced_subsample", n_jobs=-1, random_state=42 ) clf.fit(X_tr, y_tr) pred = clf.predict(X_te) print(classification_report(y_te, pred, digits=4)) # 输出特征重要性,方便排查伪特征 imp = sorted(zip(feature_names, clf.feature_importances_), key=lambda x: -x[1]) for name, v in imp[:10]: print(f"{name}: {v:.4f}") return clf

逻辑说明:min_samples_leaf=2 能抑制噪声样本导致的过拟合;class_weight="balanced_subsample" 是按每棵树的 bootstrap 样本单独算权重,比 balanced 更适合不平衡数据。特征重要性排前几的如果出现明显伪特征(比如某个来源标记混进来了),要回头查数据。

3.4 DNN:用 Keras 搭一个够用就好的分类网络

DNN 的优势是能自动学特征交互,但恶意网站检测这种表格特征场景,网络不用太深,两三层全连接足够。层数堆多了,小数据集上必过拟合。

import tensorflow as tf from tensorflow.keras import layers, models def build_dnn(input_dim: int) -> models.Model: model = models.Sequential([ layers.Input(shape=(input_dim,)), layers.Dense(128, activation="relu"), layers.Dropout(0.3), layers.Dense(64, activation="relu"), layers.Dropout(0.3), layers.Dense(1, activation="sigmoid"), ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-3), loss="binary_crossentropy", metrics=["accuracy", tf.keras.metrics.AUC(name="auc")], ) return model def train_dnn(X_tr, y_tr, X_te, y_te): model = build_dnn(X_tr.shape[1]) es = tf.keras.callbacks.EarlyStopping(patience=3, restore_best_weights=True) model.fit(X_tr, y_tr, validation_split=0.1, epochs=30, batch_size=64, callbacks=[es], verbose=2) print(model.evaluate(X_te, y_te)) return model

逻辑说明:Dropout 0.3 是表格数据的常用值,再高会欠拟合;EarlyStopping patience=3 防止在验证集上反复横跳。batch_size 64 在几千到几万样本时比较稳,样本量小就降到 32。注意 DNN 对标准化最敏感,前面 scaler 那步不能省。

4. 模型对比与上线:指标之外还要看什么

4.1 别只看准确率:恶意检测要看召回和误报成本

恶意网站检测里,漏报(把恶意判成正常)和误报(把正常判成恶意)的代价完全不同。漏报可能意味着一次入侵,误报可能意味着用户打不开正常网站。所以评估时我优先看恶意类的召回率,再看误报率,最后才看整体准确率。下面这张表是我在一份几万条样本上的典型对比,具体数字随数据变化,看趋势即可:

模型恶意类召回误报率训练耗时可解释性
SVM(RBF)较高中等中低
随机森林高低快高
DNN高中慢低

结论通常是:随机森林做线上主力,SVM 做兜底校验,DNN 用来挖掘特征交互、给特征工程提供方向。别指望一个模型打天下。

4.2 阈值调优:把概率输出变成可运营的决策

模型输出的是概率,上线要定阈值。阈值不是拍脑袋,而是按业务能接受的误报率反推。下面这段是画 P-R 曲线找阈值的做法:

from sklearn.metrics import precision_recall_curve def pick_threshold(y_true, y_prob, max_fpr=0.01): prec, rec, thr = precision_recall_curve(y_true, y_prob) # 找到误报率不超过 max_fpr 时召回最高的阈值 best_t, best_r = 0.5, 0 for p, r, t in zip(prec, rec, thr): if (1 - p) <= max_fpr and r > best_r: best_r, best_t = r, t return best_t, best_r

逻辑说明:max_fpr 是你业务能容忍的误报上限,比如 1%。这个函数在满足误报约束的前提下最大化召回。线上建议把阈值做成配置项,随流量和攻击态势动态调,而不是写死在代码里。

4.3 模型持久化与推理服务:别让训练和上线两套代码

训练完的 scaler、模型、特征顺序必须一起保存,否则线上推理时特征对不上,结果全是玄学。我一般用 joblib 存 sklearn 模型,DNN 存 SavedModel,再写一个统一的 predict 入口。

import joblib def save_artifacts(scaler, rf, svm, feature_names, path="artifacts"): import os os.makedirs(path, exist_ok=True) joblib.dump(scaler, f"{path}/scaler.pkl") joblib.dump(rf, f"{path}/rf.pkl") joblib.dump(svm, f"{path}/svm.pkl") joblib.dump(feature_names, f"{path}/features.pkl") def predict_one(url, scaler, model, feature_names): f = lexical_features(url) f.update(page_features(url)) x = np.array([[f.get(k, 0) for k in feature_names]]) x = scaler.transform(x) return model.predict_proba(x)[0, 1]

逻辑说明:feature_names 单独存是为了保证推理时特征顺序和训练完全一致,这是血泪经验——顺序错一位,模型表现直接崩。predict_one 里用 f.get(k, 0) 兜底,防止线上某个特征抽取失败导致整条报错。

5. 避坑与排查:那些让指标虚高、上线翻车的细节

5.1 数据泄漏:准确率 99% 的头号元凶

现象:离线评估准确率 99%,上线后召回惨不忍睹。原因:训练集和测试集里有重复 URL,或者同一批爬虫抓的样本被分到两边,模型其实在背答案。解决:按 URL 去重,按来源分层划分,最好再按时间划分——用旧数据训练、新数据测试,更接近真实场景。

5.2 类别不平衡:模型学会「全判正常」

现象:恶意类召回极低,但整体准确率很高。原因:恶意样本占比可能只有几个百分点,模型直接全判正常就能拿高准确率。解决:class_weight 设 balanced,或用欠采样/过采样,评估时盯住恶意类召回和 F1,别被整体准确率骗了。

5.3 特征穿越:用了未来才有的信息

现象:某个特征重要性异常高,但上线后该特征经常取不到。原因:比如用了「该域名是否在最新黑名单里」这种特征,训练时黑名单已经包含答案,线上却拿不到。解决:逐个审查高重要性特征,凡是依赖标注结果或未来信息的,一律剔除。

5.4 抓取特征不稳定:线上大量超时拖垮服务

现象:推理延迟忽高忽低,高峰期大面积超时。原因:page_features 同步抓取页面,目标站点慢或拒绝连接时阻塞整条流水线。解决:给抓取设硬超时,失败返回默认值,把页面特征做成异步预取或缓存,别在推理主路径上同步抓。

5.5 阈值写死:攻击态势变了模型没跟上

现象:上线初期效果不错,几周后误报飙升。原因:攻击者换了手法,特征分布漂移,固定阈值不再适用。解决:定期用新样本重训,监控线上概率分布,阈值做成可配置,配合人工复核样本回流形成闭环。

6. 让这套系统真正跑起来的两个进阶技巧

第一个技巧是把三个模型的输出做 stacking,而不是简单投票。具体做法:用 SVM、随机森林、DNN 在验证集上的概率输出作为新特征,再训一个逻辑回归做融合。这样能利用不同模型在不同样本上的互补性,通常比单模型召回高几个点。注意融合层的训练数据必须和基模型训练数据分开,否则一样会过拟合。

from sklearn.linear_model import LogisticRegression def stack_fusion(svm_prob, rf_prob, dnn_prob, y_val): X_meta = np.column_stack([svm_prob, rf_prob, dnn_prob]) meta = LogisticRegression(class_weight="balanced").fit(X_meta, y_val) return meta

逻辑说明:三个概率作为元特征,逻辑回归学出各自权重。class_weight 同样要设,保持对恶意类的敏感。上线时三个基模型并行推理,再走一次 meta 预测,延迟增加有限。

第二个技巧是建立样本回流闭环。线上每次人工复核的判定结果,都带着原始 URL 和特征存回样本库,每周增量重训一次。这一步听起来朴素,但它是这套系统能不能长期活下去的关键。我自己的习惯是:任何一次误报,先别急着改模型,先把那条样本捞出来看特征,十有八九是特征抽取环节出了问题,而不是模型本身不行。模型是黑匣子,特征才是你能掌控的部分。把特征管好,SVM、随机森林、DNN 谁强谁弱反而没那么重要了。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 0:37:53

atlthunk.dll丢失怎么办?官方修复流程与单文件下载风险详解

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/10/14 0:37:47

audiodev.dll丢失怎么办?官方修复流程与单文件下载风险详解

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

作者头像 李华
网站建设 2026/10/14 0:37:35

ESP8266硬件SPI驱动ST7735:从接线到局部刷新优化

简介&#xff1a;ESP8266搭配MicroPython驱动ST7735TFT屏幕的硬件SPI方案&#xff0c;面向嵌入式显示开发者和物联网项目爱好者&#xff0c;重点解决模拟SPI刷屏速率偏低、硬件SPI接口分配不明确等实际问题。压缩包体积仅约4KB&#xff0c;共3个Python文件&#xff0c;分别承担…

作者头像 李华
网站建设 2026/10/14 0:35:48

Linux标准IO缓冲机制与实战避坑指南

1. 从一次文件读写异常说起&#xff1a;为什么标准IO值得单独拎出来讲很多人第一次接触Linux下的文件操作&#xff0c;都是从open、read、write、close这四个系统调用开始的。这四个函数确实好用&#xff0c;直接、明确、控制力强。但如果你写过稍微复杂一点的程序&#xff0c;…

作者头像 李华
网站建设 2026/10/14 0:35:18

海湾MODBUS RTU规约详解:从RS485报文到消防主机数据采集实战

简介&#xff1a;《海湾MODBUS RTU通讯规约》是一份面向火灾报警系统集成与开发人员的协议说明文档&#xff0c;围绕海湾GST系列控制器与上位机之间的MODBUS RTU通信展开。文档依据火灾报警设备状态多、事件驱动等特点&#xff0c;定义了从站地址、波特率4800、无校验、1位停止…

作者头像 李华
网站建设 2026/10/14 0:33:44

匈牙利算法矩阵形式详解:从成本约减到最优分配的完整推导

前阵子有个做调度优化的朋友来问&#xff0c;说看网上的匈牙利算法教程&#xff0c;流程图一摆、代码一贴&#xff0c;看着都懂&#xff0c;可自己拿个成本矩阵一算就发懵&#xff0c;尤其是“矩阵形式”这一层&#xff0c;总觉得像魔术&#xff1a;为什么行减一减、列减一减&a…

作者头像 李华