news 2026/10/13 2:51:10

恶意网站检测实战:机器学习与URL特征工程全流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
恶意网站检测实战:机器学习与URL特征工程全流程解析

简介:面向网络安全与机器学习初学者的恶意网站检测实践资源,基于传统机器学习与深度学习方法,利用黑名单/白名单数据完成特征提取、可视化与分类建模。资源提供完整可运行的 Python 代码,覆盖特征提取、数据可视化,以及 SVM、随机森林、DNN 三种分类模型的训练与预测流程,其中 DNN 基于深度神经网络分类算法实现。数据以黑白名单形式内置,整合后的特征文件可直接用于模型训练,适合毕设、课程设计、作业或入门项目复现。包体共 7 个文件,包含 5 个 Python 脚本、1 个数据压缩包和 1 个说明文档,整体大小 3.31MB,结构简洁,可按脚本顺序运行。目前已有 186 人学习下载。代码均经过测试成功,可用于快速掌握从数据预处理、特征工程到机器学习建模的完整链路,也可在此基础上修改扩展,作为网络安全与机器学习方向的项目参考。

1. 恶意网站检测:传统机器学习路线的第一个完整实战

当你拿到一批网站 URL,需要判断哪些是恶意站点,手头又没有大规模标注数据和 GPU 算力时,传统机器学习路线依然是最稳的选择。这套资源就是一个完整的毕设源码包:translate.py 负责从 URL 中抽取特征,typelist.py 负责可视化数据分布,SVM.py、forest_split.py、DNN.py 三份脚本分别训练三种不同的机器学习模型,数据和代码全部配齐。下载后按 README 的顺序执行,就能把「机器学习检测恶意网站」的完整链路从头到尾复现一遍。它特别适合正在找机器学习实战题目、做安全方向课程设计或毕设的从业者与学生——不用从零搭环境,重点放在理解特征含义和调参逻辑上。

2. translate.py 与特征工程:把网站 URL 变成机器能懂的向量

2.1 黑白名单数据长什么样,为什么必须先做特征提取

data.zip 里存放的是白名单与黑名单的原始网站数据。原始数据通常只包含两列:URL 字符串和标签(0 代表白名单,1 代表黑名单,具体编码以 README 说明为准)。但字符串不能直接喂给 SVM、随机森林这类模型——sklearn 的绝大多数分类器都要求输入是数值矩阵,所以第一步必须把 URL 转成数值特征。

特征提取是整个项目里最影响最终效果的一环。模型性能的上限由特征决定,分类器只是在逼近这个上限,这也就是业内常说的「垃圾进,垃圾出」。恶意网站的信号往往藏在 URL 结构里:超长路径、大量数字、可疑关键词、特殊字符密度,甚至域名是不是 IP 直连。translate.py 做的事情,就是把这类先验知识翻译成机器学习模型能读的数值向量。

2.2 translate.py 核心逻辑:从 URL 字符串到数值特征

translate.py 的主流程可以还原为下面这段逻辑:

# translate.py 核心逻辑(原始数据文件名按 data.zip 实际情况调整) import pandas as pd import re from urllib.parse import urlparse def extract_features(url): parsed = urlparse(url) host = parsed.netloc path = parsed.path features = { 'url_len': len(url), # URL 总长度 'host_len': len(host), # 域名长度 # 数字占比,比绝对数量更稳定,能消除 URL 长度差异 'digit_ratio': sum(c.isdigit() for c in url) / max(len(url), 1), # 特殊字符数量,覆盖 -_.~@=?#& 等常见干扰符号 'special_char': len(re.findall(r'[-_.~@=?#&]', url)), # 路径层级深度,钓鱼页面常伪造多层目录 'path_depth': len([s for s in path.split('/') if s]), # 可疑关键词是否命中,覆盖 login/verify/pay 等钓鱼敏感词 'has_suspicious_word': int(bool(re.search( r'(login|verify|bank|pay|free|win)', url.lower()))), # 域名是否为 IP 直连,很多恶意站不做域名解析 'is_ip_host': int(bool(re.match(r'\d+\.\d+\.\d+\.\d+', host))) } return features df = pd.read_excel('data/raw_urls.xlsx') # 原始黑白名单数据 feature_df = df['url'].apply(extract_features) result = pd.concat([df[['url', 'label']], feature_df.apply(pd.Series)], axis=1) result.to_excel('data/2.xlsx', index=False) print('特征表已生成,shape =', result.shape)

这里有几个参数值得细说。url_len 和 host_len 之所以单独拆开,是因为恶意站点经常把域名和路径拉得很长,用来混淆视听;digit_ratio 用除法而不是直接用数字个数,是为了防止长 URL 天然占优的偏差;path_depth 按斜杠切分并过滤空字符串,对应的是路径嵌套层级——钓鱼页面特别喜欢伪造 /index/login/verify 这种深目录结构。

处理完成后,白名单与黑名单的整合特征会统一落到 data/2.xlsx,后续 SVM.py、forest_split.py、DNN.py 三个脚本都从这张表读取特征。也就是说,translate.py 是整个项目的数据入口,这一步的输出质量直接决定后面所有模型的上限。

2.3 特征维度怎么选:七个字段背后的判断依据

特征名类型设计意图
url_len数值捕捉超长 URL 的恶意倾向
host_len数值识别异常长域名
digit_ratio数值量化 URL 中的数字密度
special_char数值统计特殊符号数量
path_depth数值衡量路径嵌套深度
has_suspicious_word布尔命中钓鱼常用关键词
is_ip_host布尔标记 IP 直连域名

这七个字段并不是拍脑袋定的。从恶意网站的攻防逻辑看,攻击者有几个天然的行为惯性:喜欢注册长域名来模仿正规站点,喜欢在 URL 里堆数字绕过人工审查,喜欢用 login、verify、bank 这类词诱导用户输入敏感信息,还经常因为成本问题直接用 IP 而不是域名。每一列背后都对应着一种可解释的恶意行为模式,这也正是传统机器学习相比纯黑盒深度学习的一大优势——你能说清楚模型到底在看什么。

如果后续想扩展,常见的做法是再加一列 url_entropy(URL 的信息熵),用来捕捉字符串的随机程度,很多钓鱼域名和短链接的熵值会异常高。不过特征不是越多越好,像 host_len 和 url_len 就有一定相关性,加特征前最好先看相关系数矩阵,避免引入冗余。

3. typelist.py 与数据审视:训练前先看清黑白样本的分布

3.1 为什么要先可视化:三个信号影响后面的模型选型

typelist.py 的作用是把 2.xlsx 里的特征数据可视化出来,常见输出包括类别分布柱状图、单特征直方图、特征两两之间的散点图。这一步看起来不是必须的,很多初学者会直接跳过它进入训练环节——但跳过之后,问题往往在模型跑完才爆发,到时候再回头查数据就费劲了。

可视化主要暴露三个信号。第一个是类别不平衡:如果黑名单样本只占全部数据的 15%,那模型学到的决策边界会严重偏向白名单,直接跑 SVM 很可能出现「全部预测为白名单」的假高准确率。第二个是特征重叠:比如 digit_ratio 在白名单和黑名单上的分布几乎完全重合,说明这个特征区分度很低,留着它只会增加噪声。第三个是极端离群点:个别 URL 的长度可能是正常值的几十倍,这种样本在 SVM 里会成为很难处理的支撑向量,导致训练变慢甚至不收敛。

这三个信号直接用 describe() 也能看到一部分,但 typlist.py 的绘图方式更直观。我一般会先跑一遍可视化,把类别分布和每个特征的直方图摊开看一遍,心里有数之后再决定要不要做采样或特征筛选。

3.2 2.xlsx 的数据组织:白名单与黑名单特征如何整合

经过 translate.py 处理后,2.xlsx 里每一行对应一个网站样本,列结构大致是:url(原始字符串)、label(0/1 标签)、以及 2.3 节表格里的七个特征列。白名单和黑名单数据已经整合在同一张表里,训练时不再需要区分数据来源,直接按 label 列切分即可。

这里有个细节要注意:整合后的特征表是否包含 url 原始列。包含是合理的,因为后续做错误分析时需要回溯到具体的 URL;但喂给模型时一定要把它 drop 掉。如果把 url 字符串也塞进训练矩阵,sklearn 会直接报 ValueError,而且就算转成数值编码,对分类任务也是纯噪声。

3.3 用 pandas 快速检查数据:描述性统计与类别比例

typelist.py 的可视化之外,我还习惯用 pandas 做一次快速的数据体检,几行代码就能对数据质量有个量化判断:

import pandas as pd df = pd.read_excel('data/2.xlsx') print('样本总量:', len(df)) print(df['label'].value_counts(normalize=True)) # 类别占比 # 按标签分组看各特征均值,判断特征是否有区分度 print(df.groupby('label').mean()) # 检查是否存在空值或异常值 print('空值统计:') print(df.isnull().sum()) print('极端值示例:') print(df[df['url_len'] > df['url_len'].quantile(0.99)].head())

这段代码的重点在分组均值这一步。如果某一列在黑名单和白名单上的均值几乎一样,比如都是 0.3 左右,说明这个特征对区分恶意站点几乎没有贡献,后续建模时可以优先考虑剔除;如果差异明显,比如黑样本的 digit_ratio 平均值是白样本的两倍,那这个特征就是有价值的信号。类别占比则决定了后续要不要使用 class_weight 或过采样手段。

4. 三个模型实战:SVM、随机森林与 DNN 的选型与参数调试

4.1 SVM.py:先标准化,再谈 RBF 核与 C 值

SVM 是这类小样本分类任务的常客。但 SVM 有一个硬性前提:特征必须标准化。url_len 的量级可能是几百,而 has_suspicious_word 只有 0 和 1,如果不做标准化,RBF 核计算距离时会被大尺度特征完全主导,模型等价于只看了 url_len 一列。

# SVM.py 核心训练逻辑 from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop(columns=['url', 'label']) y = df['label'] # stratify=y 保证训练集和测试集中黑白比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) model = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True, random_state=42) model.fit(X_train, y_train) print('SVM 测试集准确率:', model.score(X_test, y_test))

SVC 参数里需要重点理解的是 C 和 gamma。C 是误分类惩罚系数,C 越大模型越倾向于把所有训练样本分对,但容易过拟合;C 越小决策边界越平滑,泛化可能更好。gamma 控制 RBF 核的影响半径,gamma='scale' 会让 sklearn 根据特征数量自动计算默认值,适合作为起点。probability=True 是为了后续能拿到 predict_proba 输出,调阈值和画 PR 曲线都依赖它。

训练集样本量在一万以内时,RBF 核的 SVM 通常能在几十秒内完成训练。如果发现训练很慢,先检查标准化是否做了,再考虑把 kernel 换成 'linear'——线性核虽然表达力弱一些,但在这个特征维度下往往已经够用。

4.2 forest_split.py:随机森林的特征重要性与树的数量

随机森林对特征尺度不敏感,不需要标准化,这是它和 SVM 最大的区别。forest_split.py 这个命名也直接对应了随机森林的核心机制——分裂节点时随机选取特征子集。

# forest_split.py 核心训练逻辑 from sklearn.ensemble import RandomForestClassifier X = df.drop(columns=['url', 'label']) y = df['label'] model = RandomForestClassifier( n_estimators=200, # 树的数量 max_depth=10, # 限制单棵树深度,防止过拟合 min_samples_leaf=2, # 叶节点最少样本数 random_state=42, n_jobs=-1 # 用满所有 CPU 核心 ) model.fit(X_train, y_train) print('随机森林测试集准确率:', model.score(X_test, y_test)) # 输出特征重要性,用于反推哪些特征对判断恶意站点贡献最大 importance = sorted(zip(X.columns, model.feature_importances_), key=lambda x: -x[1]) for feat, imp in importance: print(f'{feat}: {imp:.4f}')

n_estimators=200 对于七维特征的数据集已经足够,再增大收益很小,只会拖慢训练。max_depth=10 和 min_samples_leaf=2 是控制方差的组合拳,恶意网站检测的数据量通常不大,不限制深度的话每棵树都可能长成完美记忆训练集的形状。n_jobs=-1 在多核 CPU 上能明显缩短训练时间。

随机森林最有价值的地方是 feature_importances_ 输出。它能告诉你模型实际依赖哪些特征做决策——如果 has_suspicious_word 的重要性排第一,说明钓鱼关键词确实是强信号;如果 url_len 的重要性接近 0,说明这个特征在树模型看来区分度有限,可以考虑删掉。这种可解释性在安全场景里非常重要,因为你最终要向别人解释「为什么判定某个网站是恶意的」。

4.3 DNN.py:手工特征 + 全连接网络,也能做分类

严格来说 DNN 属于深度学习,但这套资源里的 DNN.py 走的是「手工特征 + 全连接网络」的路子,输入仍然是 translate.py 生成的七维特征向量,本质上还是特征工程驱动,只是把分类器换成了神经网络。在特征维度很低的情况下,一个两层的全连接网络就足够了,不需要上卷积或注意力。

# DNN.py 核心训练逻辑 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model = Sequential([ Dense(64, activation='relu', input_shape=(X_train.shape[1],)), Dropout(0.3), # 随机丢弃 30% 神经元,抑制过拟合 Dense(32, activation='relu'), Dense(1, activation='sigmoid') # 二分类用 sigmoid 输出概率 ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) # 验证集 loss 连续 10 轮不下降就提前停止,恢复最优权重 early = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) model.fit(X_train, y_train, validation_split=0.2, epochs=100, batch_size=32, callbacks=[early], verbose=0)

DNN 的关键在 Dropout 和 EarlyStopping 的组合。这个数据集的特征只有七维,网络容量稍微给大一点就很容易把训练集背下来,Dropout(0.3) 是性价比最高的正则化手段。patience=10 的意思是验证集 loss 连续 10 个 epoch 没有改善就终止训练,restore_best_weights=True 保证返回的是历史最优权重而不是最后一轮的权重——很多初学乍练的项目都栽在这一步,只保存了最后一轮模型,结果训练 loss 降了但验证指标在回升。

4.4 三个模型怎么选:一张表看清边界

维度SVM随机森林DNN
是否需要标准化必须不需要建议做
训练速度(小样本)快快中等
可解释性中等强(特征重要性)弱
过拟合风险中低(集成+剪枝)高(需正则化)
调参重点C、gamma、核函数树数量、树深度网络宽度、Dropout、早停

我的习惯是先用随机森林跑一遍基线,拿到特征重要性和一个可靠的上限参考,再用 SVM 对比,最后用 DNN 看能不能再压几个点的准确率。三个模型在同一份特征上互相验证,比单独跑通一个就收工要扎实得多,答辩时也能讲出「为什么选这几个模型」的故事。

5. 避坑与排查:恶意网站检测项目里最常见的五个翻车点

5.1 现象:运行 translate.py 直接报错,找不到文件或读不出数据

原因:代码里写死的文件路径和 data.zip 解压后的实际文件名不匹配,或者 pandas 读取 xlsx 时遇到了格式不标准的 URL 字符串。

解决:先解压 data.zip 到项目根目录,用 os.listdir() 打印一下当前目录结构,确认原始数据表的准确文件名。如果遇到个别 URL 因为特殊字符导致读取失败,可以在 extract_features 外层套一个 try-except,跳过异常样本并打印 URL 便于排查。

5.2 现象:SVM 训练非常慢,一个 epoch 都等不到

原因:常见是两个——特征没标准化,导致 RBF 核在高尺度特征上反复计算;或者 C 值设得过大,模型在硬间隔上死磕。

解决:先确认 StandardScaler 已经执行,并且只用训练集 fit、测试集只做 transform;再把 C 从 1.0 开始,不要一上来就调成 100。特征是七维小样本的话,把 kernel 换成 linear 也能把训练时间从分钟级压到秒级。

5.3 现象:DNN 训练集准确率 99%,验证集只有 82%

原因:典型过拟合。七维特征配一个 64 宽的全连接层,容量是严重超配的,模型把训练集的噪声也记下来了。

解决:Dropout 从 0.3 提高到 0.5,隐藏层从 64/32 缩到 32/16,EarlyStopping 的 patience 从 10 降到 5。改完再看验证集指标,通常会有立竿见影的回升。

5.4 现象:模型把所有样本都预测成白名单,准确率还挺高

原因:类别不平衡。如果黑样本只占 10%,模型全猜白名单就能拿到 90% 准确率,看起来「很好」实际完全没用。

解决:看混淆矩阵而不是光看准确率。在训练时给 SVC 和 RandomForestClassifier 传入 class_weight='balanced',DNN 则在 fit 时设置 class_weight;评估指标改用 precision、recall、F1 的组合。

5.5 现象:换了一批新 URL 数据后,模型准确率暴跌

原因:特征分布漂移。translate.py 提取的特征是对原始数据集拟合的,如果新数据和训练集分布差异大,比如全是短链接或全是中文域名,之前学到的决策边界就不适用了。

解决:不要一次 train_test_split 就下结论,改用交叉验证;上线前先在新数据上跑一遍 translate.py 生成特征,对比关键特征的分布直方图,如果偏差明显就说明需要补充训练数据或重新做特征工程。

6. 进阶验证与落地:交叉验证、特征重要性与阈值调优

前面几章的流程能跑通,但如果你想让结果更扎实,建议在三个方向各前进一步:用交叉验证替代单次切分、用特征重要性指导迭代、用阈值调优替代默认的 0.5。

from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier # 5 折交叉验证,比单次 train_test_split 更稳定 model = RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42, n_jobs=-1) scores = cross_val_score(model, X, y, cv=5, scoring='f1') print('5 折 F1 均值:', scores.mean(), '标准差:', scores.std())

交叉验证的 F1 均值比单次准确率可信得多,标准差能告诉你模型在不同数据划分下的稳定性。然后是特征重要性——每次迭代后都看一眼 feature_importances_,把重要性低于 0.02 的特征剔除,重新训练对比。最后是阈值:

from sklearn.metrics import precision_recall_curve proba = model.predict_proba(X_test)[:, 1] precision, recall, thresholds = precision_recall_curve(y_test, proba) # 找一个兼顾精确率和召回率的阈值,而不是默认 0.5 for t in thresholds[::10]: idx = thresholds[::10].tolist().index(t) if precision[idx] > 0.85 and recall[idx] > 0.85: print('可用阈值:', round(t, 3), 'precision:', round(precision[idx], 3), 'recall:', round(recall[idx], 3))

安全场景里漏报的代价往往比误报高,所以我会刻意把阈值调低,比如 0.4 甚至 0.35,宁可多拦几个可疑站点,也不要放走真正恶意的。这是模型层面最后一步能做的优化。

之前我做过一个类似的 URL 分类项目,当时直接用了默认 0.5 阈值上线,结果漏掉了好几个特征分值只在 0.4 左右的恶意域名,复盘时才发现阈值调整能多捞回近 15% 的恶意样本。从那以后,我每次跑完分类模型都强制走一遍交叉验证加 PR 曲线分析,不再迷信单次切分和默认阈值。这条习惯救过我很多次,希望帮到你。

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

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

SA-SSD复现避坑指南:辅助网络、Loss计算与数据增强全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 2:49:09

STM32纯软件仿真入门:寄存器级外设建模与QEMU实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 2:48:51

MNIST手写数字识别实战:三种神经网络模型对比分析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 2:48:49

离线环境Docker与Docker Compose安装包选择与部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华