ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

机器学习驱动的URL恶意检测:特征工程与sklearn分类器实战

机器学习驱动的URL恶意检测:特征工程与sklearn分类器实战 简介针对恶意网址识别任务压缩包提供一套基于机器学习的完整检测实现主要面向安全方向学生、科研人员以及希望快速构建分类原型的开发者可应用于网络钓鱼、恶意软件分发、跨站脚本等场景的恶意网址初筛。方案以网址字符串为原始输入通过特征工程提取长度、域名、路径、参数等特征并借助sklearn库完成模型训练与分类判别配套实验数据存放于data文件夹。压缩包共24个文件、约48.4MB核心为11个Python脚本和8个CSV数据文件另有2个TXT词表、2张PNG可视化图及1个Markdown说明文档。脚本覆盖特征提取、数据划分、模型训练、单条测试、皮尔逊相关性分析、WHOIS查询、VirusTotal校验等环节CSV包含正常与恶意网址样本及Fishtank等公开来源数据TXT提供黑白名单关键词PNG展示词云与相关性图谱MD为项目说明。目前已有285人浏览学习适合直接复现实验也便于在此基础上扩展新特征或尝试不同分类算法。1. 基于机器学习的URL恶意性检测这份资源到底能帮你做什么如果你正在做 URL 恶意性检测你会发现最难受的不是模型选型而是拿到一批 URL 时不知道怎么在不访问页面的前提下快速判断它是不是恶意。传统黑名单滞后人工分析太慢而机器学习给了另一条路直接对 URL 字符串做特征提取然后交给 sklearn 分类器。这套资源就是干这个的它自带实验数据bad_urls.csv、fishtanktrain.csv、popular_web.txt 等还有完整的特征提取、模型训练、单条检测脚本以及相关性分析和词云可视化。对想入门安全机器学习的人它是一份能直接跑通闭环的样本对已经在做威胁情报的工程师也能拿来当特征工程参考。2. 特征提取把URL字符串变成模型能看懂的数字恶意 URL 检测的第一步不是你想象中那些高端 AI 模型而是把 URL 变成一行行数字。这里有个关键区别我们不做页面内容分析只基于 URL 字符串本身。因此特征设计的质量基本决定了模型的上限。很多人在这一步偷懒结果后面调参再久也没用。2.1 原始数据长什么样bad_urls.csv 与 popular_web.txt 的区别打开data目录你会发现几类文件恶意样本、正常样本、辅助特征表和外部工具脚本。先说样本文件理解它们比直接跑代码更重要。文件内容在我的工作流里的用途bad_urls.csv已知恶意 URL 列表核心负样本通常含 URL 和标签fishtanktrain.csv从 PhishTank 获取的钓鱼 URL补充恶意样本丰富攻击模式popular_web.txt常见正常网站域名正样本来源注意可能只有域名World_Top_500.csv全球访问量前 500 网站另一份正常样本缓解不平衡test_data/data.csv测试用 URL 集合模拟线上新数据的评估集从这些文件看正样本来源是知名网站负样本来源是已知恶意库。这带来一个隐含问题恶意样本往往有更长的路径和参数而正常样本很多是短域名模型很容易学到短 URL正常这种表面规律。我一般会先观察两类样本的url_length分布如果重叠太少就说明数据太理想化后面要小心过拟合。2.2 feature_extraction.py 里的特征设计思路从命名和常见工程习惯来看这个脚本的核心逻辑可以概括为解析 URL提取结构特征、统计特征和词表命中特征。这里给出一个常见的最小实现import re from urllib.parse import urlparse, unquote def extract_features(url: str) - dict: decoded_url unquote(unquote(url)) # 先解码避免 %XX 干扰 parsed urlparse(decoded_url) features {} # 结构特征 features[url_length] len(decoded_url) features[domain_length] len(parsed.netloc) features[path_length] len(parsed.path) features[path_depth] len([x for x in parsed.path.split(/) if x]) features[domain_num_digits] sum(c.isdigit() for c in parsed.netloc) # 统计特征 features[special_chars] len(re.findall(r[?%], decoded_url)) features[has_ip] 1 if re.match(r^\d{1,3}(\.\d{1,3}){3}$, parsed.netloc) else 0 features[suspicious_words] sum( 1 for w in [login, verify, secure, account, free] if w in decoded_url.lower() ) return features这里urlparse会把 URL 拆成scheme、netloc、path、query等部分unquote则是处理 URL 编码的关键。has_ip特征很重要因为直接使用 IP 地址的 URL 在合法站点里很少见却常见于攻击场景。special_chars统计?、、、%的数量反映 URL 中参数和编码的复杂度。suspicious_words是词表命中词表可以后续用词云图去扩充。实际项目里我会加入更多特征比如顶级域是否为常见钓鱼域.tk、.ml、.ga 等、路径中是否包含随机十六进制串、域名是否包含连字符等。但要注意特征不要一味追求多而是每加一个都做对比实验。我自己最常用的特征集大约 15 个再多反而容易让模型过拟合到训练集的历史模式上。2.3 数据切分data_split.py 如何保证训练集和测试集不串样本有了特征下一步不是直接训练而是先切分数据。data_split.py的目标是把原始 CSV 按比例分成splited_data/train.csv和splited_data/test.csv。这一步看起来简单但有个常见错误直接random.sample结果正负样本比例失衡测试集不能反映真实分布。更稳的做法是分层切分。假设你已经把标签列变成了 1 和 0import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(data/bad_urls.csv) X df[url] y df[label] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 )stratifyy是关键参数它让训练集和测试集中的恶意样本比例和原始数据集一致。random_state42保证实验可复现。但还有一个隐患恶意样本经常是同一批域名下的不同路径单纯随机切分可能让同一个域名的不同 URL 同时出现在训练和测试里造成评估虚高。我在自己做威胁检测项目时会额外加一步按域名去重from urllib.parse import urlparse df[domain] df[url].apply(lambda u: urlparse(u).netloc) # 按域名分组保证同一域名只出现在一个集合 unique_domains df[domain].unique() train_domains, test_domains train_test_split( unique_domains, test_size0.3, random_state42 ) train_df df[df[domain].isin(train_domains)] test_df df[df[domain].isin(test_domains)]这种做法会牺牲一些训练样本但换来的是评估结果可信。你可以在切分前先检查一下splited_data里是否存在跨集的域名如果发现大量重叠就说明这个项目原本的切分没有考虑域名级去重复现时要自己修正。3. 模型训练与评估use_sklearn.py 的完整流程特征准备完就到了项目名字里的主角机器学习分类器。use_sklearn.py是训练入口。它用的不是深度学习而是 sklearn 库里的经典算法。这一点对入门者是好事sklearn 模型接口统一调参空间清晰CPU 上就能跑数据量在几千到几万条时完全够用。3.1 为什么选 sklearn逻辑回归 vs 随机森林在 sklearn 里能做二分类的算法很多逻辑回归、决策树、随机森林、SVM 都行。在这个项目里我优先推荐先尝试逻辑回归和随机森林。逻辑回归可解释性极强你能直接看到每个特征的权重。对于安全运营场景有时候需要向领导解释为什么这条 URL 被判恶意权重比黑盒模型好讲。随机森林能捕捉非线性关系比如某个特征单独看没什么异常但组合起来很可疑。它对特征量纲不敏感不需要做标准化对异常值也相对鲁棒。如果不确定用哪个我的习惯是先跑一个逻辑回归作为基线再跑随机森林看测试集上的恶意样本召回率提升是否超过 2%。如果超过了就上随机森林没超过优先用逻辑回归因为部署简单线上推理也更快。3.2 训练脚本的解读从特征矩阵到分类报告use_sklearn.py的流程大致是读取切分好的训练数据 → 调用feature_extraction.py把每条 URL 变成特征向量 → 组装成特征矩阵 → 训练模型 → 在测试集上输出分类报告。import joblib import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report from feature_extraction import extract_features train_df pd.read_csv(data/splited_data/train.csv) test_df pd.read_csv(data/splited_data/test.csv) X_train train_df[url].apply(extract_features).apply(pd.Series) y_train train_df[label] X_test test_df[url].apply(extract_features).apply(pd.Series) y_test test_df[label] clf RandomForestClassifier( n_estimators300, max_depth12, min_samples_leaf2, class_weightbalanced, random_state42 ) clf.fit(X_train, y_train) y_pred clf.predict(X_test) print(classification_report(y_test, y_pred)) joblib.dump(clf, url_clf.joblib)apply(pd.Series)把特征字典拆成多列形成 DataFrame。这里要注意特征函数返回的键顺序就是列顺序只要保证训练和预测用同一个函数顺序就不会乱。n_estimators300是决策树数量太小容易欠拟合太大训练时间成倍增加。max_depth12限制树的深度防止模型记住训练集中的噪声。min_samples_leaf2要求叶节点至少 2 个样本同样是为了降低方差。class_weightbalanced是处理正负样本不平衡的偏方它会给少数类更高的权重在恶意 URL 场景里几乎必开因为恶意样本往往不到总量的 20%。3.3 实验结果怎么看准确率、召回率、AUC 的陷阱classification_report输出 precision、recall、f1-score 等指标。很多人只盯准确率看到 0.95 就觉得模型很牛。但在恶意 URL 检测里准确率高可能是假象如果恶意样本只占 10%模型把所有 URL 都判为正常准确率也是 90%。这时候召回率才是关键——恶意样本有多少被抓出来了。我的优先级排序是恶意类召回率 正常类召回率 准确率。漏报一个恶意 URL 可能导致真实安全事件而误报一个正常 URL 最多是用户烦一点。如果恶意类召回率很低先不要调参回到特征上看分布很可能是特征本身没有包含足够的信息。如果想更全面可以加一个混淆矩阵和 AUCfrom sklearn.metrics import confusion_matrix, roc_auc_score print(confusion_matrix(y_test, y_pred)) print(AUC:, roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1]))AUC 对样本类别不平衡不那么敏感能综合反映排序能力。但 AUC 高不代表阈值选得好实际拦截还是需要根据业务设定概率阈值这个在下一章会讲。4. 单条URL检测与闭环验证single_test.py 和外部工具训练只是第一步真正能用起来的是单条检测。single_test.py把加载模型 → 特征提取 → 预测串成一条线也是你最终部署时可以平滑改造成 API 的最小原型。4.1 single_test.py 的用法加载模型、提取特征、预测常见用法是python single_test.py https://example.com/path?param1。脚本内部逻辑如下import sys import joblib import pandas as pd from feature_extraction import extract_features def predict(url: str): clf joblib.load(url_clf.joblib) features extract_features(url) X pd.DataFrame([features]) prob clf.predict_proba(X)[0, 1] pred clf.predict(X)[0] return pred, prob if __name__ __main__: url sys.argv[1] pred, prob predict(url) print(f预测结果: {恶意 if pred 1 else 正常}恶意概率: {prob:.3f})这里我刻意用了predict_proba。实际部署时不建议直接用predict输出二值而是设一个业务阈值。比如恶意概率大于 0.7 判恶意0.3 到 0.7 之间标记为待人工审核。安全场景里宁可多给分析师看一眼也不要把恶意样本悄悄放走。我在多个项目里验证过阈值哪怕只是提高 0.1误报率都可能降一个量级代价是少量边界样本进入人工队列。4.2 用 VirusTotal 和 WHOIS 做交叉验证single_test.py给的是模型判断但模型会有误报。为了验证一条 URL 到底是不是钓鱼项目里还放了virus_total_check.py和whois_info.py。virus_total_check.py调用 VirusTotal 的 API查询这条 URL 被多少安全厂商标记为恶意。VirusTotal 聚合了 70 多家安全厂商的检测结果是安全圈公认的交叉验证渠道。如果你是批量验证注意免费 API 有频率限制大概一分钟只能查 4 次。我一般会在循环里加time.sleep(15)同时用try/except捕获超时避免因为单条失败中断整个流程。whois_info.py查询域名的注册信息。恶意域名经常是刚注册的注册时长很短注册者信息模糊。WHOIS 信息可以作为特征但这里更推荐作为人工研判的辅助。当模型输出恶意概率 0.6 且 WHOIS 显示域名注册不到一个月时基本可以判定是一起早期钓鱼。4.3 完整流程串起来从训练到单条检测的工程习惯当你把data_split.py、use_sklearn.py、single_test.py串起来就形成了一个完整的闭环原始数据 → 切分 → 特征提取 → 训练 → 保存模型 → 单条预测。这个流程本身很健康。但我建议在这个闭环上再加两步第一步把single_test.py封装成一个纯函数输入 URL 输出概率方便以后接 Flask 或 FastAPI。第二步每训练一个新模型就在一个固定的验证集上跑一遍并记录结果防止模型悄悄退化。我自己会把验证集单独放在test_data/目录训练时永远不碰它。5. 避坑指南URL编码、样本不平衡、特征泄漏与其它常见问题这个部分是从多次复现这个项目时最容易翻车的地方。我挑了 5 个几乎每个人都会遇到的坑按现象 → 原因 → 解决写清楚。5.1 URL 解码问题现象有些 URL 看起来很长很可疑但模型把它判成了正常。尤其你拿dps://p?urlhttps%3a%2f%2f...这种经过百分号编码的链接去测模型基本反应不过来。原因特征提取时直接对原始字符串操作%3a、%2f这些编码字符被当成普通字符统计导致 URL 的真实路径隐藏在编码后面。攻击者很擅长这么做把https://evil.com编码成https%3a%2f%2fevil.com或者嵌套多层编码。解决在特征提取前先做标准解码。常见做法是用urllib.parse.unquote解码一次如果字符串里还有%再解一次最多解码两层。注意训练和测试必须使用同一个解码流程不能训练时解码而预测时不解码。我在extract_features的第一步固定写decoded_url unquote(unquote(url))这样恶意样本的召回率通常有肉眼可见的提升。5.2 样本不平衡导致模型全猜安全现象训练完准确率 0.97但看分类报告恶意类的召回率只有 0.05。也就是说模型根本没学会识别恶意样本只是把所有样本都猜成了正常。原因训练集里恶意样本占比太低。比如bad_urls.csv只有几百条而正常样本有上万条决策树为了降低整体损失会偏向多数类。accuracy在这个场景下会骗人。解决先用value_counts()检查标签分布。如果恶意样本占比低于 20%可以做三件事一是收集更多恶意样本来源比如把fishtanktrain.csv合并进来二是在RandomForestClassifier里设置class_weightbalanced三是采样用resample从正常样本中抽取和恶意样本相同数量的样本但注意不要对测试集做同样的操作。我最推荐先加class_weight因为简单且不容易引入偏差。5.3 特征泄漏来自未来信息的假高分现象模型在测试集上跑出 0.99 的 AUC你兴高采烈地部署上线结果线上误报率高得离谱过两天就被人投诉。原因你可能在特征里加了一些来自未来的信息。比如 WHOIS 注册时间——如果训练数据用的是某一天的快照但线上预测时用的是当前注册信息分布完全不同。更隐蔽的是如果你用整个数据集计算某个统计量等于把测试集的信息也泄漏到了特征里。解决严格区分时间切片。训练数据只使用某个时间点之前的信息测试数据使用之后的信息。对于popular_web.txt这类外部列表它本身也会变化最好定期更新并记录版本。如果不确定有没有泄漏做一个实验把标签随机打乱再重新训练如果新模型的分数依然很高说明特征里有泄漏——打乱标签后 AUC 应该接近 0.5。5.4 测试集里包含训练集的 URL现象模型评估指标很好但换一批全新 URL 就不行了。原因切分数据时直接用train_test_split同一个 URL或同一个域名下的多个路径同时出现在训练集和测试集里。模型记住了 URL 本身而不是它的模式。这在数据收集过程中很常见恶意样本可能被多个源重复收录。解决切分前先按 URL 去重更进一步按注册域名去重。把同一域名的所有样本放到同一个集合这样测试集才是真正没见过的样本。你可以自己写一个域名分组切分的函数替换掉默认的train_test_split。5.5 编码与特殊字符的坑现象single_test.py检测中文参数或国际化域名时报错或者直接误判。原因URL 里有非 ASCII 字符比如https://恶意.com/xxPython 的urlparse在这些字符上表现不稳定正则匹配也可能因编码混乱出错。另外浏览器地址栏显示中文但实际传输的是 punycode。解决在处理 URL 前先用idna编码转换域名部分。urlparse得到netloc后如果包含非 ASCII 字符用encode(idna).decode()转成 ASCII。路径部分用urllib.parse.quote统一编码。这样特征提取就在一个标准格式上操作。我会写一个normalize_url函数把纯 IP、国际化域名、大小写差异全部规整再交给特征提取这是性价比最高的预处理步骤。6. 进阶用皮尔逊相关性分析与词云迭代特征到这里基础闭环已经通了。但如果你想把这个项目做得更好比如用于论文或面试展示强烈建议看pearson_correlation.py和badword_cloud.png。它们不是炫技而是给你一套迭代特征的依据。6.1 pearson_correlation.py 怎么用pearson_correlation.py计算每个特征与标签之间的皮尔逊相关系数。相关系数绝对值越高说明该特征与是否恶意的线性关系越强。输出会是一张热力图或表格特征名与标签相关系数url_length0.37has_ip0.29suspicious_words0.22domain_length0.08看到has_ip和url_length相关性高就知道这两类特征值得保留如果某个特征系数接近 0比如scheme是否https单独看没意义。但注意皮尔逊相关系数只衡量线性关系非线性信号它看不到。相关性低不代表特征没用只能说明它不适合线性分类器。我用这个表做排除而不是做最终决策。6.2 badword_cloud.png 告诉你的信息词云图把恶意 URL 里出现频率高的词放大展示。如果login、secure、verify、account这些词非常大说明攻击者喜欢把这类词嵌在钓鱼 URL 里。这些词可以直接扩充到suspicious_words特征表里。词云还可以给你灵感比如出现free、click就可以把它们加进特征函数然后跑对比实验。6.3 特征迭代的通用套路从简单到复杂每次只变一个变量不要一口气加 20 个特征。我的做法是拿现有特征跑基线记录恶意类召回率和误报率然后每次只加一个特征或调整一个特征定义比如把url_length从原始长度改为对数值再跑一次比较结果。有提升就保留没提升就回退。这样你最终留下的每一个特征都是经过验证的。我在做类似项目时会准备一个结果记录表包含日期、特征列表、模型参数、测试集 ID、召回率、误报率。这个习惯帮过大忙有一次我发现某个特征在 7 月有效但 9 月完全失效一查才发现词云对应的攻击团伙已经换了套路。从那以后我每次迭代特征都强制走一遍先记录基线再改一个变量的流程再也没有被表面上的模型优化骗过。希望这套流程和这些坑能帮你在 URL 恶意性检测上少走几步弯路。本文还有配套的精品资源点击获取
返回列表