新闻详情

新闻详情

首页 / 资讯中心 / 详情

恶意网站检测实战:基于特征工程与LightGBM的白盒方案

发布时间:2026/9/16 6:34:04来源:尧图网络
恶意网站检测实战:基于特征工程与LightGBM的白盒方案
简介基于传统机器学习的恶意网站检测算法源码包面向计算机、人工智能、大数据等专业的学生尤其适合正在准备课程设计、期末大作业或毕业设计的读者。项目围绕恶意网站识别问题采用支持向量机SVM、深度神经网络DNN、随机森林等经典算法配合数值类型转换、类别映射等数据预处理脚本构成从特征处理到模型训练与评估的完整流程代码已经过调试可直接运行适合具备一定编程基础的学习者阅读、复现并做二次改进。压缩包共 7 个文件以 5 个 Python 脚本为主体覆盖数据处理、模型构建和结果输出等环节另含一个数据集压缩包和 README 项目说明整体体积仅 3.31MB结构清晰便于按需查看。目前已有 148 人学习下载既可作为传统机器学习落地的参考样例也能帮助对比各算法在恶意网站检测任务上的表现为后续研究或项目报告提供实验基线与扩展思路。1. 恶意网站检测用传统机器学习先特征工程再谈模型一个反直觉的事实是手头没有浏览器内核、没有情报库、也没有 GPU仅凭一条 URL 字符串、几条 DNS 解析记录和一次 HTTP 请求回来的页面字节流就能训练出 AUC 超过 0.95 的恶意网站检测模型。这里的关键不在模型选得多新而在于把“像不像恶意网站”这件事拆成一组可复现的特征再交给梯度提升树去拟合边界。标题里那种“算法源码项目说明”的压缩包拆开之后通常就是三样东西特征工程脚本、模型训练脚本、一个可调用的预测函数。读完这篇你能按同样的结构自己搭一套可离线运行、可解释、可迭代的白盒检测方案适合安全工程师、服务端开发以及想给网关或爬虫加一层 URL 前置判断的团队。2. URL 字符串、域名与页面内容检测特征从哪来2.1 URL 字符串特征的提取与清洗恶意 URL 和正常 URL 在字符串形态上是有统计差异的这是特征工程的第一层也最容易被做浅。常见的错误是只数长度和点数丢掉了大量可用的表层信号。我一般会先把 URL 拆成 scheme、host、path、query 四段再分别计算统计量因为“host 的攻击性”和“path 的攻击性”含义完全不同。比如digits_ratio这个特征放在 host 上是高风险信号放在 path 上就不一定。import math import re from urllib.parse import urlparse def host_entropy(host: str) - float: 字符串信息熵用于刻画随机生成域名的无序程度 if not host: return 0.0 freq {} for ch in host: freq[ch] freq.get(ch, 0) 1 length len(host) entropy -sum((count / length) * math.log2(count / length) for count in freq.values()) return round(entropy, 4) def url_string_features(url: str) - dict: u urlparse(url) host u.netloc.split(:)[0] path u.path or query u.query or full_text url.lower() ip_flag 0 if re.match(r^\d{1,3}(\.\d{1,3}){3}$, host): ip_flag 1 sensitive_words [login, verify, confirm, webmoney, free-gift, banking] return { url_len: len(url), host_len: len(host), path_len: len(path), query_len: len(query), path_depth: len([seg for seg in path.split(/) if seg]), subdomain_count: len(host.split(.)) - 2 if len(host.split(.)) 2 else 0, digits_ratio: sum(ch.isdigit() for ch in full_text) / max(len(full_text), 1), special_char_ratio: sum(not ch.isalnum() for ch in full_text) / max(len(full_text), 1), max_consecutive_digits: max((len(m) for m in re.findall(r\d, host)), default0), host_entropy: host_entropy(host), is_direct_ip: ip_flag, sensitive_word_hits: sum(1 for w in sensitive_words if w in full_text), has_query: 1 if query else 0, }这段代码里有几个参数值得单独说。subdomain_count用len(host.split(.)) - 2估算子域数对常规域名是准的但遇到带端口或畸形输入会偏所以上游必须先做校验保证host来自urlparse且不含端口。max_consecutive_digits捕捉的是类似“数字随机串”的域名生成算法特征这类域名经常拼一串连续数字来绕过人工审核。sensitive_word_hits的词表只是示例真正上线时应该从历史已确认的恶意样本里统计 top 词而不是手工维护。直接对full_text做子串匹配的方式简单但会误伤“login”这种正常词后续可以通过模型权重自动发现哪些词在负样本里更常见。2.2 域名年龄、DNS 解析数与 WHOIS 时态特征URL 字符串特征解决的是“看起来像不像”而域名解析和注册信息解决的是“这个域名是不是刚冒出来的”。恶意站点的典型特征是生命周期短域名在恶意行为曝光前几小时到几天才注册DNS 解析记录少且经常变更。这类信息的获取成本比页面抓取高但对检测效果的提升非常明显尤其是针对钓鱼站点。我在实际工程里会从三个来源拉特征DNS 解析A 记录数量、解析结果是否变化、TTL 值、是否命中动态解析特征。WHOIS域名注册距今天数、过期时间与当前时间距离、注册者是否开启隐私保护。历史情报当前解析 IP 是否曾在恶意样本中出现过本地用 IP 哈希集合即可不需要外部接口。def dns_whois_features(domain: str) - dict: 封装 DNS 与 WHOIS 查询超时均控制为 2 秒。 外部服务不稳定时返回全部 None由调用方决定填充策略。 import socket import whois try: answers socket.getaddrinfo(domain, 80, protosocket.IPPROTO_TCP) ips {item[4][0] for item in answers} a_count len(ips) except Exception: ips, a_count set(), None try: w whois.whois(domain) creation w.creation_date if isinstance(creation, list): creation creation[0] days_since_creation (datetime.now(tztimezone.utc) - creation).days if creation else None except Exception: days_since_creation None return { dns_a_count: a_count, dns_ttl_mismatch: None, whois_days_since_creation: days_since_creation, whois_privacy_enabled: None, }这里要重点提醒一个工程问题WHOIS 查询是阻塞且慢的稍大一点的并发就能把特征服务打挂。常见做法是加一个进程内 TTL 缓存比如对同一域名 24 小时内不重复查 WHOISDNS 结果缓存 10 分钟。代码里的whois_privacy_enabled在多数查询接口里拿不到稳定值我一般会置空而不是猜一个默认值填进去。模型侧对这类特征要允许缺失并在训练时把缺失值作为独立的分支条件交给树模型处理。2.3 页面内容与脚本行为的轻量特征第三层特征是页面内容。这里的原则是能不用浏览器渲染就不用浏览器渲染。无头浏览器在网关场景下太重且目标站点的反爬机制会拖慢整个链路。我只做一次带超时的原始 HTTP 请求抓取前 200KB 内容从中提取脚本密度、外链域名分布、跳转次数和敏感表单特征。这部分代码要能容忍各种脏数据非 UTF-8 编码、截断的 HTML、压缩响应头不一致等。def page_content_features(html: bytes) - dict: text html.decode(utf-8, errorsignore) length max(len(text), 1) features { html_len: len(html), script_ratio: text.count(script) * len(script) / length, iframe_count: text.count(iframe), hidden_input_count: text.count(typehidden), password_input_count: text.count(typepassword), external_domain_count: 0, redirect_count: text.count(location.href) text.count(window.location), } features[entropy] host_entropy(text[:8192]) return features参数说明script_ratio的分母用文本长度而不是字节数是因为编码差异会让字节数失真按字符统计对中文页面更公平。external_domain_count在示例里被置为 0实际实现时需要用正则提取src//xxx、href//xxx里的域名再和主域名比较。这个特征对 C2 页面和挂马页很有效因为它们经常内嵌多个外部资源域名而正常业务站点绝大多数资源来自自家 CDN 或固定域名。需要注意redirect_count匹配的是字符串模式如果页面用前端框架渲染跳转例如 Vue Router 的router.push这里会漏检这是轻量方案的固有边界不要试图用正则覆盖所有前端框架行为。2.4 特征表与缺失值策略训练和推理共用同一份源码三层特征加到一起最终落到一张定长的表里。下面这个表是我在项目里通常保留的最小特征集作为通信约定直接绑定 URL 检测模块和上游调用方。每一条都会写清楚缺失时怎么办否则上线后会有人问你“这个字段为什么是 0”。特征层特征示例缺失值处理对检测的作用URL 层url_len、host_entropy、is_direct_ip、sensitive_word_hits直接计算不可能缺失区分随机域名与人工注册域名域名层whois_days_since_creation、dns_a_count、dns_ttl_mismatch固定填训练集中位数另加 is_missing 标志列识别短生命周期域名内容层script_ratio、iframe_count、redirect_count抓取失败全填 0加 page_fetch_failed 标志识别挂马页与钓鱼表单这里有个值得注意的做法缺失值不统一填 -1而是“数值填中位数/0同时加一列whois_missing_flag”。树模型虽然天然支持缺失但 LightGBM 默认会把缺失值统一分到最优方向这可能掩盖“查询失败”和“真实值为 0”的语义差异。加一个显式标志列让模型自己学习“WHOIS 都查不到”这一事件本身的含义效果通常更好。3. 选树模型而非深度学习恶意网站检测算法与调参3.1 为什么默认先试 LightGBM 而不是神经网络特征维度在几十到一两百之间样本量在几十万量级这种表格数据场景下梯度提升树是性价比最高的选择。我的默认选项是 LightGBM原因有三个它对缺失值和离散特征不敏感训练吞吐高且自带特征重要性输出方便安全团队解释检测结果。“传统机器学习”在这里并不是退而求其次反而是刻意选择安全场景里误报的定位成本很高模型能不能回答“为什么拦截这个 URL”比再涨 0.001 的 AUC 更重要。同一个数据集换成神经网络需要做特征缩放、Embedding 编码、更细致的验证集设计收益在多数场景下达不到工程成本的量级。只有当你能拿到序列化输入比如完整的 JavaScript 代码 token 流时深度学习方案才值得重新评估。3.2 类别不平衡与按域名分组的交叉验证恶意 URL 在真实流量里的占比通常远低于 1%直接拿原始分布训练会让模型把几乎所有样本都预测成负类。处理时我先做两件事一是负样本不用全网爬虫的 URL而是用户真实触达的 URL因为爬虫样本与恶意样本的分布差异会造成严重过拟合二是按域名做分组交叉验证而不是随机打散。同一个域名的多个 URL 之间有强相关性随机切分会造成数据泄露验证集上的指标会虚高 3 到 5 个点。import lightgbm as lgb import pandas as pd from sklearn.model_selection import GroupKFold feature_cols [col for col in df.columns if col.startswith(url_) or col.startswith(whois_) or col.startswith(dns_) or col.startswith(page_)] gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(df, df[label], groupsdf[host]): train_df, val_df df.iloc[train_idx], df.iloc[val_idx] d_train lgb.Dataset(train_df[feature_cols], train_df[label]) d_val lgb.Dataset(val_df[feature_cols], val_df[label]) model lgb.train( params, d_train, num_boost_round5000, valid_sets[d_val], callbacks[lgb.early_stopping(200), lgb.log_evaluation(100)], ) break # 演示只跑第一折正式训练请保留所有折这段代码的要点在GroupKFold而不是默认的KFold。groupsdf[host]保证同一个域名的所有页面只会出现在训练集或验证集之一。如果你在某个开源“源码笔记”里看到随机切分训练集的做法要警惕它的线上指标那很可能是过拟合的假象。3.3 核心参数与早停策略我常用的参数表如下按经验排序不是按官方参数顺序。参数取值作用说明learning_rate0.03偏低的学习率配 5000 轮迭代早停更稳定num_leaves31与树深度 2^depth 解耦控制在 32 以内防过拟合min_child_samples40叶节点最少样本数调高 20-50 显著减少噪声分支feature_fraction0.8列采样对密集特征矩阵有明显泛化收益bagging_fraction0.8行采样配合 bagging_freq1 使用reg_lambda1.0L2 正则特征相关性高时加大到 5-10scale_pos_weight20-100正负样本比的倒数先小后大验证集上微调早停轮数我固定给 200。learning_rate0.03意味着模型需要较多轮数才能收敛如果早停设成 50很可能在验证集 AUC 还没爬过峰值时就停了。scale_pos_weight不是唯一处理类别不平衡的手段相比直接对负样本降采样它的好处是保留了全量样本的信息代价是概率输出偏置——之后必须重新校准阈值不能直接用 0.5。3.4 阈值怎么定基于误报成本而不是 AUC训练结束后最关键的一步是确定判定阈值。误报在安全场景里的代价是真实的工单和用户投诉所以阈值要按业务成本来定不是一个固定值。fp_cost 1.0 # 误报告警的运营成本 fn_cost 50.0 # 漏报恶意站点的潜在损失 scores model.predict(val_df[feature_cols]) best_thr 0.5 best_cost float(inf) for thr in [x / 20 for x in range(1, 20)]: y_pred (scores thr).astype(int) fp_count ((y_pred 1) (val_df[label] 0)).sum() fn_count ((y_pred 0) (val_df[label] 1)).sum() total_cost fp_count * fp_cost fn_count * fn_cost if total_cost best_cost: best_cost total_cost best_thr thr这个成本函数的参数需要由运营方拍脑袋提供不要自己定。技术侧只需要保证阈值改变时模型打分本身保持稳定。4. 实时判定服务的实现与常见坑4.1 黑白名单前置与两阶段判定把模型直接暴露在流量链路上之前一定要做黑白名单前置。白名单的价值大于黑名单企业自己的域名、长期运营的知名站点、CDN 节点域名这些根本不需要让模型判断。黑名单则用于“一票否决”命中即拦截不进特征计算。我在项目里会用 Redis 存黑白名单白名单只存域名主键和过期时间避免缓存污染。层级顺序是白名单放行 → 黑名单拦截 → 模型打分 → 阈值判定 → 归档和人工复核。这个顺序不能反否则攻击者可以用大量低分命中加高成本误报拖垮你的特征服务。4.2 推理接口设计与超时控制特征计算在线上的最大风险是外部依赖不可用。DNS 和 WHOIS 都可能阻塞数秒页面抓取更不可控。我会把每个环节的耗时上限明确写进代码宁可牺牲一点特征完整度也要保证判定在 300 毫秒内返回。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class UrlRequest(BaseModel): url: str app.post(/predict) def predict(req: UrlRequest): url req.url start time.time() features compute_features(url, timeout_ms150) vector [features.get(col, 0) for col in feature_cols] score float(model.predict([vector])[0]) level block if score block_thr else monitor if score watch_thr else pass return {url: url, score: score, level: level, elapsed_ms: int((time.time() - start) * 1000)}compute_features内部要分别对 DNS、WHOIS、页面抓取做超时控制而不是依赖 FastAPI 的单层超时。单层超时的问题是线程可能早已卡在 socket 等待上后续请求堆积在线程池里最终把服务搞挂。常见做法是把外部调用全部放进ThreadPoolExecutor用future.result(timeout...)强制中断。4.3 特征口径不一致训练和线上必须共用同一个函数这是最隐蔽的坑。训练脚本里的特征函数和线上推理代码只要分家哪怕只差一次round()操作线上预测的分值分布就会偏移。我在项目里要求feature_engineering.py同时被训练脚本和推理服务 import而不是各自维护一份副本。如果必须拆开部署就在 CI 里加一个校验任务读取同一批 URL比对两份函数输出的 JSON 是否完全一致。另一个容易忽略的口径问题是时区。WHOIS 的creation_date在不同注册局返回的时区不一致统一转成 UTC 再计算天数。DNS TTL 值在某些运营商那里会重写这类字段建议只用于统计建模不要直接作为判定阈值。4.4 新域名冷启动兜底新注册域名几乎没有 DNS 历史WHOIS 也可能查询失败模型输出的分布会整体偏向中间分数。我的兜底策略是对“域名注册时间 7 天且找不到任何解析历史”的 URL即使分数没超过block_thr也强制进入人工复核队列而不是直接放行。这一步不能用规则替代模型但可以作为模型之外的第二道闸门。5. 误报治理用 SHAP 解释单条判定把误报送回去重训5.1 用 SHAP 输出单条 URL 的决策依据恶意网站检测最消耗精力的不是调模型而是给运营解释“为什么拦截了这条 URL”。我在推理接口里回传的不仅是分数还有该样本的 top 特征。用 LightGBM 模型可以直接用shap.TreeExplainer它对树模型是精确计算不是近似估计。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(vector)[0] reason_items sorted(zip(feature_cols, shap_values), keylambda x: -abs(x[1]))[:5] return {url: url, score: score, reasons: reason_items}这里的reason_items形如[(host_entropy, 1.32), (whois_days_since_creation, -0.41)]运营看到的是“域名熵值偏高”而不是一串浮点数。还需要提醒一句SHAP 值衡量的是特征对本次预测的贡献不直接等于因果。某些特征相关性高时SHAP 可能在两个强相关特征间分配贡献所以只看 top 特征时偶尔会出现“两个互斥原因同时存在”的错觉这属于正常现象。5.2 把误报归档成最廉价的重训样本每条人工确认为误报的样本都值得被记录下来。我在工单系统里会要求追加三个字段原始 URL、推理时的特征向量快照、SHAP top 特征。原因很简单误报样本是特征漂移最敏感的探测器集群误报往往先于模型指标下降出现。misclassified pd.DataFrame([ {url: url, label: 0, host_entropy: feats[host_entropy], whois_days_since_creation: feats[whois_days_since_creation], script_ratio: feats[script_ratio]} ]) misclassified.to_parquet(fp_archive.parquet, enginepyarrow, indexFalse)重训前我会把这份误报归档作为追加样本并进原始训练集且赋 2 倍样本权重让模型更重视“我曾经错过的边界”。同时单独留出一周的新误报不参与训练用它验证这轮重训是否真的压低了误报率。误报治理到这里就成一个闭环了特征不变、代码不变、数据不断累积重训周期可以从月缩到周长期效果比频繁调参更稳定。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

粒子群算法优化RSSI定位的Matlab实现 2026/9/16 7:13:07

粒子群算法优化RSSI定位的Matlab实现

1. 项目概述:粒子群算法在RSSI定位中的优化实践在无线传感器网络定位领域,RSSI(Received Signal Strength Indicator)测距技术因其低成本、易实现的特性被广泛应用。但环境干扰导致的信号波动问题始终是精度提升的瓶颈。去年我在某…

阅读更多 →
Python+OpenCV实现高效批量图像处理与智能抠图 2026/9/16 7:13:07

Python+OpenCV实现高效批量图像处理与智能抠图

1. 图像处理效率提升的核心痛点在数字内容爆炸式增长的今天,图像处理已成为设计师、自媒体从业者和电商运营人员的日常刚需。但传统单张处理的方式在面对上百张产品图、活动海报或文章配图时,往往让人陷入重复劳动的泥潭。我曾为一家电商代运营公司优化工…

阅读更多 →
Kubernetes离线部署指南:kubeadm+containerd内网集群搭建全流程 2026/9/16 7:13:07

Kubernetes离线部署指南:kubeadm+containerd内网集群搭建全流程

Kubernetes离线部署这件事,放在开发测试环境里,基本是每个团队迟早都会撞上的一道坎。很多项目的研发网段完全隔离,或者企业内部对系统外联有严格约束,apt、yum、docker pull这些日常操作全被卡死,可是K8s从系统依赖到…

阅读更多 →
2026杭州哪家GEO服务商靠谱?杭州巨宇科技凭硬核实力上榜 2026/9/16 7:13:07

2026杭州哪家GEO服务商靠谱?杭州巨宇科技凭硬核实力上榜

2026年,当企业主们把“杭州哪家GEO优化服务商靠谱”这个问题从搜索引擎搬进豆包、DeepSeek时,一场关于品牌可见度的新排位战已经开打。在多家第三方测评与行业榜单中,杭州巨宇科技信息服务有限公司(以下简称“杭州巨宇科技”&…

阅读更多 →
ESP32-S3 N16R8开发实战:PlatformIO+PSRAM工程化指南 2026/9/16 7:13:07

ESP32-S3 N16R8开发实战:PlatformIO+PSRAM工程化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
【NeurIPS 2025 (Spotlight)】G-Memory 论文解读:多智能体三层图记忆,把协作轨迹变成可检索经验|从多智能体记忆架构视角 2026/9/16 7:10:07

【NeurIPS 2025 (Spotlight)】G-Memory 论文解读:多智能体三层图记忆,把协作轨迹变成可检索经验|从多智能体记忆架构视角

摘要 本文解读 NeurIPS 2025 Spotlight 论文《G-Memory: Tracing Hierarchical Memory for Multi-Agent Systems》。该论文提出三层图记忆架构 G-Memory,通过融合insight graph(可泛化洞见)、query graph(查询与任务状态&#xf…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞