简介:基于Python的启发式特征钓鱼网站检测系统项目,面向高校计算机相关专业学生与初级开发者,适用于课程设计、毕业设计或入门实践。系统解析网址和网页内容,提取多项启发式特征,并利用支持向量机(SVM)模型完成钓鱼网站识别,覆盖从特征处理到模型预测的完整流程。压缩包共5个文件,包含两个Python源码脚本、一个Markdown说明文档、一个TXT介绍文件和一个HTML页面,整体仅16KB,便于快速部署和二次开发。目前已有63人浏览学习。资源附带设计说明与运行介绍,代码结构清晰、注释到位,既能帮助理解特征工程和分类模型的实际应用,也支持在此基础上扩展功能或迁移至其他网络安全检测任务,是课设与毕设的有力参考。
1. 启发式特征钓鱼网站检测系统,Python 落地要先解决“黑名单滞后”问题
一个钓鱼网站的平均存活时间按小时计,纯靠定期更新黑名单的检测方案,从发现到同步往往已经错过拦截窗口。启发式特征的思路是不等情报源,直接从前端的 URL 规则、页面 DOM 结构、表单提交行为里挖异常,按加权评分判断“像不像钓鱼站”。这套思路用 Python 实现很顺:requests 拿页面、BeautifulSoup 解析、特征计算、再叠加一个分类器,短时间内就能做出可演示的原型。标题里出现“最新开发.zip”,实际工作中通常是一个带依赖的离线交付包,意味着要处理好环境、特征库和接口封装,而不只是有算法。下面从特征设计、代码实现到评估调优和打包分发,把一条可跑通的路径讲清楚。
2. 钓鱼网站启发式特征:从 URL、DOM 到内容语义怎么选特征
特征设计决定系统上限,算法只是把特征加工成判断。我习惯把特征分成三层:URL 词法层、DOM/行为层、内容语义层。各层之间有重叠,但维度不同,分开提取更容易定位问题。
2.1 URL 词法特征:数字 IP、短链接和敏感词命中
URL 是最先拿到的信息,不需要等页面加载。钓鱼 URL 常见做法是把正常域名伪装成相似字符,或者直接使用 IP、端口、子域来混淆。特征计算尽量用标准库,减少依赖,这样后面打成 zip 包时不会带太多第三方模块。
| 特征名称 | 计算方式 | 为什么有效 |
|---|---|---|
| url_length | len(url) > 75 记分 | 钓鱼链接常带长参数伪装 |
| has_ip | hostname 是 IPv4 | 正规站很少用 IP 直接服务 |
| has_at_sign | '@' in url | 浏览器解析时 @ 后才是真实地址 |
| port_non_standard | 非 80/443 端口 | 钓鱼常用非常规端口规避检查 |
| num_dots | hostname.count('.') >= 4 | 多层子域掩盖真实域名 |
| has_shortener | 命中 bit.ly/t.cn 等 | 短链接难以直观看出目标 |
| keyword_hits | 命中 login/verify 等 | 特征词密度高 |
from urllib.parse import urlparse def url_features(url: str) -> dict: parsed = urlparse(url) hostname = parsed.hostname or "" port = parsed.port # 计算 URL 自身异常特征 is_ip_like = hostname.count(".") == 3 and all( part.isdigit() for part in hostname.split(".") ) return { "url_length": len(url), "has_ip": 1 if is_ip_like else 0, "has_at_sign": 1 if "@" in url else 0, "port_non_standard": 1 if port and port not in (80, 443) else 0, "num_dots": hostname.count("."), "has_shortener": 1 if any( s in hostname for s in ("bit.ly", "t.cn", "goo.gl") ) else 0, "keyword_hits": sum( 1 for kw in ("login", "verify", "account", "update", "free", "bonus") if kw in url.lower() ), }这段代码把所有结果都转成数值,方便后面直接拼接 pandas DataFrame。参数说明:url_length阈值 75 不是绝对标准,带查询参数的正常页面经常超过 100,所以它只能作为弱信号;keyword_hits使用的是固定词表,实际业务里要按被仿冒品牌调整,否则像update这种词会大量命中正常站。has_ip这里只兼容 IPv4,IPv6 地址要用ipaddress模块判断,避免误判。
2.2 DOM 与表单行为特征:解析页面而不是只看源码
拿到 HTML 后用 BeautifulSoup 检查结构。钓鱼页面的核心目标是诱导输入凭证,所以表单是重点,其次是隐藏元素和 iframe 这类容易被忽略的加载方式。
from bs4 import BeautifulSoup def dom_features(html: str, original_url: str) -> dict: soup = BeautifulSoup(html, "html.parser") forms = soup.find_all("form") password_inputs = soup.select("input[type='password']") # 统计表单 action 是否指向当前站外 external_forms = 0 for form in forms: action = form.get("action") or "" if not action.startswith(original_url): external_forms += 1 return { "has_form": 1 if forms else 0, "num_password_inputs": len(password_inputs), "external_form_count": external_forms, "has_iframe": 1 if soup.find_all("iframe") else 0, "hidden_elements": len( soup.find_all(style=lambda v: v and "display:none" in v) ), "small_title": 1 if soup.title and len(soup.title.string or "") < 6 else 0, }external_form_count的对比方式是简单前缀匹配,遇到路径变更会误判,更稳的方案是用urljoin把 action 解析成完整地址,再比较域名和协议。hidden_elements只能做参考,很多正常页面也用隐藏字段存 token。iframe 特征用于点击劫持检测,但也要注意不少 CDN 广告脚本会插入 iframe。这一层特征的问题是依赖页面完整返回,如果被反爬拦截或者页面是纯 JavaScript 渲染,提取结果会失真。
2.3 内容语义特征:品牌关键词、标题不匹配与登录意图
钓鱼站会模仿目标品牌。维护一个品牌词表,例如paypal、apple、microsoft,再和页面标题、正文关键词做交集,比单纯看 URL 更接近真实意图。单独统计“登录”“验证”这类词也很关键,但要注意正常客服页面同样会出现。
def semantic_features(text: str, title: str) -> dict: brands = ["paypal", "apple", "appleid", "microsoft", "amazon", "google"] intent_words = ["login", "sign in", "verify", "unlock", "confirm", "password"] text_l = text.lower() title_l = title.lower() brand_found = [b for b in brands if b in title_l or b in text_l[:500]] intent_found = [w for w in intent_words if w in text_l] # 标题含品牌比正文命中更可信 return { "brand_mentioned": 1 if brand_found else 0, "intent_keywords": len(intent_found), "title_has_brand": 1 if any(b in title_l for b in brands) else 0, }这段只统计前 500 个字符,是为了避免页脚版权信息干扰判断。在工程里要小心:目标品牌名也可能出现在合法客服页面,所以品牌特征要和 URL 词法特征组合,比如“URL 不含品牌域名但页面标题含品牌词”就是强信号。三层特征最后合并成一个 dict,交给后续评分或训练流程。
3. 用 Python 搭建可运行的启发式检测系统:代码骨架和参数说明
这一章从无到有把系统跑起来,包括样本准备、特征提取、规则评分和机器学习融合。先给整体目录结构,再逐块写代码。
3.1 环境准备与数据采集:从一个 python 爬虫到本地样本集
开发环境建议 python 3.9 以上,依赖 requests、beautifulsoup4、scikit-learn、pandas。安装命令如下,注意在 Linux 系统装 python 时经常出现多个解释器并存,强烈建议先建 venv 再装。
python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate python -m pip install requests beautifulsoup4 scikit-learn pandas数据采集不是系统核心,但样本质量直接影响模型效果。我会准备phishing.txt和legitimate.txt,每行一个 URL,再用 requests 抓取 HTML。需要注意:抓取钓鱼站本身有风险,尽量只在隔离环境里跑,并且控制请求频率。
import requests def fetch_html(url: str, timeout: int = 10) -> str: headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} try: resp = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) if resp.status_code != 200: return "" # 限制页面大小,防止异常大页面拖慢特征提取 return resp.text[:200000] except requests.RequestException: return ""参数说明:timeout控制单次请求上限,批量任务中默认 10 秒已经够宽裕;allow_redirects=True是因为钓鱼站常做跳转,保留最终 URL 后重跑url_features才有意义。请求失败直接返回空字符串,后续特征会用 0 填充,不让异常中断整批任务。如果要做几百个 URL 的批量清理,建议改成asyncio加aiohttp并发,并发数控制在 5 到 10,否则很快会被防护拦掉。
3.2 特征提取合并:把 URL、DOM、语义特征拼成一条记录
fetch_html返回后,按顺序调用前面三个特征函数。实际运行中要做好容错,一个页面解析失败不能影响下一批。
from bs4 import BeautifulSoup def extract_all_features(url: str, html: str) -> dict: features = url_features(url) if html: soup = BeautifulSoup(html, "html.parser") page_text = soup.get_text(" ", strip=True)[:5000] title = soup.title.string if soup.title and soup.title.string else "" features.update(dom_features(html, url)) features.update(semantic_features(page_text, title)) else: # 页面抓不到时用 0 填充分词特征 features.update({ "has_form": 0, "num_password_inputs": 0, "external_form_count": 0, "has_iframe": 0, "hidden_elements": 0, "small_title": 0, "brand_mentioned": 0, "intent_keywords": 0, "title_has_brand": 0, }) return features这个函数把耗时集中在网络请求和 HTML 解析上,中间不要混入其他无关逻辑。dom_features内部又BeautifulSoup了一次,和这里重复解析,性能敏感时可以改为直接传soup对象。特征字段顺序必须保持固定,后面构造 DataFrame 才不会错列。
3.3 规则评分与机器学习分类:保留可解释性的融合方案
纯规则系统可以上线,但死角多。常见做法是保存一个规则评分,作为一列特征输入模型,这样模型既吸收启发式经验,又能在数据里学出非线性组合。
def rule_score(features: dict) -> float: score = 0.0 # URL 层 if features["url_length"] > 75: score += 0.15 if features["has_ip"]: score += 0.25 if features["num_dots"] >= 4: score += 0.10 if features["has_at_sign"]: score += 0.30 # DOM 层 if features["num_password_inputs"] >= 2: score += 0.20 if features["external_form_count"] > 0: score += 0.15 # 语义层 if features["brand_mentioned"] and not features["title_has_brand"]: score += 0.30 if features["intent_keywords"] >= 3: score += 0.20 return min(score, 1.0)规则评分范围是 0 到 1,最后用min截断。实际调参时不要直接把 0.5 当阈值,要用验证集算 F1 再定。规则之间可能存在相关性,例如长 URL 往往同时触发num_dots,会放大分数,建议先做特征相关性分析,把相关系数超过 0.8 的合并。特征合并后,用随机森林完成最后的分类。
import pandas as pd from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier def train_model(df: pd.DataFrame): feature_cols = [c for c in df.columns if c not in ("url", "label")] X_train, X_val, y_train, y_val = train_test_split( df[feature_cols], df["label"], test_size=0.2, random_state=42, stratify=df["label"] ) clf = RandomForestClassifier( n_estimators=300, max_depth=8, class_weight="balanced", n_jobs=-1, random_state=42, ) clf.fit(X_train, y_train) return clf, feature_cols参数说明:class_weight="balanced"解决钓鱼样本偏少的问题;max_depth=8防止在几百个特征上过拟合;n_estimators=300是效果和耗时之间的折中。样本量小于 500 时,优先用逻辑回归并设置C=0.1,结果更稳定,也更容易解释特征权重。
| 常见误用 | 问题 | 建议做法 |
|---|---|---|
| 只用 URL 黑名单 | 新站漏报 | 叠加启发式特征 |
| 单条规则一票否决 | 误报率高 | 加权评分或模型综合判断 |
| 每次请求都抓整页 | 速度慢、易被拦 | 限制 HTML 大小并做缓存 |
| 忽略样本不平衡 | 准确率虚高 | 用stratify和class_weight处理 |
4. 启发式检测系统的评估与误报治理:用指标而不是感觉定阈值
一个检测系统在测试集上表现好,上线后误报才爆发,通常是评估方式和阈值没有对齐真实环境。启发式系统的可调点集中在规则权重、模型超参数和最终判定阈值三处。
4.1 准确率不是唯一标准:召回率、误报率和 ROC-AUC 怎么看
对于钓鱼检测,样本极端不平衡。假设钓鱼流量只占全部请求的 1%,一个把所有流量都判为正常的模型,准确率是 99%,却毫无用处。所以至少要输出混淆矩阵,再看召回率、误报率和精确率。
from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix def evaluate_model(model, X_val, y_val): pred = model.predict(X_val) proba = model.predict_proba(X_val)[:, 1] tn, fp, fn, tp = confusion_matrix(y_val, pred).ravel() fpr = fp / (fp + tn) if (fp + tn) else 0 print(classification_report(y_val, pred, digits=3)) print(f"FPR={fpr:.3f} ROC_AUC={roc_auc_score(y_val, proba):.3f}")classification_report给出 P/R/F1 的按类汇总,ROC_AUC能衡量排序能力,但在正样本很少时PR-AUC更直观。FPR是误报率,直接影响运营人工成本。在安全运营里,我会宁可让模型多判出几个“疑似”,然后交给人工复查,也不能接受高误报导致的告警疲劳。
4.2 用 PR 曲线选判定阈值,而不是抽脑袋定 0.5
模型输出的概率需要用阈值转成类别。阈值降低能抓到更多钓鱼,但误报也会上升。验证集上画精确率-召回率曲线,选 F1 最高的点作为工作阈值是最稳妥的做法。
import numpy as np from sklearn.metrics import precision_recall_curve def select_threshold(model, X_val, y_val): proba = model.predict_proba(X_val)[:, 1] precisions, recalls, thresholds = precision_recall_curve(y_val, proba) f1_scores = 2 * (precisions * recalls) / (precisions + recalls + 1e-6) best_idx = np.argmax(f1_scores[:-1]) return thresholds[best_idx]precision_recall_curve返回的thresholds对应每个概率边界,最后一个点没有对应阈值,所以f1_scores要截去末尾。1e-6防止除零。这个方法同样可以套在规则总评分上,只要把输入改成规则分数数组,就能找到一个让业务可接受的规则阈值。
4.3 启发式权重优化的 3 个必调参数和常见踩坑
规则引擎里的初始权重只代表 0.5 的起点,需要用小样本和网格搜索校正。可以先固定模型阈值,用scipy.optimize或逐个试权重。下面几个参数最值得先调。
| 参数 | 影响 | 推荐起点 |
|---|---|---|
| url_length 阈值 | 过长 URL 是否判异常 | 75 到 100 |
| num_password_inputs 阈值 | 登录框数量敏感度 | 1 到 2 |
| intent_keywords 阈值 | 语义意图词多少算可疑 | 2 到 3 |
| external_form_count 阈值 | 表单外发敏感度 | 0 到 1 |
| brand/title 匹配 | 品牌词和 URL 域名关系 | 混合规则 |
常见踩坑:把页面标题和品牌词匹配做得太宽,导致包含“apple id”的普通教程页被误判;对 iframe 一票否决,会漏掉纯页面重定向的钓鱼站;把图片链接的 alt 文本也纳入语义特征,结果大量正常图站被误伤。每次改完特征后重新计算特征相关性,去掉冗余特征,再跑一遍评估脚本,这是最省事的回归测试方式。
5. 给启发式检测系统收尾:zip 包封装、HTTP 接口与规则热更新
开发完算法只是第一公里。交付物是 zip 包时,重点是让目标机器能便捷运行。常见做法是本地把依赖下载成 wheel 文件,连同源码打进一个 zip,目标机器只需要有 python 解释器,不需要在线安装。
python -m pip wheel -r requirements.txt -w ./vendor python -m zipfile -c phishing-detector.zip app/ vendor/ start.pypip wheel会把依赖从缓存或远端下载成.whl文件,放到vendor/目录,目标机器离线安装时用pip install --find-links=./vendor -r requirements.txt即可。python -m zipfile -c是标准库命令,比手写压缩脚本更省事,在 Windows 和 Linux 上行为一致。打包前要把样本数据和规则文件单独放,不混进代码目录,否则每次更新都要重新打 zip。
5.1 用 FastAPI 把检测封装成 HTTP 接口
接口化让 Web 前端、SOAR 剧本或日志平台接入更方便。说明一下,这里假设你已经训练好model,并且feature_cols是训练时保存的字段顺序。
from fastapi import FastAPI from pydantic import BaseModel import pandas as pd app = FastAPI() class DetectRequest(BaseModel): url: str @app.post("/detect") def detect(req: DetectRequest): html = fetch_html(req.url) features = extract_all_features(req.url, html) sample = pd.DataFrame([features])[feature_cols] proba = float(model.predict_proba(sample)[0][1]) return { "url": req.url, "score": round(proba, 4), "risk": "high" if proba >= best_threshold else "low", }fetch_html里有网络请求,在接口层要再加超时和重试上限,否则用户传一个慢速链接会拖住进程。更稳的方案是改成异步任务,先返回任务 ID,再通过状态接口拿结果。sample这里用feature_cols做列过滤,就是为了防止前端多传字段导致 pandas 列顺序错位。
5.2 规则与词典热更新:避免每次都重新发 zip
特征库上线后要和情报变化保持同步,规则和品牌词典独立成 JSON 文件,启动时加载,运行中定期比对版本。
import json def load_rules(path: str = "rules.json") -> dict: with open(path, "r", encoding="utf-8") as f: return json.load(f)常见做法是把rules.json单独放在 zip 包外的目录,启动时用 HTTP 拉取远端版本,本地缓存一份。只要词典版本号变了,就重新加载,检测进程不需要重启。这样做之后,每次规则更新只是覆盖一个 JSON 文件,不是重新发布 zip。最好把规则同步服务和检测服务拆成两个进程,让更新失败不会影响主检测链路,重启成本才真正降下来。
本文还有配套的精品资源,点击获取