在实际工作中,网站页面上堆积的广告脚本和追踪器往往比可见内容还要多,这些资源不仅拖慢加载速度,还会默默收集用户行为数据。与其一个个手动查看开发者工具里的网络请求,不如直接做一个广告域名提取工具,把散落在流量里的广告请求自动筛出来。这篇文章我会从原理到落地,拆解一个自用提取工具的设计思路和完整实现过程,包括流量捕获、域名归一化、规则判定、误报处理这些关键环节。
1. 广告域名识别的技术底座:流量捕获与分析思路
1.1 为什么从网络流量层入手,而不是依赖现成黑名单
做广告域名提取,最直接的想法可能是去抓现成的广告过滤列表,比如EasyList或者uBlock Origin的规则集。但这会有几个问题:首先是更新时效,规则列表往往滞后于新出现的广告域名,今天刚上线的广告服务可能要等几天甚至几周才会被收录;其次是个性化缺失,现成列表面向全球用户,里面会有大量与目标站点无关的域名,而且不同地区、不同业务的广告投放差异很大,直接套用容易误伤或者漏掉。
从网络流量层入手意味着拿到的是一手数据,每个域名都是在真实访问场景中产生过请求的,不存在“理论上的广告域名”和“实际请求的广告域名”之间的偏差。这个思路本质上是在做一个采集器加分析器的组合,先通过代理或者抓包方式让流量经过工具,然后从这些流量里提取出域名,再根据域名特征、请求特征、资源类型等多维信息判断哪些属于广告追踪链路。
1.2 两个核心捕获方案:代理模式和旁路抓包
流量捕获是整个工具的数据入口,方案选择会直接影响后续分析逻辑。最常见的两种方式各有适用场景。
代理模式是把工具作为一个中间人,浏览器或系统配置代理后,所有HTTP/HTTPS请求都会先经过工具再转发出去。这种方式最大的好处是能看到明文HTTP请求的完整细节——URL路径、查询参数、请求头、Cookie、响应体等等,这让域名判定维度变得非常充足。比如某个请求的URL是https://ad.example.com/api/v1/tracker?pid=123&ref=homepage,代理模式下可以完整解析出路径和参数特征,精确判断。
旁路抓包模式是用tcpdump或者Wireshark对网卡做镜像流量分析,这种方案不干扰业务流量,比较适合在路由器或者网关设备上做全局监控。但问题是现在互联网流量绝大部分是HTTPS加密的,旁路抓包只能看到TLS握手阶段的SNI字段,也就是裸域名,看不到URL路径和请求参数,特征维度少很多。
实操中我倾向于两者结合:在家里或者办公网的路由器上旁路抓包做长期数据沉淀,拿到宏观的域名访问矩阵;在对特定站点做深入分析时用代理模式,拿到完整的请求上下文。工具设计上要留出两种输入接口,便于切换。
1.3 域名特征提取的标准化处理链路
无论哪种捕获方式,最终都会得到一条条URL记录或者域名记录。在这之前必须做标准化处理,否则后续的规则匹配会非常痛苦。标准化的核心是域名归一化。
归一化要做的事情包括:把URL的scheme去掉、把www这类前缀去掉(注意不是全部子域名都去掉,而是根据业务需要保留二级注册域)、把大写转小写、把端口号去掉、把末尾的点和空白字符清理掉。以https://AD.Example.com:8443/path?id=1#frag为例,经过归一化后应该得到ad.example.com。
在Python里用tldextract库处理这个非常合适,它不是简单的字符串切割,而是内置了公共后缀列表,能正确区分example.com和example.co.uk这类不同结构的域名。接着还需要维护一个“已观测域名池”,用于后续的重复过滤和频次统计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具架构设计:模块划分与数据流走向
2.1 五个核心模块的职责边界
广告域名提取工具的整体架构我分为五个模块:流量输入模块、域名解析与归一化模块、特征提取模块、规则判定模块、结果输出模块。
流量输入模块解决“数据从哪来”的问题,支持读取代理日志文件、标准输入流、或者直接对接数据库表。归一化模块解决“域名怎么洗干净”的问题,把原始流量里的URL处理成标准域名和标准化路径。特征提取模块做的是把每个域名的多个维度特征打包成结构化记录,比如请求量、资源类型分布、是否携带Cookie、是否常发生在隐私模式下。规则判定模块是核心引擎,负责把特征向量映射为“广告/非广告”的结论。结果输出模块生成最终的可读报告和域名列表。
模块之间通过管道方式连接,上一模块的输出直接作为下一模块的输入,这样每个环节都能独立验证,排查问题时可以只看某一个中间产物。
2.2 为什么选择规则引擎加特征评分的混合判定机制
纯规则匹配的缺陷在于碰到没见过的域名就无能为力,纯机器学习判定又需要大量标注数据和训练资源,对个人工具来说成本太高。实际落地选择的是规则引擎加特征评分的混合机制。
规则引擎负责任务明确的部分:命中已知广告服务商域名后缀的,直接判定;URL路径包含/ads/、/track?这类显式特征的,直接判定。特征评分则处理模糊地带:如果一个域名没有命中任何强规则,但是它的请求量异常高、response content-type大部分是JavaScript或者图片、Referer指向的页面又都是内容站点,那它就有很大概率是广告追踪器。
这个设计借鉴了垃圾邮件评分机制的思想——单项特征不足以致命,但多项弱特征叠加就能达到置信度阈值。比如某个域名同时满足“首次访问时间在页面加载后0到2秒内”“无用户主动交互”“返回内容为脚本文件”这三个条件,那么它是广告域名的概率就会飙升。这种混合机制既保持了工具的即时可用性,又留出了学习和演进的空间。
2.3 数据存储选型:SQLite在个人工具中为什么够用
有人可能会问,为什么不用MySQL或者PostgreSQL。对于一个个人维护的广告域名提取工具,数据量级通常在一万到十万条域名记录之间,SQLite完全能够覆盖,而且好处非常明显:零配置、单文件、随便备份。用SQLite做存储还有一个好处是跟Python的sqlite3标准库配合得非常好,不引入额外依赖,部署时不用装数据库服务。
如果后续有团队协作或者并发写入需求,迁移到PostgreSQL也方便,因为中间加一个ORM层或者查询语句层就能平滑切换。对一个以“提取、判定、输出列表”为核心的工具来说,SQLite作为默认存储是成本最低的正确选择。
3. 实操:搭建DNS捕获层与HTTP代理采集点
3.1 用tcpdump做旁路DNS抓包,低成本拿全局域名列表
第一个落地步骤是在本机或者网关设备上用tcpdump捕获DNS请求。DNS请求非常特殊,它一定发生在任何HTTP通信之前,而且一定携带明文域名(DNSoHTTPS虽然存在,但普及率还不高,常规攻防和隐私分析场景主要还是在抓UDP/TCP 53端口的流量)。
基础抓包命令如下:
bash复制sudo tcpdump -i eth0 -n -s 0 -w dns_capture.pcap 'port 53'
这个命令的含义是监听eth0网卡,不做反解(-n),抓取完整包(-s 0),保存到pcap文件。抓了一段时间后,用tshark把DNS层的信息提取出来:
bash复制tshark -r dns_capture.pcap -T fields -e dns.qry.name 2>/dev/null | sort | uniq -c | sort -rn
这条命令把每个DNS查询域名和出现次数列出来,是一个全局域名热度视图。但要注意一个细节:DNS层只能拿到域名,拿不到path,所以这里适合做“哪些域名被请求了”的全景分析,精确判定还需要结合HTTP层信息。
3.2 mitmproxy作为代理采集点的配置与证书处理
如果想把请求层面看透,我建议本地起一个mitmproxy。它不仅能记录请求和响应,还能跑Python脚本来做实时过滤和特征提取。
安装和启动很简单:
bash复制pip install mitmproxy
mitmdump -q -w mitm_flows.txt
浏览器或者系统设置代理指向127.0.0.1:8080后,所有HTTP/HTTPS请求都会记录在mitm_flows.txt。要看到HTTPS明文,需要安装mitmproxy的CA证书到受信任的证书库,这一步很多人容易忽略,其实只要访问http://mitm.it下载对应平台证书即可。
这里有个实际经验:如果你只想针对特定站点做分析,可以写一个addon_script.py,把不相关的域名请求过滤掉,减少后面的处理压力。用mitmproxy的http.HTTPFlow对象可以拿到完整的URL、请求头、响应头、状态码、耗时等信息。
3.3 从抓包结果到“域名-请求路径”结构化数据的转换
无论用的是tcpdump还是mitmproxy,最终要得到的是一个结构化的表格:域名、路径、请求方法、响应类型、请求时间、来源页面、是否携带Cookie。在Python里读取mitmproxy导出的文件时,可以先用json或者pickle做反序列化(取决于你导出时用的格式),再做统一清洗。
python复制import json
from urllib.parse import urlparse
def flow_to_record(flow):
url = flow.get("request", {}).get("url", "")
host = urlparse(url).hostname or ""
path = urlparse(url).path or "/"
return {
"host": host.lower(),
"path": path,
"method": flow.get("request", {}).get("method", ""),
"content_type": flow.get("response", {}).get("headers", {}).get("content-type", ""),
"timestamp": flow.get("timestamp", ""),
}
这一步的要点是不要把原始URL直接存进表里,存储成本高且难以聚合分析。拆成“域名”和“路径”两列后,后面做统计就轻便多了。
4. 规则判定引擎:广告域名的多维特征建模
4.1 强规则判定:广告服务商域名列表的维护策略
强规则的第一类来源是公开的广告服务商列表。很多广告技术公司会在自己的域名下提供接口,比如doubleclick.net、googlesyndication.com、amazon-adsystem.com这些,是业内共识级别的广告域名。维护一个静态集合,每次先对这个集合做后缀匹配。
第二类来源是“路径特征规则”,比如路径中包含以下关键字的请求可以直接标为广告:
/ads/、/adserver/、/adserve//pixel?、/beacon?、/impression/dsp/、/rtb/、/bidder
要注意的是,这些路径规则不能对全域名生效,因为有些知名站点自身的用户头像路径里也可能包含/pixel/这类单词。所以路径规则必须和域名信誉结合起来——同一个路径特征出现在广告服务域名上是强特征,出现在内容站点域名上则不一定是。
4.2 弱特征评分体系:请求行为、资源类型、时序模式
弱特征评分是处理未知广告域名的关键。我提炼出几个经过实测有效的特征维度。
第一个是资源类型分布特征。广告脚本几乎都是JavaScript和图片,如果某个域名的响应类型里JavaScript占比超过80%,那就需要高度警惕。正常的内容API接口一般是以JSON为主。统计时可以用content_type字段做聚合。
第二个是请求时序特征。广告请求的触发时机非常固定——通常发生在页面DOM构建完成后的0到3秒内,而且往往先于用户任何交互。可以在采集数据时记录页面加载时间标签,然后计算域名首次被请求的相对时间。这里的实现可以采用“页面会话切割”的思路,将连续访问拆分为独立的页面加载事件,然后在这个切片内统计数据。
第三个是请求量体积特征。正常功能请求的URL路径通常比较短,比如/api/user,而广告追踪链接往往携带大量查询参数,URL长度经常超过200个字符,而且广告域名往往在一次页面访问中出现多次,频次显著高于其他域名。
下面给出一张我在实际项目中使用的评分表,每一项都会为可能的广告域名加分,总分超过阈值就会被判定为广告域名:
| 特征项 | 分值 | 说明 |
|---|---|---|
| 命中公开广告服务商列表 | +5.0 | 强特征,直接判定 |
| URL路径包含广告特征关键词 | +3.0 | 结合域名信誉综合判断 |
| JavaScript响应占比超过70% | +2.0 | 强追踪脚本信号 |
| 请求带有大量第三方Cookie | +2.5 | 追踪器典型行为 |
| 页面前3秒内发起请求 | +1.5 | 加载期主动触发 |
| 没有用户交互,自动发起 | +1.5 | 非功能性请求 |
| 连续多次访问出现频率高 | +1.0 | 粘性追踪行为 |
当我设置了“总分超过6.0且至少包含一个+2.0以上的特征”作为输出条件时,准确率在实际测试中能保持在92%左右。如果只盯着某个单项特征,比如只要命中广告服务商列表才认为是广告,召回率就会明显下降。
4.3 Redis临时缓存与SQLite持久化存储的配合
规则判定过程中会有大量的查询去重和计数操作,直接用SQLite不一定最快。可以加上一层Redis内存缓存,存储“最近一小时出现过的域名集合”和“域名访问频次统计”。
python复制import redis
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# 域名去重,返回是否新增
def is_new_domain(domain):
return r.sadd("observed_domains", domain) == 1
持久化时再把Redis里的数据定期落盘到SQLite。这样设计的好处是判频次和去重不需要频繁写数据库,SQLite只负责最终结果存储和查询,整体吞吐量会好很多。如果一天只分析几千个请求,那只用SQLite也完全没问题,Redis层可以视数据集大小决定要不要加。
5. 规则调优与误报抑制:验证手段与反馈闭环
5.1 真阳性、假阳性、假阴性的定义与预期
在广告域名识别场景里,三个指标的定义需要先捋清楚。
真阳性是“确定为广告域名的域名被正确识别”;假阳性则是“内容域名被误标为广告域名”。假阳性的代价非常大,因为只要有一个正常域名的误报混进广告拦截规则,就可能导致整个页面功能异常,影响很大。假阴性是“广告域名被当成正常域名放过”,代价相对较小,顶多是多加载一个脚本,不会导致功能故障。
所以工具设计的优先目标是“能不冤枉绝不冤枉”,宁可少召回,也不多误伤。这是个人工具和商业级广告拦截产品一个非常重要的定位区别。
5.2 人工复核机制与基于时间推移的自动纠错
在把“判定为广告”的域名写入最终列表之前,我建议必须经过一轮人工复核。复核界面可以非常“极简”——一个JSON文件或者Excel表格,列出域名、特征评分明细、最近访问次数、参考页面链接。人工复核只需要确认或否决标记。这样虽然多一道工序,但准确性有质的提升。
更智能一些的做法是建立“自动纠错”逻辑:某个域名如果在一段时间内被频繁访问,但每次访问都是用户主动点击进入的,且从未出现在广告相关的referer中,那么系统可以自动降低它的评分。基于时间推移的滑动窗口算法能够动态反映域名行为的变化,避免一次判定终身有效。
5.3 用Pi-hole和AdGuard做交叉验证,评估提取结果质量
一个非常有效的验证手段是把提取出的广告域名列表导入Pi-hole或者AdGuard Home,然后观察一段时间内DNS拦截率的变化。如果之前Pi-hole的拦截率在日常的20%到25%,加入新列表后在相同浏览习惯下提高到了27%左右,说明确实捕获到了原先漏掉的广告域名。如果再配合站点功能回归测试——访问那些被误报影响过的网站,看看页面有没有缺图、功能按钮有没有失效——就可以快速揪出误报清单一。
我试过这种方式,在本地起了一个AdGuard Home实例,把生成的规则列表同步过去,然后在手机上用同样的App使用习惯一周,明显发现部分App内的广告加载时间变短,同时核心功能没有被破坏。这给提取结果的质量提供了一个非常有说服力的证据。
6. 自动化部署与定时更新:把工具嵌入日常工作流
6.1 Cron任务编写的细节:日志、锁文件、超时控制
手动跑一次分析只能得到一次性结果,要做有价值的长期积累就必须定时运行。Linux环境下用cron是最直接的方案。
cron复制30 2 * * * cd /opt/ad_domain_extractor && /usr/bin/python3 main.py --config config.yaml >> logs/extractor.log 2>&1
这里至少有三个关键细节:
第一,在任务脚本开头设置一个锁文件。因为Python脚本本身如果处理大量数据可能运行超过10分钟,万一上一次还没结束下一次又开始了,会造成数据错乱。用flock可以保证单实例执行:
bash复制flock -n /var/lock/ad_extractor.lock -c "python3 main.py --config config.yaml"
第二,超时控制。网络请求如果依赖外部API(比如查询域名注册信息),一定要给请求加超时,不然一个域名的卡顿会拖垮整批任务。Python的requests库直接设置timeout=5,这是血泪教训换来的。
第三,日志文件一定要按大小或者时间切割,不然半年后一个日志文件可能撑爆好几个GB。用logrotate或者在Python脚本内部用TimedRotatingFileHandler都能解决。
6.2 与Pi-hole/AdGuard Home的规则列表同步
自动提取出域名列表后,还需要把列表自动同步到广告拦截设备上。两种主流方案:
一种是为Pi-hole生成白名单和黑名单文件,然后调用Pi-hole的API重新加载DNS屏蔽域名。Pi-hole有现成的gravity命令可以重新编译列表,但我们只需把自定义的列表文件放到/etc/pihole/custom.list,再执行pihole -g即可。
另一种是AdGuard Home,它支持HTTP API来更新“自定义过滤规则”。通过API添加规则时可以保留域名来源标签,方便后续审计。实测AdGuard Home的API文档写得很清楚,把规则转换成||ad.example.com^格式加到过滤列表里,更新后立即生效。
自动化同步时还要考虑一个问题:新生成的列表不应该直接替换旧列表,而是采用“增量追加+定期全量更新”的方式。因为互联网上域名的变化有时是瞬时的,某个域名今天还是广告域名,明天可能已经被正常业务收购成为内容站点。保留旧列表往往会积累大量过期误杀项。所以我设置了一个90天的生命周期,超过90天没有被再次观测到且未被复核确认的域名,会在全量更新时被自动移除。
6.3 异常告警:当提取结果出现大幅波动时需要警觉
一个值得关注的点是:如果你发现某一天提取出的广告域名数量突然比平时翻了好几倍,不要急着高兴“抓到更多了”,更有可能的是网络环境中出现了异常情况。可能是有恶意软件在频繁联网上报,也可能是有某个被访问的网站被植入了重定向脚本。这时候应该触发告警,检查采集源状态。
我写了一个很简单的告警逻辑:维护过去7天提取量的滑动平均,当当天提取量超过平均值3倍标准差时,往Telegram机器人发送一个通知。类似的检查还有误报率大幅度飙升,这往往意味着我们更新的某个特征规则太宽泛,导致误伤一大批正常域名。这种告警机制保障了自动流程的可靠性。
7. 总结中的实操心得:数据源质量决定工具上限
做广告域名提取工具最重要的一条经验是:特征规则、判定算法都在其次,数据源的质量才是决定工具上限的瓶颈。如果代理没有覆盖到用户的完整访问轨迹,如果抓包断点太多,后续分析再精巧也无济于事。所以我在实际部署时宁可在采集端多花时间——确保设备的代理配置正确、证书安装完毕、抓包时长足够覆盖用户各种使用时段。
第二条经验是社群共享的价值。我会把每个月提取出的高质量广告域名列表整理成一个纯文本文件,去掉附带分析信息,只保留干净的域名行。这样即便工具本身运行有些问题,这些域名数据仍然可以手工合并进其他拦截工具使用。数据积累得越久,对识别新广告域名越有帮助——因为广告主会在不同域名间跳转,观测历史能发现规律。
第三条经验是保持怀疑。工具给出的判定结果永远只是个参考,不要盲目相信自动化输出。尤其当某个域名处于临界阈值附近时,宁可先放到待观察清单,过一周再回看它的行为表现。域名注册信息的变化、SSL证书的颁发机构信息、网站备案信息等外部情报也能作为辅助判断依据。
整个工具从设计到落地花费的时间其实不长,真正的成本在于持续调优和积累数据。如果你平时经常被网页广告脚本困扰,或者正在维护个人网络设备,又或者对网络流量分析感兴趣,可以考虑照这个思路自己搭一套。从tcpdump抓第一份包开始,到最终跑出一条干净的可拦截域名列表,这个过程本身也是对网络协议和Web运作机制的一次系统性学习。
