多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于启发式特征的钓鱼网站检测:Python特征工程与模型训练实战

基于启发式特征的钓鱼网站检测:Python特征工程与模型训练实战 简介这份资源是面向计算机相关专业学生、教师及企业员工的Python启发式特征钓鱼网站检测系统包含完整源码、数据集与详细文档可用于毕业设计、课程设计、作业或项目演示。系统通过分析域名、内容、行为与安全性等特征利用预设启发式规则判断网站是否为钓鱼站点并能检测未被数据库记录的新兴钓鱼网站具备一定自我学习与更新能力。源码采用Python开发涵盖网络请求处理、网页内容解析、特征提取与规则匹配等模块。压缩包共5个文件以py源码、md说明文档、html页面及备份文件为主整体约16KB结构精简便于快速理解与二次开发。目前已有46人学习下载。读者可获得可运行的检测系统实现、配套数据集与设计文档用于掌握网络安全领域特征工程与规则匹配的实践思路。1. 钓鱼网站检测为什么不能只靠黑名单做安全方向毕业设计时很多人第一反应是拉一个 URL 黑名单命中就拦截。这个思路在演示阶段能跑通但放到真实流量里很快就崩了新注册的钓鱼域名存活时间常常只有几小时到几天黑名单更新永远慢半拍。我在某次内部演练里见过一个仿冒登录页从上线到被举报不到四十分钟等黑名单同步过去攻击者已经换了一批域名。所以真正能拿得出手的方案得从 URL 本身的特征出发做判别而不是依赖“这个域名有没有被记录过”。这就是启发式特征驱动的思路把 URL 字符串结构、域名构成、路径关键词、页面表单行为等可量化信号抽出来喂给分类模型让模型学会“长得像钓鱼”的统计规律。Python 在这个方向上是主力工具特征工程、模型训练、接口封装一条链路都能覆盖。这篇笔记面向正在做安全类毕业设计、或者想搭一套可演示钓鱼检测原型的同学。我会把特征怎么选、数据怎么标、模型怎么训、接口怎么封、坑在哪按能复现的顺序讲清楚。你不需要先有安全背景但得会基本的 Python 和 pandas。2. 启发式特征体系怎么搭从 URL 字符串到页面行为2.1 为什么特征工程比换模型更值得花时间钓鱼检测这类任务数据分布偏、正负样本不均衡、攻击手法还在变。你换一个更强的模型提升往往只有一两个点但你把特征从“只有 URL 长度”扩到“域名熵值 敏感词命中 子域名层级 是否 IP 直连”效果可能是十几个点的差距。我一般会先把特征分成四组来管理词法特征URL 总长度、host 长度、路径深度、参数个数、特殊字符、-、_、%计数、数字占比。域名特征顶级域名类型、子域名层数、是否使用 IP、域名熵值、是否包含品牌词变体。内容特征页面标题是否含“登录/验证/账户”、表单 action 是否指向第三方域、是否有密码输入框、外链比例。行为特征跳转次数、是否弹窗索要凭证、资源加载是否大量来自陌生域。这四组里词法和域名特征可以纯离线从 URL 算出来速度快、可解释内容特征需要抓页面成本高但判别力强。毕业设计里我建议先做前两组把基线跑稳再按需加内容特征。2.2 用 Python 抽第一批可解释特征下面这段代码是我常用的最小特征抽取器输入一条 URL输出一个字典。它不依赖任何外部服务纯字符串运算方便你先把流程跑通。import re import math from urllib.parse import urlparse from collections import Counter def shannon_entropy(s: str) - float: 计算字符串的香农熵用来衡量域名随机程度 if not s: return 0.0 counts Counter(s) length len(s) return -sum((c / length) * math.log2(c / length) for c in counts.values()) def extract_url_features(url: str) - dict: parsed urlparse(url) host parsed.netloc.split(:)[0] # 去掉端口 path parsed.path or query parsed.query or # 敏感词表实际项目里可以外置成配置文件 sensitive_words [login, verify, account, secure, update, confirm] full url.lower() features { url_len: len(url), host_len: len(host), path_depth: path.count(/), query_param_count: len(query.split()) if query else 0, digit_ratio: sum(c.isdigit() for c in url) / max(len(url), 1), special_char_count: len(re.findall(r[\-_%], url)), host_entropy: shannon_entropy(host), subdomain_depth: max(host.count(.) - 1, 0), is_ip_host: int(bool(re.match(r^\d{1,3}(\.\d{1,3}){3}$, host))), sensitive_hit: int(any(w in full for w in sensitive_words)), has_at_symbol: int( in url), https_flag: int(parsed.scheme https), } return features逻辑说明shannon_entropy用来量化域名的随机性钓鱼域名常用随机字符串规避检测熵值会偏高subdomain_depth统计子域名层数攻击者常用多级子域伪装成正规路径is_ip_host直接判断是否用 IP 访问正规站点极少这么干。参数方面sensitive_words列表建议根据你的数据集词频动态调整不要照抄digit_ratio分母用了max(len(url),1)防止空串除零。2.3 特征归一化和缺失值处理抽出来的特征量纲差异很大url_len可能上百is_ip_host只有 0/1。树模型对量纲不敏感但如果你用逻辑回归或 SVM必须做标准化。我一般统一走一遍StandardScaler并把抓页面失败导致的缺失值填成 -1而不是 0因为 0 在数值特征里有真实含义-1 能明确表示“未知”。import pandas as pd from sklearn.preprocessing import StandardScaler def build_feature_matrix(urls): rows [extract_url_features(u) for u in urls] df pd.DataFrame(rows).fillna(-1) scaler StandardScaler() scaled scaler.fit_transform(df) return df, scaled, scaler注意fit_transform只能在训练集上做验证集和测试集要用训练集 fit 出来的 scaler 去 transform否则会数据泄漏。这个坑我在早期项目里踩过线下 AUC 虚高上线就掉。3. 数据集构建与标注正负样本从哪来、怎么洗3.1 正负样本的采集边界钓鱼样本正类常见来源是公开的钓鱼 URL 订阅源但直接拿来用有两个问题一是很多链接已经失效二是部分样本带重定向抓到的页面和 URL 本身对不上。我的做法是先做一轮存活性和可达性过滤只保留能返回 200 或 3xx 且最终落地页可解析的样本。正常样本负类从常见站点列表里采样注意要覆盖不同 TLD、不同路径风格否则模型会学到“只有 .com 才是正常”这种伪规律。样本比例上不要一上来就 1:1。真实场景里钓鱼占比极低我一般按 1:5 到 1:10 构造训练时再用class_weightbalanced或过采样补偿。这样模型输出的概率更接近真实先验。3.2 去重和近似去重钓鱼样本里大量同模板变体如果不去重模型会过拟合到某个模板。除了精确去重还要做近似去重把 URL 的 host 部分做归一化去掉数字、替换随机串再算相似度。import hashlib def normalize_host(host: str) - str: 把域名里的数字和长随机串替换掉便于近似去重 host re.sub(r\d, N, host) host re.sub(r[a-z0-9]{12,}, RAND, host) return host def dedup_urls(urls): seen set() result [] for u in urls: host urlparse(u).netloc key hashlib.md5(normalize_host(host).encode()).hexdigest() if key not in seen: seen.add(key) result.append(u) return result逻辑说明normalize_host把连续数字替换成N把长度超过 12 的字母数字串替换成RAND这样login-12345.example.com和login-67890.example.com会被归为同一类。dedup_urls用 MD5 做键保证同一归一化域名只保留一条。参数上12 这个阈值可以调太短会误伤正常长单词太长去重效果差。3.3 标注质量怎么保证毕业设计里标注往往是一个人做的容易前后不一致。我的经验是先标 200 条做一轮校准把边界案例比如正常站点的登录页、被黑的正常域名单独拎出来定规则再批量标。标完抽 10% 复核如果一致率低于 90%说明规则没定清楚得回去改。4. 模型训练与评估别只看准确率4.1 基线模型选型特征维度不高几十维、样本量几千到几万我一般先上梯度提升树。它在表格特征上稳定、可解释、调参成本低。逻辑回归可以作为对照用来验证特征是否线性可分。深度学习在这类任务上不是不能做但收益和投入不成正比毕业设计阶段没必要。from sklearn.ensemble import GradientBoostingClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score def train_model(X, y): X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) clf GradientBoostingClassifier( n_estimators200, max_depth3, learning_rate0.1, subsample0.8, random_state42 ) clf.fit(X_train, y_train) proba clf.predict_proba(X_test)[:, 1] print(classification_report(y_test, (proba 0.5).astype(int))) print(AUC:, roc_auc_score(y_test, proba)) return clf参数说明max_depth3控制树复杂度防止过拟合subsample0.8做行采样增加泛化n_estimators200是迭代轮数配合learning_rate0.1是常见组合。stratifyy保证训练测试集正负比例一致样本不均衡时必加。4.2 评估指标怎么选准确率在样本不均衡时没有参考价值。钓鱼检测更关心两件事漏报率把钓鱼判成正常和误报率把正常判成钓鱼。漏报的代价是用户可能被骗误报的代价是正常业务被拦。我一般看召回率Recall和精确率Precision再结合业务定阈值。如果场景偏保守阈值可以调到 0.3宁可误报不可漏报。4.3 特征重要性分析训练完一定要看特征重要性这既是论文里可写的分析也是排查数据泄漏的手段。如果某个特征重要性异常高比如https_flag占了 80%那大概率是样本构造有问题——正常样本全用了 https钓鱼样本全是 http模型学的是这个伪相关不是真正的钓鱼模式。import pandas as pd def show_importance(clf, feature_names): imp pd.Series(clf.feature_importances_, indexfeature_names) print(imp.sort_values(ascendingFalse).head(15))5. 避坑与排查那些让模型线下虚高的细节5.1 数据泄漏特征里混进了标签信息现象线下 AUC 0.99上线后掉到 0.6。原因特征抽取时用了页面最终 URL 或跳转后的域名而钓鱼样本的最终 URL 往往就是钓鱼域名本身等于把标签喂给了模型。解决所有特征必须只基于“检测时刻可见的信息”也就是用户点击前的 URL 和可选的实时页面内容不能用事后才知道的跳转结果。5.2 时间穿越训练集和测试集时间重叠现象随机划分后效果很好但按时间划分就崩。原因钓鱼域名有生命周期同一批域名在训练集和测试集里都出现模型记住了它们。解决按时间切分用早期数据训练后期数据测试。如果样本量不够至少按域名做分组划分保证同一域名不出现在两边。5.3 类别不均衡导致的阈值偏移现象模型输出的概率普遍偏低正常样本也被判成钓鱼。原因训练时负样本远多于正样本模型偏向预测负类。解决用class_weightbalanced或者在评估时画 PR 曲线根据业务需求选阈值而不是固定 0.5。5.4 抓页面超时拖垮整个流程现象批量抽内容特征时部分 URL 卡住整个脚本跑不完。原因没有设置超时和并发上限。解决用requests时显式设timeout(3, 5)并用线程池控制并发数失败的直接标记为缺失不要让单条拖垮全局。5.5 特征版本不一致现象训练时用的特征和线上抽取的特征对不上导致预测结果异常。原因特征代码改过但训练好的模型没重训。解决把特征抽取逻辑和模型版本绑定每次改特征必须重训并记录版本号。这个习惯能省掉大量排查时间。6. 从脚本到可演示系统接口封装与阈值调优技巧把模型训好只是第一步毕业设计要能演示得有一个能接收 URL、返回判定结果的接口。我一般用 FastAPI 封一个最小服务加载训练好的模型和 scaler对外暴露一个 POST 接口。from fastapi import FastAPI from pydantic import BaseModel import joblib import numpy as np app FastAPI() model joblib.load(phish_model.pkl) scaler joblib.load(scaler.pkl) class URLRequest(BaseModel): url: str app.post(/predict) def predict(req: URLRequest): feats extract_url_features(req.url) vec np.array(list(feats.values())).reshape(1, -1) vec scaler.transform(vec) prob model.predict_proba(vec)[0, 1] # 阈值可配置默认偏保守 threshold 0.35 return { url: req.url, phishing_prob: round(float(prob), 4), is_phishing: bool(prob threshold) }逻辑说明接口只做三件事——抽特征、标准化、预测。threshold设成 0.35 而不是 0.5是因为钓鱼检测里漏报代价更高宁可多报一点。这个阈值不是拍脑袋定的要在验证集上画 PR 曲线找到召回率满足要求时精确率还能接受的点。参数上joblib.load加载的模型和 scaler 必须是同一批训练产物否则特征对齐会出问题。阈值调优有个实用技巧不要只调一个全局阈值可以按域名类型分组调。比如 IP 直连的 URL 单独设一个更低的阈值因为这类样本本身风险就高。分组阈值在演示时也更有说服力能体现你对业务的理解。还有一个容易被忽略的点接口要做输入校验。用户可能传一个不带 scheme 的字符串urlparse解析出来 netloc 是空的特征全变成默认值预测结果没有意义。我一般会在入口处补一个 scheme 检查不合法直接返回错误而不是让模型去猜。最后说个我自己的习惯每次改完特征或阈值我都会留一份“回归样本”——几十条已知结果的 URL跑一遍看判定有没有异常翻转。这个习惯帮我拦下过好几次因为特征代码改动导致的静默故障。模型这东西线下指标好看不代表线上稳能复现的验证流程比什么都重要。希望帮到你。本文还有配套的精品资源点击获取
返回列表