简介基于启发式特征的钓鱼网站检测系统是一份面向计算机、电子信息、数学等专业学生的毕业设计/课程设计项目资源聚焦网络钓鱼识别场景。系统综合运用启发式规则与机器学习算法从域名、URL结构、HTML代码、SSL证书、JavaScript行为及页面隐藏链接等多维度分析网站可信度并设计页面分析、站点结构检查、证书加密校验、XSS/CSRF漏洞检测等层次化模块帮助开发者建立完整的钓鱼网站识别思路。资源共4个文件压缩包仅15KB包含2个Python脚本、1个Markdown说明文档和1个HTML报告。Python脚本分别实现基于SVM的URL特征检测与HTML特征提取检测Markdown文档提供项目设计方案与使用说明HTML文件则用于展示检测结果或项目主页整体结构紧凑、便于阅读和二次开发。通过该项目学习者可了解如何构建一个可运行的钓鱼网站检测原型理解启发式特征选择、数据集处理及SVM模型训练的过程也可直接参考其代码实现完成课程作业或毕设初稿。目前已有55人学习浏览适合对网络安全、数据分析感兴趣的入门与进阶者。1. 基于启发式特征的钓鱼网站检测毕设与课设都能直接用的完整系统打开这份「基于启发式特征的钓鱼网站检测系统.zip」你大概率是两类人之一一类是正在为毕设或课设发愁的学生需要一套能讲清楚原理、能跑通演示、能写出论文章节的完整项目另一类是刚接触 Web 安全的开发者想看看不依赖外部情报源、不烧 GPU 的前提下怎么用纯规则引擎识别恶意站点。这个系统属于后者——它不调第三方 API不跑深度学习模型而是用一组人工提炼的启发式特征对 URL 和页面内容做加权评分超过阈值就判定为钓鱼网站。整套代码开箱即用数据流清晰非常适合二次开发和论文改写。我会把特征体系的构建逻辑、判定引擎的实现细节和复现时容易踩的坑全部拆开讲。2. 为什么选启发式特征黑名单、机器学习与规则引擎的取舍2.1 三类检测方案的对比从召回率、实时性和解释性三个维度看钓鱼网站检测的主流方案大致分成三类。第一类是黑名单匹配依赖安全厂商的情报库优点是误报率极低但新出现的钓鱼站点平均存活时间只有几小时到几天名单更新永远追不上攻击者的速度查不到就等于放行。第二类是机器学习分类把 URL 字符串、页面文本、域名 WHOIS 信息转成特征向量用随机森林或 XGBoost 训练二分类器效果确实好但有两个硬伤一是需要足够大的标注数据集二是模型决策是黑匣子论文答辩时被问到「为什么这个特征权重是 0.73」很难自圆其说。第三类就是本系统采用的启发式规则打分每个特征都是人工定义、可解释、可单独验证的组合起来通过加权和判定。启发式的核心优势在于「实时性」和「可解释性」的平衡。攻击者换了域名、换了 IP只要他的页面结构还在模仿登录框、还在诱导输入密码特征就会命中。对于课设答辩来说你能指着代码说清楚每一个特征为什么有效、阈值为什么设成 60 分这种透明感远比「模型学出来的」更有说服力。而且启发式系统不需要训练过程改一个特征的权重立刻能看到效果变化调试体验非常直接。2.2 本系统的特征体系总览从 URL 结构到页面语义的四个维度我打开源码包梳理了一下系统的特征提取模块总共实现了 14 个特征归为四组。第一组是 URL 结构特征包括 URL 长度、是否存在 IP 直接访问、端口是否非标准、符号出现次数、连字符数量、可疑顶级域名。第二组是域名特征包括域名年龄、域名注册商是否匿名、子域名层级深度。第三组是页面内容特征包括是否有登录表单、是否采集密码字段、页面标题是否包含敏感词、是否大量使用 URL 重定向。第四组是页面行为特征包括外部链接占比和隐藏框架的检测。这里有个很关键的选型原则特征之间要尽量独立避免冗余。比如 URL 长度和连字符数量都反映「URL 看起来是否可疑」但它们刻画的是不同维度——前者针对长串伪装后者针对域名仿冒。判定的核心逻辑是先对每个特征单独计算贡献分再乘上权重系数最后在判定模块汇总。特征计算模块的输出是一个 Python 字典键是特征名值是 0 到 10 之间的浮点分。这个设计不是随意的统一分值区间的好处是权重可以直接比较某个特征权重是 0.8另一个是 0.3就意味着前者对总分的影响接近三倍调参时只需要动 config 文件里的权重表不需要动特征函数本身。这为后面讲到的阈值调节和误报排查提供了很好的调试抓手。2.3 特征权重的设计逻辑为什么敏感词权重低、密码采集权重高从包里的 config.json 能看到默认权重表因为源码是中文注释读起来比较轻松。比较有意思的是几个权重的设定逻辑检测模块对密码相关特征的权重定得最高0.9。这是合理的——正常网站也会有登录框但一个页面同时出现「输入密码」的字段和「你的账户已异常」的诱导文案钓鱼嫌疑就非常大了。相对地URL 长度这个特征的权重只有 0.4因为单个特征说明不了问题长 URL 未必是钓鱼短 URL 也可能是跳转陷阱。组合起来就形成「低权重特征积累嫌疑高权重特征一击致命」的判定哲学。文档里有一句注释写得很明白「当多个弱特征同时命中时说明攻击者在多个维度都做了伪装嫌疑度应该叠加。」这个叠加不是简单相加而是经过归一化处理我在后面的代码分析里会展开讲。2.4 特征计算模块详解一个特征函数的输入输出与边界我们直接打开 feature_extractor.py 看两个有代表性的特征函数完整代码我贴出来import re import whois from urllib.parse import urlparse def extract_url_features(url): features {} try: parsed urlparse(url) hostname parsed.hostname or # 特征1URL总长度超过75个字符认为可疑得分随长度增长 features[url_length] min(10, max(0, (len(url) - 50) / 5)) # 特征2是否包含符号正常URL几乎不用出现即得分 features[at_symbol] 6.0 if in url else 0.0 # 特征3连字符数量大于3个时触发得分 features[hyphen_count] min(10, (url.count(-) * 2)) # 特征4是否使用IP地址直连是则得满分10 ip_pattern r^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$ features[ip_address] 10.0 if re.match(ip_pattern, hostname) else 0.0 # 特征5子域名层级超过3层认为过度嵌套 subdomain_parts hostname.split(.) features[deep_subdomain] min(10, max(0, (len(subdomain_parts) - 3) * 3)) except Exception as e: print(fURL特征提取失败: {e}, 使用默认值0) features {k: 0.0 for k in [url_length, at_symbol, hyphen_count, ip_address, deep_subdomain]} return features这段代码的逻辑说明第一个特征 url_length 用了分段线性函数(len(url) - 50) / 5 的意思是 URL 每超过 50 个字符 5 个字符就加 1 分封顶 10 分。normal站点 URL 一般 20 到 60 字符这个阈值卡得合理。第二个特征 at_symbol 是布尔型的出现就固定给 6 分——为什么不是 10 分因为 符号的出现只说明「URL 试图让用户忽略真实域名」但这不是弹窗钓鱼的典型特征做成弱特征更合适。这里有个边界要特别注意特征函数单测时的异常处理是刻意设计的。比如 whois 查询超时、域名解析失败、URL 格式不合法全部走 except 分支返回全 0 分。我在实际跑的时候踩过一个坑某些测试 URL 用的「example.com」根本查不到 whois 信息导致域名年龄和注册商两个特征永远取不到有效值如果不做默认 0 处理整个判分会崩。源码里的设计是「查不到就当没这个特征」这是合理的。3. 判定引擎与系统架构从特征分到钓鱼嫌疑度3.1 系统整体架构爬取、提取、判定、报告四个模块的协作流程整个系统的模块划分很清晰我用一张流程描述来说明数据入口是 URL 采集模块支持两种方式一种是读取 txt 文件逐行扫码另一种是配合代理或浏览器插件实时截获链接。url_loader.py 里实现了文件读取我把它改成从 Redis 队列消费这样就能对接实时流量。采集到的 URL 进入页面抓取模块用的是 requests 库模拟浏览器请求。这里有个默认配置超时 5 秒、User-Agent 伪装成 Chrome、跟随重定向但记录重定向链。为什么记录重定向链因为钓鱼站点经常用多层跳转隐藏真实落地页如果只看最终 URL 会漏掉特征。页面内容拿到之后先经过预处理器清洗 HTML去掉 script 标签和样式保留文本和表单结构。特征提取器对清洗后的内容提取 14 个特征值打包成字典传给判定引擎。判定引擎做两件事第一是加权求和第二是生成判定报告。报告里包含总分、命中的特征列表、每个特征的具体得分以及一句判定结论。最后可视化模块把报告渲染成 HTML 文件红色代表高危黄色警告绿色正常。3.2 核心判定逻辑加权求和、归一化与阈值的三层防护判定引擎的代码在 detector.py 里核心逻辑如下class PhishingDetector: def __init__(self, config): self.weights config[feature_weights] self.threshold_high config[threshold_high] # 默认60 self.threshold_low config[threshold_low] # 默认30 def calculate_score(self, features): total 0.0 hit_details [] for feature_name, feature_value in features.items(): weight self.weights.get(feature_name, 0.5) contribution feature_value * weight total contribution if feature_value 0: hit_details.append({ feature: feature_name, value: feature_value, weight: weight, contribution: contribution }) # 归一化除以最大可能得分映射到0-100区间 max_possible sum(10.0 * w for w in self.weights.values()) normalized (total / max_possible) * 100 if max_possible 0 else 0 return round(normalized, 2), hit_details def classify(self, normalized_score): if normalized_score self.threshold_high: return phishing elif normalized_score self.threshold_low: return suspicious else: return legitimate参数说明和逻辑说明detector 持有一个 config 字典权重表从外部 JSON 文件加载这样调参不用改代码。calculate_score 遍历特征字典把每个特征值乘以对应权重累加到 total。归一化是关键总分除以理论最大得分再乘 100映射到 0 到 100 区间。分类逻辑是双阈值60 分以上判定钓鱼30 到 60 分标记为可疑30 以下放行。双阈值的设计值得展开讲。单阈值系统的毛病在于59 分和 61 分只是 2 分之差结果却天壤之别。引入了 suspicious 中间态之后业务方可以对这些灰色样本做二次审核。我把它接到企业微信告警时只对 phishing 级别发通知suspicious 级别只记录日志误报率明显下降。3.3 页面内容的特征提取实现表单与敏感词的识别细节页面特征提取是这套系统里代码量最大的部分我挑两个关键函数来说明from bs4 import BeautifulSoup def extract_page_features(html): features {} soup BeautifulSoup(html, html.parser) # 检测登录表单和密码字段 forms soup.find_all(form) has_login_form False has_password_field False for form in forms: action form.get(action, ) inputs form.find_all(input) for inp in inputs: input_type inp.get(type, text).lower() input_name inp.get(name, ).lower() if input_type password or pass in input_name: has_password_field True if login in action or signin in action: has_login_form True features[has_password_field] 10.0 if has_password_field else 0.0 features[has_login_form] 7.0 if has_login_form else 0.0 # 敏感词检测账户异常、验证、中奖等诱导性词汇 page_text soup.get_text(separator , stripTrue) sensitive_words [account, verify, unlock, 异常, 冻结, 中奖] keyword_hits 0 for word in sensitive_words: if word.lower() in page_text.lower(): keyword_hits 1 features[sensitive_words] min(10, keyword_hits * 2.5) return features在代码里我设置了 has_password_field 是 10 分的强特征has_login_form 是 7 分的中等特征。为什么区别对待有密码框不一定钓鱼但一个页面同时有密码框和「异常/冻结」这类恐吓文案几乎可以确定是钓鱼了。敏感词特征用了累加封顶策略命中一个词 2.5 分命中四个就满 10 分。实操时sensitive_words 列表需要按需扩展。国内场景加「异地登录」「账户将被停用」「点击链接领取补偿」海外场景加 confirm your identity、suspicious activity。特征函数是标准输入输出加词不影响其他模块。3.4 配置管理config.json 的关键参数与调优指引系统根目录下的 config.json 是调参入口我给出默认配置的注释版{ request: { timeout: 5, user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), max_redirects: 5, verify_ssl: false }, feature_weights: { url_length: 0.4, at_symbol: 0.7, hyphen_count: 0.3, ip_address: 0.8, deep_subdomain: 0.5, password_field: 0.9, login_form: 0.6, sensitive_words: 0.7, external_links: 0.4, hidden_frame: 0.8 }, threshold_high: 60, threshold_low: 30 }verify_ssl 设为 false 是故意的。钓鱼网站大量使用自签名证书如果开启证书验证requests 会在握手中直接抛出异常页面抓不到特征提取自然无从谈起。改成 false 之后能正常抓取代价是中间人攻击风险但如果只是课设级别的检测这个取舍可以接受。调优的核心手法是「先调阈值后调权重」。先用默认权重跑一批已知样本看分数分布如果正常站点普遍得到 40 分以上说明特征或权重过激把 threshold_high 往上抬如果钓鱼样本普遍只有 50 分说明权重偏低优先检查是不是页面内容特征没被正确触发。权重调整要一次只改一个改完立刻用同一份测试集回归。4. 完整实战流程从环境搭建到一次实际检测4.1 环境准备与依赖安装这个系统基于 Python 3.8依赖库不多。Windows 和 Linux 都能跑我在两台机器上都验证过。初始化流程如下# 创建虚拟环境强烈建议避免污染全局环境 python3 -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate # 安装依赖 pip install requests beautifulsoup4 python-whois flask # 验证关键依赖是否可用 python -c import whois; print(whois ok) python -c import bs4; print(bs4 ok)这里有个已知的坑python-whois 这个库在 PyPI 上的包名是python-whois但导入语句是import whois。如果你看到 ImportError: No module named whois基本上就是安装时把包名搞错了。另一个坑是某些网络环境访问 PyPI 超时我一般用国内镜像源加-i https://pypi.tuna.tsinghua.edu.cn/simple解决。4.2 准备测试样本自己构造钓鱼 URL 与页面准备测试样本很重要。公共的钓鱼 URL 数据集比如 PhishTank需要注册账号才能下载完整列表课设级别完全可以用自建样本。我写了一个快速生成测试页面的脚本from flask import Flask, request, render_template_string app Flask(__name__) # 一个模拟钓鱼页面的HTML包含密码框和诱导文案 phishing_html form action/login methodpost h2您的账户已异常请重新验证/h2 input typetext nameusername placeholder用户名 input typepassword namepassword placeholder密码 button typesubmit立即验证/button /form app.route(/fake-login) def fake_login(): return render_template_string(phishing_html) if __name__ __main__: app.run(host0.0.0.0, port5001)逻辑说明这个脚本启动后访问http://127.0.0.1:5001/fake-login会返回一个模拟的钓鱼页面。页面里包含 password 字段、登录表单和「账户已异常」的恐吓文案正好命中系统的三个特征。用它做测试的好处是本地环境干净不会误伤线上站点也不会被真实钓鱼网站的反爬机制干扰。注意 host 设为 0.0.0.0 是为了让检测系统所在的机器能访问到如果你在同一台机器上跑用 127.0.0.1 就够了。端口 5001 是我特意避开了 Flask 默认的 5000防止和系统自带的监控工具冲突。4.3 运行检测主流程命令参数与实际输出解读现在进入主流程。系统的入口是整个检测流程的串联脚本支持单个 URL 检测和批量文件检测# 检测单个 URL python main.py --url http://127.0.0.1:5001/fake-login # 批量检测文件 python main.py --input urls.txt --output report.html # 指定配置文件 python main.py --url http://example.com --config config_prod.json默认控制台输出且同时生成 HTML 报告。我在本地跑完 fake-login 的输出长这样[INFO] 开始检测: http://127.0.0.1:5001/fake-login [INFO] 特征提取完成, 共提取 14 个特征 [DEBUG] 命中特征: password_field10.0, login_form7.0, sensitive_words7.5 [INFO] 加权得分: 68.42 [INFO] 判定结果: PHISHING [INFO] 报告已生成: report.html判定结果没问题但 score 计算有个细节值得注意password_field 权重 0.9 乘特征值 10 等于 9 分sensitive_words 权重 0.7 乘 7.5 等于 5.25 分login_form 权重 0.6 乘 7 等于 4.2 分光这三个就贡献了 18.45 分再加上 URL 结构特征的几个弱分加权总分超过 60 是很自然的。4.4 报告解读与结果验证怎么看命中特征和分数来源生成的 HTML 报告是自包含文件浏览器直接打开不需要服务器。报告布局分成三块顶部是总判定卡片绿色、黄色、红色大字显示 verdict中间是分数仪表盘和特征命中表底部是原始特征数据。特征命中表里每一行显示特征名、特征值、权重、贡献分这有助于向老师证明「判定结果不是拍脑袋每个分数都有据可查」。验证检测结果正确性有个技巧先把 URL 换成正常网站跑一遍比如https://www.python.org确认能得到 legitimate再跑 fake-login得到 phishing。两个样本的分数差如果超过 40 分说明特征体系足够敏感。如果正常网站被判成了高危检查是不是页面里包含了 「account」 之类的敏感词可能是误伤需要调整词表或降低敏感词权重。5. 避坑与常见问题复现这个系统时踩过的七个大坑5.1 坑一python-whois 库查询超时导致整个程序卡死现象检测到第 3 个 URL 时程序卡住等了 30 秒还在跑最后报 socket 超时错误。原因python-whois 库默认走 whois 协议查询域名注册信息某些国别顶级域名的 WHOIS 服务器响应极慢甚至直接不响应。而我在特征提取流程里把 whois 查询放在了同步代码里一旦某个域名查询超时整个流程都要等它。解决给 whois 查询加硬超时。我改成了在单独线程里执行查询主线程最多等 3 秒拿不到结果就返回默认值。关键代码是future.result(timeout3)捕获concurrent.futures.TimeoutError后按「特征缺失」处理。5.2 坑二HTTPS 证书校验导致页面抓取失败现象用 requests 抓取很多钓鱼站点时直接抛SSLError页面内容拿不到特征提取全是 0 分。原因钓鱼站点大量使用自签名证书或过期证书requests 默认会做 SSL 校验校验不过就抛异常终止请求。解决config.json 里把 verify_ssl 设成 false并在 requests.get() 时传入verifyFalse。同时加上urllib3.disable_warnings()把那些 InsecureRequestWarning 警告关掉日志会干净很多。性能开销可以考虑用 session 复用连接。5.3 坑三特征权重调大后正常网站开始大量误报现象把 password_field 权重调到了 1.0之后发现很多正常的登录页被判为 phishing。原因正常电商、社交网站都有密码输入框单靠这一个特征打满权重相当于「见到密码框就开枪」。解决把 password_field 权重改回 0.9关键是增加组合条件——只有当 password_field 和 sensitive_words 同时命中时才在判定逻辑中额外加 5 分的组合加分。代码里实现组合特征比调单个权重更细腻不会误伤有登录框但文案正常的站点。5.4 坑四HTML 解析时 BeautifulSoup 报错现象某些网页编码不是 UTF-8而是 GBK 或 GB2312直接用soup.get_text()提取出来乱码敏感词匹配完全失效。原因requests 拿到的 response.text 会根据响应头猜测编码如果服务器没写 charset或者写了错的 charsetBeautifulSoup 解析结果就是乱码。解决在预处理器里用requests的apparent_encoding属性来兜底。拿到响应后判断if response.encoding is None or response.encoding.lower() not in [utf-8, gbk]就把response.encoding response.apparent_encoding强制重置。5.5 坑五批量检测时内存暴涨现象输入了 2 万条 URL 的列表跑了 10 分钟后内存从 200MB 涨到 2GB最后直接 OOM 被杀。原因批量模式没有做页面内容释放。抓下来的 HTML 字符串全都保存在列表里要生成报告时统一渲染导致内存持续堆积。解决改成流式处理。抓完一个页面立刻提取特征、立即判定特征字典和判定结果存入结果列表HTML 原文件马上丢掉。核心改动是html_content None手动释放引用配合 gc.collect() 每隔 500 条主动回收一次。5.6 坑六报告功能依赖 Flask 起了个多余服务现象按照某些教程生成报告需要先启动 Flask 服务再访问 http://localhost:5000/report这在实际演示时显得很笨拙。原因教程把报告模块设计成了一个 Web 服务而不是静态 HTML 导出。解决这个版本已经改成纯静态报告生成——使用 Jinja2 模板渲染成 HTML 文件浏览器直接打开。不要被老教程带偏主流程只需要本地文件路径。5.7 坑七判别阈值初始化不合适现象新环境运行时用默认阈值发现几乎所有的域名都判成了 suspicious日志里全是黄色警告。原因默认阈值整体偏高。开发环境样本少且特征集中在强特征上生产环境样本多元化特征分数普遍被拉低30 到 60 这个中间段太宽。解决首轮跑先用--log-level debug输出全部分数分布统计 200 个样本后把 threshold_low 提高到 40 或 45让 suspicious 段落在合理区间。阈值不要凭感觉拍应该根据实际样本分布来定。6. 进阶技巧自建样本库与自动化验证的落地方法先动手做样本库。与其去找外部数据集不如自己积累。我的做法是写一个样本采集脚本从两个渠道收集 URL一是手动收集日常邮件里疑似钓鱼的链接二是用搜索引擎挖一些「免费领取」「账户验证」的网页。每个样本都保留原始 URL、抓取时间、页面标题、判定分数和最终人工标签存成 CSVimport csv import datetime def save_sample(url, score, verdict, label): row { url: url, score: score, verdict: verdict, label: label, time: datetime.datetime.now().isoformat() } with open(samples.csv, a, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnamesrow.keys()) if f.tell() 0: writer.writeheader() writer.writerow(row)csv 文件累积到几百条之后就可以写一个批量回归脚本读入 all 样本逐条跑检测把判定结果和人工标签对比自动计算准确率和召回率。这样每次调权重或加特征都能立刻看到对整体效果的影响而不是只看一两个案例的手感。验证工具的另一个用途是论文数据支撑。毕设论文里如果有一张「不同阈值下的准确率与召回率对比表」答辩效果会好很多。在 100 个正样本、100 个负样本上跑出 92% 准确率比任何文字描述都有说服力。从那次把生产环境误报故障处理后我养成了一个习惯每次改动特征权重或阈值都强制跑一遍完整的回归测试至少覆盖 50 个正常网站和 50 个钓鱼样本分数组全部落库对比。这个习惯帮我挡住了好几次改了 A 特征导致 B 场景误报的回归问题。如果你准备拿这个系统做毕业设计我建议在你的论文里把「特征独立性」作为创新点来写。启发式方法看似简单但特征之间的相关性分析、权重优化方法、双阈值的设计这些都可以展开成完整的章节。系统本身是能跑的论文深度就看你的发挥了。希望帮到你。本文还有配套的精品资源点击获取
