钓鱼邮件识别模型实战:从特征工程到机器学习全流程
发布时间:2026/10/18 5:08:30 锦皓数字建站

简介这份资源面向网络安全初学者与机器学习实践者聚焦钓鱼网站与钓鱼邮件的自动识别问题提供一套可运行的建模方案。包内共14个文件以10个Python脚本和4个CSV数据集为主压缩包约339KB脚本覆盖数据预处理、词向量工具、TextCNN、LSTM_CNN、CNN_LSTM及简单神经网络等模型实现CSV则用于承载训练与测试样本。已有241人学习下载说明该方向具备一定关注度。读者可据此完成从URL与邮件文本特征提取、TF-IDF或词嵌入编码到朴素贝叶斯、SVM、随机森林及深度学习模型训练评估的完整流程并借助交叉验证、F1与AUC-ROC等指标对比不同算法表现理解正负样本不平衡时的采样策略与超参数调优思路适合作为课程设计、安全实验或入门项目的参考代码。1. 钓鱼邮件识别模型从一封可疑邮件到可运行的机器学习工程你收到过那种邮件吗发件人显示是某银行客服正文里一个蓝色按钮写着“立即验证账户”鼠标悬停上去真实链接却指向一个跟官网长得几乎一模一样的域名。人工判断靠经验但经验会疲劳。这个项目要做的就是把这种判断固化成一个可训练、可复现的机器学习模型——输入邮件文本和链接特征输出“钓鱼”或“正常”的概率。它适合两类人一是想跑通一个完整 ML 工程的新手从数据清洗到模型保存全链路走一遍二是需要把钓鱼检测能力嵌入自己系统的开发者直接拿模型文件做推理。整个项目不依赖外部 API纯本地训练数据格式清晰改起来不玄学。2. 项目架构拆解数据流、模块划分与技术选型2.1 整体数据流从原始邮件到特征向量这个项目的核心链路并不复杂但每一步都有讲究。原始数据通常是一批邮件文本可能来自公开钓鱼邮件数据集也可能是你自己从邮件网关导出的日志。数据流大致是原始邮件 → 解析发件人/正文/链接 → 特征提取 → 向量化 → 模型训练 → 模型评估 → 保存模型。其中特征提取是最关键的一环因为钓鱼邮件和正常邮件的区分度八成靠特征工程两成靠模型选型。常见做法是把邮件拆成几个独立字段发件人域名、正文文本、URL 列表、是否有附件、HTML 标签数量。每个字段单独处理最后拼成一个稀疏矩阵。这样做的好处是后续想加新特征比如邮件头里的 SPF 验证结果只需要在特征拼接层加一列不用动模型代码。我一般会把整个工程分成四个目录data/放原始数据和清洗后的数据features/放特征提取脚本models/放训练好的模型文件和评估报告src/放训练和推理的入口代码。这种划分方式不花哨但半年后回来看能一眼找到该改哪里。2.2 技术选型为什么是 scikit-learn pandas项目标题里写的是“机器学习模型”没有指定框架。对于钓鱼邮件识别这种中等规模、特征以文本和类别为主的任务scikit-learn 是最稳的选择。它内置了 TF-IDF、逻辑回归、随机森林、梯度提升树不需要 GPU训练几分钟就能出结果。pandas 负责数据清洗和特征拼接numpy 做数值运算三者组合足够覆盖从原型到落地的全流程。如果你后续想上深度学习也可以把特征提取层换成 BERT 微调但那是另一个量级的工程。当前项目定位是“能跑起来、能改得动”所以选型偏向轻量和可解释。逻辑回归的系数可以直接看哪些词对钓鱼判定贡献最大这对安全场景很重要——你不能只给一个黑盒结果得能解释为什么这封邮件被拦了。2.3 目录结构每个文件负责什么一个典型的目录结构如下project/ ├── data/ │ ├── raw_emails.csv │ └── cleaned_emails.csv ├── features/ │ ├── extract_url_features.py │ └── extract_text_features.py ├── models/ │ ├── phishing_model.pkl │ └── vectorizer.pkl ├── src/ │ ├── train.py │ ├── predict.py │ └── evaluate.py └── requirements.txtraw_emails.csv至少包含两列text和labellabel 用 0 表示正常1 表示钓鱼。extract_url_features.py负责从正文里正则匹配出所有 URL然后提取域名长度、是否含 IP、路径深度、是否有可疑顶级域名等。extract_text_features.py负责正文的 TF-IDF 向量化。train.py把两类特征拼起来训练模型并保存。predict.py加载模型和 vectorizer对新邮件做推理。这个结构的好处是特征提取和模型训练解耦。你可以单独测试 URL 特征提取逻辑不用每次都跑完整训练。调试的时候先看特征提取对不对再看模型学没学到东西排查路径清晰。3. 环境搭建与训练流程从零跑通第一个模型3.1 依赖安装与数据准备先建虚拟环境再装依赖。requirements.txt 里至少要有这些pandas2.0.3 numpy1.24.4 scikit-learn1.3.0 scipy1.11.1 joblib1.3.2安装命令python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt数据准备阶段你需要一个 CSV 文件至少两列。如果手头没有现成数据可以先用少量样本手动构造比如 50 条正常邮件、50 条钓鱼邮件跑通流程后再扩充。注意正常邮件和钓鱼邮件的比例不要严重失衡否则模型会偏向多数类。常见做法是保持 1:1 到 1:3 之间如果钓鱼样本太少可以用 SMOTE 过采样但更推荐直接找更多数据。提示数据里如果有缺失值pandas 读进来后先用df.dropna(subset[text, label])清掉否则后续向量化会报错。3.2 特征提取代码逐段解析先看 URL 特征提取。这段代码从邮件正文里找出所有链接然后计算几个统计特征import re from urllib.parse import urlparse def extract_url_features(text): # 匹配 http/https 开头的链接 urls re.findall(rhttps?://[^\s], text) features { url_count: len(urls), has_ip_url: 0, max_url_length: 0, has_suspicious_tld: 0, avg_path_depth: 0.0 } if not urls: return features suspicious_tlds [.tk, .ml, .ga, .cf, .gq] total_depth 0 for url in urls: parsed urlparse(url) host parsed.hostname or # 判断域名是否为纯 IP if re.match(r^\d\.\d\.\d\.\d$, host): features[has_ip_url] 1 features[max_url_length] max(features[max_url_length], len(url)) if any(host.endswith(tld) for tld in suspicious_tlds): features[has_suspicious_tld] 1 total_depth len([p for p in parsed.path.split(/) if p]) features[avg_path_depth] total_depth / len(urls) return features逻辑说明url_count是正文里链接总数钓鱼邮件往往塞多个链接提高点击率。has_ip_url标记是否直接用 IP 地址访问正常邮件极少这样。max_url_length捕捉超长 URL常见于隐藏真实域名的钓鱼手法。has_suspicious_tld检查是否用了免费顶级域名这类域名注册成本低钓鱼者偏爱。avg_path_depth是路径平均深度钓鱼链接常有多层路径伪装成正常页面。参数说明suspicious_tlds列表可以根据你的业务场景调整比如某些地区性域名如果不在你的用户群里出现也可以加进去。re.findall的正则只匹配 http/https如果邮件里有www.开头的短链接需要额外加一条正则。再看文本特征提取用 TF-IDFfrom sklearn.feature_extraction.text import TfidfVectorizer def build_text_vectorizer(max_features5000): return TfidfVectorizer( max_featuresmax_features, stop_wordsenglish, ngram_range(1, 2), min_df2, max_df0.95 )max_features5000控制词汇表大小太大容易过拟合太小会丢信息。ngram_range(1, 2)同时考虑单词和双词组合比如“verify account”作为一个整体特征比单独的“verify”更有区分度。min_df2过滤掉只出现一次的词这些词多半是噪声。max_df0.95过滤掉几乎所有邮件都有的词比如“the”“dear”它们没有区分能力。3.3 训练与评估一条命令跑完训练脚本把 URL 特征和文本特征拼起来然后训练逻辑回归import pandas as pd import joblib from scipy.sparse import hstack from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from features.extract_url_features import extract_url_features from features.extract_text_features import build_text_vectorizer # 读取数据 df pd.read_csv(data/cleaned_emails.csv) df df.dropna(subset[text, label]) # URL 特征 url_feats df[text].apply(extract_url_features).apply(pd.Series) # 文本特征 vectorizer build_text_vectorizer() text_feats vectorizer.fit_transform(df[text]) # 拼接特征 X hstack([text_feats, url_feats.values]).tocsr() y df[label].values # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 训练 model LogisticRegression(max_iter1000, class_weightbalanced) model.fit(X_train, y_train) # 评估 y_pred model.predict(X_test) print(classification_report(y_test, y_pred)) # 保存 joblib.dump(model, models/phishing_model.pkl) joblib.dump(vectorizer, models/vectorizer.pkl)逻辑说明hstack把稀疏的文本特征和稠密的 URL 特征拼在一起注意 URL 特征要先转成 numpy 数组再拼否则 scipy 会报维度不匹配。class_weightbalanced在样本不均衡时自动调整权重比手动过采样省事。stratifyy保证训练集和测试集的标签比例一致避免测试集里全是正常邮件导致评估失真。参数说明max_iter1000是逻辑回归的迭代次数如果数据量大或者特征多可能需要调到 2000。random_state42固定随机种子保证每次划分结果一样方便复现。test_size0.2是测试集比例数据量小于 1000 条时建议用 0.3让评估更稳定。跑完训练后你会看到 precision、recall、f1-score。钓鱼检测场景下recall 比 precision 更重要——漏掉一封钓鱼邮件的代价通常比误拦一封正常邮件大。如果 recall 偏低可以调低逻辑回归的判定阈值比如把 0.5 改成 0.3让更多邮件被标为钓鱼。4. 推理与集成把模型用起来4.1 单封邮件预测从文本到结果训练完模型只是第一步真正要用起来得写推理脚本。下面这段代码加载模型和 vectorizer对单封邮件做预测import joblib import pandas as pd from scipy.sparse import hstack from features.extract_url_features import extract_url_features model joblib.load(models/phishing_model.pkl) vectorizer joblib.load(models/vectorizer.pkl) def predict_email(text): # 文本特征 text_feat vectorizer.transform([text]) # URL 特征 url_feat pd.Series(extract_url_features(text)).to_frame().T # 拼接 X hstack([text_feat, url_feat.values]).tocsr() # 预测概率 prob model.predict_proba(X)[0][1] label 钓鱼 if prob 0.5 else 正常 return label, round(prob, 4) # 测试 email_text Dear user, your account has been suspended. Click http://192.168.1.1/verify to reactivate. print(predict_email(email_text))逻辑说明vectorizer.transform而不是fit_transform因为推理阶段不能重新拟合词汇表必须用训练时保存的 vectorizer。predict_proba返回两个概率索引 1 是钓鱼类的概率。阈值 0.5 可以按业务需求调整比如安全要求高就调到 0.3。参数说明extract_url_features返回的是字典转成 DataFrame 再取 values是为了保持和训练时一致的列顺序。如果训练时 URL 特征有 5 列推理时也必须 5 列顺序不能乱否则模型会错乱。4.2 批量推理与性能注意点如果要对几千封邮件批量预测不要用 for 循环一封封调predict_email那样每次都要重新加载模型和 vectorizer慢得离谱。正确做法是一次性把所有文本转成特征矩阵然后调一次model.predictdef batch_predict(texts): text_feats vectorizer.transform(texts) url_feats pd.DataFrame([extract_url_features(t) for t in texts]) X hstack([text_feats, url_feats.values]).tocsr() probs model.predict_proba(X)[:, 1] return probs这样做的另一个好处是hstack只执行一次内存占用可控。如果邮件量超过 10 万建议分批次处理每批 5000 条避免内存溢出。注意推理时如果遇到训练集里没出现过的词TF-IDF 会直接忽略不会报错。但如果 URL 特征提取函数抛异常整个批次都会失败。建议在extract_url_features里加 try-except对异常邮件返回全零特征保证批量推理不中断。5. 避坑与常见问题血泪经验五条5.1 现象模型在测试集上准确率 99%上线后一塌糊涂原因训练集和测试集来自同一批数据分布一致但线上邮件的用词、链接风格和训练数据差异很大。这是典型的过拟合加数据泄露。解决划分数据时按时间切分比如用前 8 个月的数据训练后 2 个月的数据测试。如果做不到至少保证训练集和测试集的发件人域名不重叠。另外定期用新数据重新训练模型别指望一个模型用一年。5.2 现象URL 特征提取报错提示维度不匹配原因训练时 URL 特征有 5 列推理时extract_url_features返回的字典少了某个键导致 DataFrame 列数不一致。解决在extract_url_features里固定返回所有键即使某些特征无法计算也返回默认值 0。训练和推理共用同一个函数不要写两份逻辑。5.3 现象TF-IDF 向量化后内存爆了原因max_features设得太大或者ngram_range设成了 (1, 3)导致词汇表爆炸。解决先用max_features5000跑一遍看模型效果。如果效果不够再逐步加到 10000同时观察内存占用。ngram_range一般 (1, 2) 就够了三元组带来的提升有限但内存翻倍。5.4 现象钓鱼邮件召回率低很多漏判原因逻辑回归默认阈值 0.5对钓鱼类偏保守。或者训练数据里钓鱼样本太少模型没学到足够特征。解决先看训练数据里钓鱼和正常的比例如果低于 1:3补充钓鱼样本。然后调低判定阈值比如 0.3用model.predict_proba(X)[:, 1] 0.3代替model.predict。调完后重新评估 recall 和 precision 的平衡。5.5 现象模型文件加载后预测结果和训练时不一致原因保存模型时用了 joblib但 vectorizer 没有一起保存推理时重新 fit 了 vectorizer词汇表变了。解决训练完必须同时保存 model 和 vectorizer推理时加载同一份 vectorizer。版本也要一致scikit-learn 1.3 训练的模型用 1.2 加载可能报 warning 甚至出错。6. 二次开发方向从能跑到好用这个项目跑通之后最值得改的地方是特征工程。当前 URL 特征只有 5 个你可以加更多域名注册时长通过 WHOIS 查询、SSL 证书是否有效、链接是否被短链服务包装、正文里是否有紧迫性词汇“立即”“24小时内”。这些特征对钓鱼检测的区分度很高加进去通常能把 recall 提升 5 到 10 个百分点。另一个方向是模型替换。逻辑回归可解释性好但拟合能力有限。你可以换成 LightGBM 或 XGBoost特征不用变直接把LogisticRegression替换成LGBMClassifier训练速度也很快。如果数据量超过 10 万条可以考虑用线性 SVM在高维稀疏特征上表现稳定。验证改造效果的方法很简单固定测试集每次只改一个变量看 classification_report 里钓鱼类的 recall 和 f1-score 变化。不要一次改多个地方否则出了问题不知道是哪个改动导致的。我自己的习惯是每次改完特征提取逻辑先跑一遍extract_url_features对 10 封已知钓鱼邮件的输出肉眼确认特征值合理再跑完整训练。这个步骤花不了两分钟但能避免很多低级错误。从那以后我每次动特征代码都强制走一遍这个小验证希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。