
简介基于传统机器学习的恶意网站检测算法源码与项目说明包面向计算机、人工智能、大数据等专业正在做课程设计或毕业设计的学生适合具备一定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?token12345 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_size0.3, stratifyy, random_state42 ) X_val, X_test, y_val, y_test train_test_split( X_temp, y_temp, test_size0.5, stratifyy_temp, random_state42 ) # 3. 选择模型先跑随机森林保留逻辑回归做对比 model RandomForestClassifier( n_estimators300, # 树的数量太多会拖慢推理 max_depth16, # 限制深度防止过拟合 min_samples_leaf4, # 叶节点最少样本数平滑边界 class_weightbalanced, # 处理样本不平衡的关键参数 n_jobs-1, # 使用全部 CPU 核心 random_state42 ) 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_, indexfeature_names) print(importance.sort_values(ascendingFalse).head(10))这段代码里几个参数是踩过坑之后才固化下来的。class_weightbalanced是在没有做重采样时最简单的处理样本不平衡的方法它让分类器自动把少数类的权重调高。如果恶意样本占 2%这个参数几乎必须加否则模型会学到“全部判正常”的退化策略。min_samples_leaf4是一般经验值太小容易让树记住单个样本太大又会让模型太钝先固定为 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_hits4和has_ip1你就能把这条告警解释成“该站点直接使用 IP 并且命中多个钓鱼关键字极大概率是钓鱼站”。这对一线运营人员非常有用他们不会盲信一个纯粹的黑匣子有了特征解释人工二次确认的效率会翻倍。最后再谈一个习惯问题。传统机器学习做恶意网站检测最大的优势就是每次迭代都能通过特征重要性回答“模型到底在看什么”。模型上线后我习惯每两周导出一次特征重要性排行把它和业务侧的威胁情报事件做对照如果某个特征的重要性突然上升往往意味着攻击者开始集中使用某种绕过手法。这个信号比盯着模型指标更有预警价值。希望这篇笔记能帮你在恶意网站检测这个方向上少走一圈弯路把传统机器学习的价值真正发挥出来。本文还有配套的精品资源点击获取