☰
传统机器学习在恶意网站检测中的应用:从特征工程到模型落地
2026/9/25 4:43:41 网站建设 项目流程

简介:基于传统机器学习的恶意网站检测算法源码与项目说明包,面向计算机、人工智能、大数据等专业正在做课程设计或毕业设计的学生,适合具备一定Python与机器学习基础、希望直接复用可运行代码的读者。源码已完成调试,能够直接运行,并配有项目说明,便于快速定位数据处理、模型训练与评估等关键环节。压缩包共7个文件,以5个Python脚本为主,分别覆盖类型列表处理、数据转换以及SVM、DNN、随机森林等算法实现;另含一份Markdown项目说明和独立zip数据包,便于对照阅读与离线训练。整体大小约3.31MB,轻量但环节完整,从特征构造到分类器对比均有代码支撑。目前已有148人学习下载。整体比较适合作为传统机器学习文本分类实践的项目基底,既能用于复现恶意网站检测流程,也便于在此基础上扩展特征或替换模型,支撑课程报告、期末大作业等后续工作。

1. 为什么恶意网站检测又绕回了传统机器学习

恶意网站检测这个方向,前几年几乎被深度学习刷屏了,仿佛不用神经网络就不配叫检测系统。但你真跑到一线去落地就会发现,绝大多数安全运营团队手里并没有海量打标数据,也扛不住每轮迭代都要烧显卡训练的开销。传统机器学习在这类场景里不但没退场,反而因为特征可解释、训练成本低、上线快,成了很多公司做 URL 检测时的第一选择。

这个标题所指的方案,就是用逻辑回归、随机森林这类经典算法,对手工提取的 URL 特征、域名特征、页面内容特征做分类,判断一个网址是正常站点还是恶意站点。它不依赖深度神经网络那种端到端的黑匣子,而是把安全经验直接编码成特征。适合谁?适合有少量标签样本、需要快速交付基线模型的安全工程师,也适合想搞懂检测原理、不想被花哨框架带走的学生和研究者。本文就把这套方案从数据准备到模型训练,再到上线验证的完整路径拆开讲清楚,连踩坑的细节一起给你。

2. 数据与特征:恶意网站检测的样本从哪来、特征怎么设计

2.1 数据来源与标签口径:先定标准再谈算法

做任何监督学习,标签质量决定模型上限。恶意网站检测的标签不是“非黑即白”那么简单,常见的安全数据源有三类。第一类是公开情报源,比如 PhishTank、OpenPhish 这类社区维护的钓鱼网站列表,适合做初始样本;第二类是自有流量日志里人工确认过的违规站点,这个质量最高但规模往往不大,通常只有几千条;第三类是从威胁情报平台上购买的恶意 URL 数据,覆盖面广但要自己过滤误报。

标签口径要想清楚:是“URL 层面”打标,还是“域名层面”打标?很多新手在这里翻车。同一个域名下可能同时存在恶意子路径和正常页面,如果给整个域名打标恶意,模型会学到很多噪声;反过来也一样。我一般建议按 URL 粒度去打标,宁可样本少一点,也不要把标签搞脏。

标签平衡度也要提前看。恶意样本和正常样本的比例在实际业务里往往在 1:50 甚至更低,训练时不能拿原始比例直接训,后面章节会讲处理办法。这里先提醒一句:先花两天时间把样本整理干净,比后面调两周参数都值。

2.2 特征设计:手工特征为什么还能打

传统机器学习做恶意网站检测,核心工作不在模型,而在特征。特征大体分三块:URL 结构特征、域名属性特征、页面内容特征。

URL 结构特征看的是字符串本身,比如 URL 总长度、路径层级数、是否包含 IP 地址、是否有可疑关键字(比如“login”、“verify”、“update”这类钓鱼高频词)、数字字符占比、特殊字符占比。这些特征有个共同特点:提取成本极低,一条正则就能搞定,而且具有较强的可解释性。攻击者想绕过,得付出改写 URL 的代价。

域名属性特征包括域名年龄(WHOIS 里注册了多久)、注册商是否常见、域名是否是顶级域名(比如顶级域名的子域下很少直接出现恶意站)、DNS 解析是否解析到已知的 IP 段、域名是否出现在第三方恶意域名黑名单里。这些特征需要外接数据源,比如 WHOIS 服务和 DNS 解析接口,但价值非常高。

页面内容特征稍微重一点,需要实际去拉取页面正文。可以统计页面标题是否与品牌相关、页面里是否包含登录表单、重点域名出现的次数、页面 Jaccard 相似度等。这类特征能抓到“批量复制钓鱼页面”的情况,同一套钓鱼模板改改域名就上线,页面上只有 logo 和登录框,正常站点不会有这种结构。

手工特征的取舍原则是:宁缺毋滥,但要覆盖不同的“攻击视角”。攻击者改 URL 结构,域名特征就能兜底;攻击者新注册域名,WHOIS 年龄特征就能兜底;攻击者复用页面模板,内容特征就能兜底。

2.3 特征提取代码:从 URL 字符串到向量的一步到位

下面给一段特征提取的 Python 代码,覆盖 URL 结构特征和部分基础域名特征。这是常见的做法,源码包里也是这么组织的,按函数拆开便于后续扩展。

import re import tldextract from urllib.parse import urlparse # 常见钓鱼/恶意关键字,可自行扩充 SUSPICIOUS_KEYWORDS = [ "login", "verify", "account", "update", "confirm", "wallet", "secure", "signin", "webscr" ] def extract_url_features(url: str) -> dict: # 1. 基础解析 parsed = urlparse(url) ext = tldextract.extract(url) # 返回 subdomain, domain, suffix # 2. URL 结构特征 path_len = len(parsed.path) # 路径长度 query_len = len(parsed.query) # 查询参数长度 num_digits = sum(c.isdigit() for c in url) # 数字字符占比的分母 num_tokens = len(re.findall(r'[a-zA-Z]+', url)) # 字母块数量 has_ip = bool(re.search(r'\d+\.\d+\.\d+\.\d+', parsed.netloc)) # 是否直接使用 IP depth = len([x for x in parsed.path.split('/') if x]) # URL 路径层级数 # 3. 可疑关键字命中次数 keyword_hits = 0 for kw in SUSPICIOUS_KEYWORDS: keyword_hits += len(re.findall(kw, url.lower())) # 4. 域名属性特征的基础部分 domain = f"{ext.domain}.{ext.suffix}" if ext.suffix else "" domain_len = len(domain) subdomain_len = len(ext.subdomain.split('.')[0]) if ext.subdomain else 0 return { "path_len": path_len, "query_len": query_len, "digit_ratio": num_digits / max(len(url), 1), "num_tokens": num_tokens, "has_ip": int(has_ip), "url_depth": depth, "keyword_hits": keyword_hits, "domain_len": domain_len, "subdomain_depth": subdomain_len, } # 示例调用 sample_url = "http://login-verify.example.com/account/update?token=12345" print(extract_url_features(sample_url))

这段代码的核心思路是把一条 URL 映射成一个定长字典,后续交给 DictVectorizer 或直接拼成向量训练。参数说明:SUSPICIOUS_KEYWORDS这个列表不要一上来就堆五六十个词,先放十个左右命中率高的,等模型迭代时再看漏报和误报去增删;digit_ratio用占比而不是绝对计数,是为了避免 URL 长短带来的偏差;has_ip是二值特征,直接访问 IP 的 URL 恶意概率显著高于域名访问,这个特征值得单独保留。

tldextract库在提取主域和子域时很实用,它会自动处理带后缀的域名如com.cn。如果源码包里没有单独安装,建议加入 requirements;没有这个库时也可以用urllib.parse自己截取,但边界情况容易处理不好。域名年龄、DNS 解析这些需要外部调用的特征,建议单独封装一个类,避免在特征提取循环里频繁做阻塞式网络请求。

2.4 特征矩阵落地:用配置文件管理特征,别写死

特征提取写好之后,下一步是把特征定义和提取逻辑分开管理。常见做法是维护一个特征清单 JSON,程序启动时加载,这样新增特征不用改主代码,只加一行配置就行。

{ "features": [ "path_len", "query_len", "digit_ratio", "num_tokens", "has_ip", "url_depth", "keyword_hits", "domain_len", "subdomain_depth" ] }

这样做的好处有两个:一是训练和预测时特征顺序天然一致,减少了“训练时用了 9 个特征、预测时只传 8 个”的低级事故;二是后续做特征筛选时,只需要在配置文件里注释掉对应行,模型代码不用动。这个习惯看起来简单,但在真实项目里能省很多排查时间。

特征全部提取完之后,保存成 CSV 或者直接用np.save存成数组文件。不要在这个环节就想上数据库,本地文件足够支撑几万到几十万的样本训练。如果样本量超过几十万,再考虑用 parquet 格式或者直接抽到 HDFS 上训练,那属于大数据工程的范围,和算法本身是两回事。

3. 训练与调参:用 scikit-learn 跑通第一版检测模型

3.1 模型选型:逻辑回归、随机森林还是梯度提升

特征工程做好之后,模型的选择反而没那么玄。传统机器学习里适合恶意网站检测的主要有三类模型,各有适用场景。

逻辑回归是首选基线。它训练极快,输出概率可以被直接解释成“置信度分数”,而且权重能直接映射到每个特征的重要性上。缺点是拟合能力有限,特征和标签之间的关系如果高度非线性,逻辑回归容易欠拟合。但作为第一版基线,它最大的价值是把整个数据链路跑通。

随机森林对特征缩放不敏感,也不用过多调参,能自动处理特征交互。在几千到几万样本的规模下,随机森林的效果通常明显好于逻辑回归。缺点是模型体积偏大,预测 10 万条 URL 耗时比逻辑回归慢一个数量级,而且对区间外数据的表现不稳定——训练时没见过的 URL 结构,它可能给出非常离谱的置信度。

梯度提升(如 XGBoost、LightGBM)是传统机器学习里的高阶选项,在公开比赛和工业场景里常年表现靠前。它对特征工程的要求比前两者低一些,因为树模型能自己找分裂点。但代价是调参空间变大,随便跑一下不难,但要调好需要理解学习率、树深度、正则项之间的关系。如果团队里没人深入调过 GBDT,建议选随机森林。

下面给出一个实际对比表,按训练耗时、推理耗时、可解释性、易调参程度四个维度去看:

模型训练耗时推理耗时可解释性调参难度适用场景
逻辑回归极快极快极高(权重可解释)低,调 C 值即可基线模型、对延迟极敏感的服务
随机森林快中等(多棵树取投票)较高(特征重要性)低,树深和棵树影响大中小样本、追求稳的效果
LightGBM中等快中等(SHAP 可解释)高,参数组合多大样本、效果优先的场景

实际项目里,我一般先用逻辑回归跑通流程,然后换随机森林看效果上限。如果随机森林比逻辑回归的准确率提升了不到 1 个百分点,那说明特征工程已经饱和了,问题不在模型,该去加特征、洗样本。

3.2 训练脚本:从数据切分到评估指标的一次性成型

直接上一份能跑的完整训练脚本,这算是“源码+项目说明”里核心算法的部分。代码结构不复杂:读数据、切分、训练、输出指标。注意这里用了分层抽样来保持正负样本比例一致。

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, StratifiedKFold from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import (roc_auc_score, classification_report, precision_recall_curve, auc) # 1. 读取特征矩阵 # 读之前保证特征列和标签列分离,特征顺序以特征清单为准 df = pd.read_csv("url_features.csv") X = df.drop(columns=["label", "url"]).values # 特征列 y = df["label"].values # 标签列,0 表示正常,1 表示恶意 # 2. 分层切分:训练 70% / 验证 15% / 测试 15% X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42 ) # 3. 选择模型:先跑随机森林,保留逻辑回归做对比 model = RandomForestClassifier( n_estimators=300, # 树的数量,太多会拖慢推理 max_depth=16, # 限制深度,防止过拟合 min_samples_leaf=4, # 叶节点最少样本数,平滑边界 class_weight="balanced", # 处理样本不平衡的关键参数 n_jobs=-1, # 使用全部 CPU 核心 random_state=42 ) model.fit(X_train, y_train) # 4. 验证集评估:AUC 和 PR-AUC 都看 proba_val = model.predict_proba(X_val)[:, 1] # ROC-AUC:对类别不平衡不敏感,用于全局排序能力评估 roc_auc = roc_auc_score(y_val, proba_val) # PR-AUC:恶意样本占比低时,PR 曲线更能反映模型真实价值 precision, recall, _ = precision_recall_curve(y_val, proba_val) pr_auc = auc(recall, precision) print(f"验证集 ROC-AUC: {roc_auc:.4f}") print(f"验证集 PR-AUC: {pr_auc:.4f}") # 5. 按 0.5 阈值输出混淆矩阵摘要 pred_val = (proba_val >= 0.5).astype(int) print(classification_report(y_val, pred_val, target_names=["正常", "恶意"])) # 6. 特征重要性输出,排第一的特征值得你关注 feature_names = df.drop(columns=["label", "url"]).columns importance = pd.Series(model.feature_importances_, index=feature_names) print(importance.sort_values(ascending=False).head(10))

这段代码里几个参数是踩过坑之后才固化下来的。class_weight="balanced"是在没有做重采样时最简单的处理样本不平衡的方法,它让分类器自动把少数类的权重调高。如果恶意样本占 2%,这个参数几乎必须加,否则模型会学到“全部判正常”的退化策略。min_samples_leaf=4是一般经验值,太小容易让树记住单个样本,太大又会让模型太钝,先固定为 4,之后用网格搜索微调就行。

阈值 0.5 只是初始默认值,不代表业务最优。恶意网站检测的阈值选择要看业务承受力:宁可放走一些可疑样本还是宁可多拦截一些正常站点?这个取舍在代码层面就是用precision_recall_curve的输出去找“recall 不低于 90% 时 precision 最高”的那个点,而不是死盯 0.5。

3.3 阈值选择与调参边界:不要盲目追求准确率

传统机器学习里一个常见的误区是把准确率当成唯一指标。在恶意网站检测这个场景,正常样本远多于恶意样本,一个全部预测为“正常”的模型,准确率也能高达 98%。这显然是废的。所以评估时至少要同时看召回率和 PR-AUC。召回率代表“恶意样本被抓住的比例”,安全场景里通常要求召回率不低于 90%,然后在这个约束下尽量把精确率做高。

调参边界也要说清楚:不要一上来就用网格搜索穷举几百组参数,先固定一个基线,然后一次只动一个变量。比如先固定树深度为 16,把树的数量从 100 加到 500,看验证集 AUC 的变化;AUC 不再明显上升,就说明树的数量已经够了。然后固定树的棵树,再扫一遍min_samples_leaf,取验证集上 PR-AUC 最高的那个值。整个过程控制在十几次训练以内。

还有一个容易忽略的环节是特征标准化。随机森林和梯度提升不需要标准化,但如果你要切换到逻辑回归或者 SVM,必须先对连续特征做标准化(比如StandardScaler),否则数值范围大的特征会主导权重更新。注意标准化必须在训练集上 fit,之后在验证集和测试集上只做 transform,不能把测试集数据混进来一起 fit,否则会造成数据泄漏,验证结果虚高。

4. 避坑清单:样本过期、标签污染与阈值漂移的典型翻车现场

4.1 样本过期:模型训练完就过时的根本原因

现象:模型上线第一周效果很好,一个月后漏报率明显上升,新出现的恶意站点几乎全部漏掉。

原因:恶意网站的生命周期很短,很多钓鱼网站活跃时间不超过 48 小时。训练集里的样本大多是“已经发生过”的恶意 URL,攻击者在不断注册新域名、生成新 URL 结构,旧特征分布和新样本分布逐渐产生偏差。机器学习领域管这叫概念漂移,安全领域里就是黑产在变着花样绕过规则。

解决:建立样本更新的流水线,至少每周增量补充一次新恶意样本进去,重新训练模型。如果业务要求更高,可以按天做增量训练。不要指望一个模型用半年,这在恶意网站检测场景下是个美好愿望。更接地气的做法是同时维护一个在线规则集(比如域名黑名单、已知攻击者 IP),让规则集承担“最近 24 小时内新攻击”的拦截,模型承担长尾检测,两者互补。

4.2 标签污染:你以为的正样本其实是误报

现象:模型在验证集上 PR-AUC 到 0.98,上线后误报率却高得惊人,大量正常业务域名被判为恶意。

原因:训练标签来源本身包含了误报。公开的恶意 URL 情报源用的是自动化检测,它们自己也会有误报。比如某家企业域名被第三方标成恶意,但其实是同行恶意投诉;或者 URL 里有测试专用子路径被平台抓取后误判,这些都会把“正常”标签污染成“恶意”。模型并不是学错了,而是忠实地学进了你给的错误标签。

解决:在训练之前做标签清洗,至少抽 500 条“恶意”样本人工过一遍,看有多少实际是正常的。如果人工确认后发现超过 5% 是误报,那必须要改数据源或换个更可靠的情报源。清洗后的样本要单独存放,不要和原始数据混在一起。代价是每次更新样本时都要花时间做清洗,这是安全团队逃不掉的活。

4.3 阈值漂移:模型没变,预测分布却悄悄变了

现象:模型还是同一个模型,代码也没改,但线上的“恶意概率平均值”从 0.1 缓慢涨到 0.3,导致超过阈值被判成恶意的站点数量暴增。

原因:特征分布发生了变化,而不是模型退化。比如某个时间段攻击者集中使用某种 URL 模板,模板里的关键字命中率升高;或者新上线的业务系统大量使用带verify的服务,导致正常 URL 的keyword_hits特征均值上升。模型看见“更可疑”的特征,自然给出更高的概率。

解决:监控线上预测分数的分布。每天统计predict_proba输出分数的均值、95 分位数,画一条趋势线,设定告警。分数分布出现异常跳变时,优先去查特征分布的变化,而不是急着重新训练。这个排查思路能省很多无效训练时间。诊断脚本可以写得很简单:每天把预测分数和关键特征均值存一份日志,按天对比。

4.4 特征泄漏:验证集分数虚高的常见原因

现象:验证集 PR-AUC 高达 0.99,模型一上线立刻变成废柴,漏报误报双高。

原因:特征构造时混入了和标签直接相关的信息。最典型的两个场景:一是把“该 URL 是否在 Virustotal 上被标记”当成特征,而训练样本的标签本身也来自类似的公开情报,特征和标签高度同源,模型等于在背答案;二是特征提取时用了整个 URL 做统计,但标签是域名级的,同一个域名下的多个 URL 训练和测试时被抓进两边,相当于模型看到了训练集里的相似样本。

解决:做特征清单审查,凡是“能直接查到标签”的特征一律删除。时间类特征也要注意,比如“该域名首次出现的时间”在训练时可能有效,但如果测试样本都是新出现的域名,这个特征就变成了全 0,没有任何区分度。判断特征有没有泄漏的方法很简单:单独拿这个特征去看和标签的相关性,如果单特征 AUC 超过 0.95,它大概率在泄漏。

5. 上线前的最后一步:用回归测试与特征解释做模型体检

模型训练完只是开始,落地部署前至少要完成一项工作:回归测试。把历史上的恶意样本、正常样本各抽一批固定下来做成回归集,每次更新模型后都跑一遍,确认新模型不会在旧样本上开倒车。这一步逻辑上相当于给模型做防退化体检,传统机器学习里回归测试做得越规范,后续迭代越省心。

回归集怎么建?建议按时间维度分成两部分:一部分是最近一个月的样本,用于检验模型对当前攻击手法的敏感度;另一部分是三个月的存量样本,用于检验模型是否保留了对旧攻击手法的识别能力。每次训练的模型在这两个子集上分别计算召回率,任何一边下降超过 2 个百分点,就需要查清原因才能上线。这个标准不是拍脑袋定的,而是基于“模型迭代最怕看不到回退”这个实际教训——不设标准,你永远不知道新模型是不是在捡了芝麻丢了西瓜。

特征解释可以借助shap库。假设随机森林已经训练好,在测试集上跑一下shap.TreeExplainer,它能告诉你每一个特征对每一条预测的贡献是正向还是负向。比如某条恶意样本被判 0.95 的恶意概率,关键驱动特征是keyword_hits=4和has_ip=1,你就能把这条告警解释成“该站点直接使用 IP 并且命中多个钓鱼关键字,极大概率是钓鱼站”。这对一线运营人员非常有用,他们不会盲信一个纯粹的黑匣子,有了特征解释,人工二次确认的效率会翻倍。

最后再谈一个习惯问题。传统机器学习做恶意网站检测,最大的优势就是每次迭代都能通过特征重要性回答“模型到底在看什么”。模型上线后,我习惯每两周导出一次特征重要性排行,把它和业务侧的威胁情报事件做对照:如果某个特征的重要性突然上升,往往意味着攻击者开始集中使用某种绕过手法。这个信号比盯着模型指标更有预警价值。

希望这篇笔记能帮你在恶意网站检测这个方向上少走一圈弯路,把传统机器学习的价值真正发挥出来。

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

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

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

立即咨询