广告验证抓取需要区域与语言保持一致
区分哪些广告抓取差异来自出口区域、哪些来自语言列表、哪些两者都不是,并在每次抓取时记录这份对齐关系。
BotBrowser Team
广告验证抓取回答的是一个很窄的问题:带着这些声明属性的请求,在这个区域收到了什么创意、落到了什么落地页。BotBrowser 文档说明了由代理推导的地理默认值,以及对手动设置时区、区域设置和语言的覆盖方式,文档注明这类手动覆盖需要 ENT Tier1 许可,因此一次抓取所声称的区域可以被显式声明,而不必继承自运行它的机器。它不负责生成广告流量,也不参与竞价,不能保证广告服务器返回什么;每次抓取仍然取决于平台条款是否允许。
摘要
- 抓取代表什么,由两个输入决定:请求从哪里出去,以及浏览器声明什么语言。两者不一致时,这次抓取不描述任何一个连贯区域。
- 出口区域变化可能改变请求收到的创意;声明语言变化可能改变请求被送往的落地页。
- 两者都没改变的不一致依然让比较失效,因为两次抓取不再描述同一个请求。
- 抓取前先跑一次对齐检查,并把出口 IP 类别、时区与语言列表随每次被接受的抓取一起存下来。
ads.txt声明的是发布方授权哪些系统销售其库存,它对一次抓取是否被允许没有任何说明。
目录
一次抓取真正声明的两个输入
任何 HTTP 请求携带的都不只是一个 URL。它带着 Accept-Language,用来列出客户端偏好的自然语言;它也来自一个网络地址,服务器或第三方可以把该地址解析成大致位置。RFC 9110 把这个头定义为响应中所偏好的自然语言集合,并把 Vary 定义为告诉缓存哪些请求字段影响了所选表示。当响应包含 Vary: accept-language 时,服务器是在声明语言选择参与了这次选择。
MDN 的参考补充了一个对抓取工作很关键的细节:浏览器用当前的界面语言生成该头,而 Accept-Language 与 navigator.languages 都可能出于防指纹目的有意省略用户偏好列表的一部分。因此一次抓取观察到的是声明出来的偏好列表,而不是一个人完整的语言画像。
网络侧是另一件事。代理或直连决定了请求看起来来自哪个地址,BotBrowser 的 GeoIP 指南描述了把这个出口 IP 翻译成时区、区域设置、语言和近似坐标等地理默认值,并说明手动参数优先于推导值。文档也直白地给出了精度边界:国家级映射通常可靠,城市级映射可能落到邻近城市或区域。
由此有两个推论。第一,一次抓取可能内部自洽,也可能不是:语言列表和时区可以与出口区域一致,也可能描述一个请求并不来自的地方。第二,对齐是抓取的属性,而不是浏览器二进制的属性,所以必须记录,不能假定。
不一致会改变什么,不会改变什么
有用的划分不是「一致还是坏了」,而是「这次不一致到底改变了响应里的什么」。广告系统各不相同,因此下表列出三个不一致类别和一行对照,以及每类能支持什么观察。它是分类辅助,不是对任何具名广告服务器行为的断言。
| 抓取条件 | 观察到的创意 | 观察到的落地页 | 这次抓取能支持什么 |
|---|---|---|---|
| 出口区域变化,语言列表固定 | 可能不同 | 可能不同 | 两个区域观察;彼此都不是对方的基线 |
| 语言列表变化,出口区域固定 | 可能不同 | 可能不同 | 同一区域内的内容协商观察 |
| 语言列表与时区一致,出口不同 | 未观察到变化 | 未观察到变化 | 无结论:请求不描述同一个地方 |
| 对照:三者一致,运行之间来源变了 | 可能不同 | 可能不同 | 来源侧变化,而非区域效应 |
第一行最容易被误读。团队把浏览器语言列表保持在 en-US,en,把出口从一个国家换到另一个国家。如果创意变了,诱人的结论是广告系统按地理定向。这个观察只支持「两次抓取不同」,而差异同样可能来自排期、预算调整、频次规则或区域同意状态。要分开这些原因,需要在同一时间于原区域再抓一次,而不是对前两次做更强的断言。
第二行正相反。出口固定,把语言列表从 en-US,en 改成 de-DE,de,可能改变请求被重定向到的落地页,因为语言协商正是 RFC 9110 所描述的内容。于是落地页差异成了一个已声明改动的预期结果,而不是任何异常的证据。
第三行最浪费时间。什么都看不见变化,抓取看起来正常,两个月后这组数据被用来把一个市场和它自己比较。语言列表指向一个区域、请求却从另一个区域出去的抓取,两个区域都不描述,因此不属于任何一组序列。
第四行覆盖真正的来源侧变化。如果三个输入完全相同,创意仍然不同,差异就在广告系统自己的状态里:定向改动、投放排期、频次上限或实验分组。正确记录对齐,正是让这个结论成为可能的原因,因为它排除了抓取本身作为解释。
同意状态是最常躲过前三行的混淆因素。来自已生效同意框架国家的请求,可能返回不同创意或在落地页之前插入一个中间页,而这种差异看起来像区域定向,实际是监管门槛。由于门槛通常按区域而非按语言触发,比较语言列表抓不到它。请单独保留一个字段,记录是否发生了同意交互以及处于什么模式;混有门槛与无门槛运行的序列不是区域序列,无论它的地理输入怎么说。
频次与投放排期会在多次运行之间制造类似陷阱,而不是在一次运行内部。广告活动可能对某个标识符限制创意展示次数,因此从同一个配置文件抓取的序列会看到创意消失,而区域与语言都没变。这是否适用于验证抓取,取决于系统把上限绑定到什么标识符,那是平台问题而不是浏览器问题。实践上的防御不变:记录对齐、保留观察时间,并且在来源本身就能产生差异时,不要把无法解释的差异归因于地理。
在第一次抓取之前跑对齐检查
下面这段检查在任何抓取之前运行,遇到不一致就直接失败。它比较三个值:期望区域、浏览器声明的主语言、浏览器报告的时区。决定结论的是第一项,因为列表以另一种语言开头时,请求首先索要的就是那种语言,即使后面出现了匹配的标签。标签与区域的比较故意做得粗糙,因为目标是抓出描述两个不同地方的请求,而不是验证某个地理数据库。
const EXPECTED = 'DE';
const REGION_LANGUAGES = { DE: ['de-DE', 'de'], US: ['en-US', 'en'], JP: ['ja-JP', 'ja'] };
const REGION_TIMEZONES = { DE: 'Europe/Berlin', US: 'America/New_York', JP: 'Asia/Tokyo' };
const languages = navigator.languages.slice();
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// The primary language decides the region, not any match anywhere in the list:
// ["en-US", "de-DE"] still asks for English first, which is the mix this check must reject.
const languageOk = REGION_LANGUAGES[EXPECTED].includes(languages[0]);
const timezoneOk = timezone === REGION_TIMEZONES[EXPECTED];
// navigator.languages is the preference list, not the transmitted header; some browsers add
// fallback tags. Treat this as an estimate and read the real header from the captured request.
const acceptLanguageEstimate = languages
.map((tag, i) => (i === 0 ? tag : `${tag};q=${(1 - i * 0.1).toFixed(1)}`))
.join(',');
console.log(
JSON.stringify({ expected: EXPECTED, languages, timezone, acceptLanguageEstimate, languageOk, timezoneOk }, null, 2)
);
if (!languageOk || !timezoneOk)
throw new Error('capture would describe more than one region; fix inputs before capturing');
真正让这段检查有意义的是第一项。en-US,de-DE 这样的列表里也有德语标签,但请求仍然首先索要英语,所以只要列表里任意匹配就放行的检查,恰好会放过它本该拦下的混合请求。在一个构造用例里,浏览器声明 en-US,en、报告 America/New_York,而操作方期望 DE,输出会同时点名两项失败:
{
"expected": "DE",
"languages": ["en-US", "en"],
"timezone": "America/New_York",
"acceptLanguageEstimate": "en-US,en;q=0.9",
"languageOk": false,
"timezoneOk": false
}
Error: capture would describe more than one region; fix inputs before capturing
这段检查不能证明请求从哪里出去,因为页面无法可靠读取自己的公网地址。把出口检查单独保留:在操作方一侧确认出口 IP 类别,通过代理服务商自己的报告,或通过 BotBrowser GeoIP 指南所描述的显式 --proxy-ip,并把这个值写进抓取记录。页面内检查覆盖页面能观察到的两个输入,操作方检查覆盖它观察不到的那一个。
在一组序列的第一次抓取之前把两者都跑一遍,之后只要代理、配置文件或语言设置发生变化就重跑。只在初始化时跑一次的检查,一旦有人改了参数,就不再描述这次运行。
如果没人读输出,有两种失败模式能活过书面检查。第一种是过期期望:操作方在运行手册里改了目标区域,却把 EXPECTED 留在旧值,于是检查通过,而脚本和抓取都在描述错误的地方。第二种是部分覆盖:代理换了,但上一轮显式的 --bot-timezone 还留在启动参数里,推导出的时区悄悄输给了这个参数。两者都可以通过打印解析后的值而不只是布尔结果来廉价发现,这也正是示例在抛出错误之前先记录语言列表、时区和拼接出来的头的原因。
解析值日志还给记录留下了可引用的东西。后来有人问为什么某次运行被标记为已对齐,答案是那次运行打印出的时区与请求头,而不是对配置的回忆。把这一行留在运行日志里,并从 alignment_result 引用它,让两个工件彼此绑定。
每次被接受的抓取都要记录对齐
只有当一份抓取记录说明了决定响应的属性,它才与其他记录可比。下表把三个对齐输入与观察放在一起,让复核者能看出某个差异是否可能属于区域,还是必须属于来源。
| 记录字段 | 内容 | 为什么不能省 |
|---|---|---|
expected_region | 本次运行针对的 ISO 3166-1 两位代码 | 给后续每次比较一个固定主体 |
exit_ip_class | 服务商报告的出口地址或其范围 | 把「同区域」与「同服务商、不同城市」分开 |
accept_language_estimate | 由 navigator.languages 推算的请求头 | 表示页面以为自己发送的值;真实头请从抓到的请求读取 |
timezone_reported | 浏览器返回的 IANA 时区 | 发现与自身代理相矛盾的配置文件 |
creative_ref | 发布方一侧的标识、哈希或文件引用 | 在不重新分发素材的前提下比对差异 |
landing_url | 跟随重定向后的最终地址 | 把协商与客户端重定向分开 |
observed_at | 抓取时间(UTC) | 排序观察并暴露投放排期变化 |
alignment_result | aligned 或具体的不一致名称 | 防止未对齐的行进入序列 |
表中有两处细节容易被跳过、重建起来又很贵。第一处是请求地址与最终落地地址的区别。重定向链可能在协商之后移动请求,只记录请求地址会隐藏真正解释差异的那一步。第二处是创意引用。存哈希或发布方标识既让记录可复核,又符合保留限制,还能让后续运行区分「同一创意」和「同一活动、新素材」。
这与 SERP 排名抓取模型 用于自然结果的是同一套上下文纪律:一个序数值离开产生它的上下文签名就没有意义。广告位在同样规则下可比,尽管输出不同。
保留策略值得单独决定,因为创意引用承载的东西可能比看起来更重。素材哈希通常足以区分两次观察,但落地 URL 可能带上属于某次具体投放请求的标识符。在保存 URL 之前,把携带投放标识符的参数去掉或做哈希;只有在平台条款和你自己的保留政策都允许时,才保存未修改的值。轮换代理的配置文件记录 对会话身份用的是同一套做法:保留稳定引用,丢弃增加风险却不增加可比性的逐请求细节。
一组序列还需要明确的结束条件。广告活动按抓取观察不到的日程变化,因此无期限的序列最终会把同一活动的两个状态放在一起比较,并称之为区域差异。序列开始时声明观察窗口,窗口关闭时标记最后一次抓取,把之后的抓取当作带自己对齐记录的新序列。这一条纪律就能挡住长期、无标注抓取最容易产生的大部分结论。
ads.txt 适用到哪里、止步于哪里
在抓取工作里,ads.txt 经常被两个方向同时误读。规范描述的是一个让发布方公开声明谁被授权销售其广告库存的机制,文件格式在域级别记录被授权的系统与经销商关系。因此查看发布方的文件回答的是供应链问题:这个销售关系是否被声明。
它不回答抓取流程常问的三个问题。它不说采集是否被允许,因为规范讲的是库存的授权销售,而不是谁可以请求页面。它不证明某次被观察到的展示是可计费的,因为文件描述的是声明授权而非结算。它也不指示创意如何被选择,因为创意选择完全不在它的范围内。
有价值的用法更窄,但仍值得做:当一次抓取与合作伙伴报告的广告位对比时,ads.txt 记录可以显示该域声明 的卖方列表里是否包含合作伙伴所指的系统。那里的不一致是给合作伙伴的供应链问题,不是关于区域定向的推断。把两个发现放在不同字段里,避免后来的读者把它们合并成一个结论。
文件本身的维护对这种比较很重要。一条记录可能以某种关系类型列出,而该类型会改变系统的处理方式;规范期望条目能在被观察域名的稳定路径上读取。如果两次抓取之间该域名返回了不同文件,卖方问题就已经独立于任何区域问题发生了变化,两次抓取不再描述同一条供应链。把文件获取时间和正文哈希与卖方比对一起记录,供应链变化以后就不会被误当成广告投放变化。
不要把「被声明的卖方」当作质量信号。规范描述的是销售库存的授权,范围刻意很窄:它不涉及创意质量、可见性、品牌安全或任何报告的准确性。需要这些答案的合作沟通必须使用合作伙伴自己的报告,而一次 ads.txt 查询只贡献一个事实。
工具之前先定条款与范围
一次广告验证运行会触及发布方页面、广告系统的响应,可能还有测量服务商的脚本,因此一次抓取要同时受三套条款约束,而不是一套。每套条款决定的都比工具选项更多。技术上做得到的抓取,仍可能落在平台允许范围之外,再多的配置文件或代理设置也改变不了这一点。
由此有两个实践边界。第一,观察不等于参与:一个记录页面返回内容的 GET 请求,与测量脚本发出的展示与可见性信标是不同的行为,后者不在抓取流程的范围内。第二,两次抓取之间的差异是一个观察,不是欺诈或投放错误的结论;那些结论属于持有投放日志与合同关系的一方。
电商监控边界 为价格与库存观察划了同一条线:渲染页面与合作伙伴数据源回答不同问题,不应合并成一条序列。广告验证更需要这种分离,因为三个独立系统都可能各自改变答案。
常见问题
即使创意看起来相同,区域不一致也会让抓取作废吗?
会。这次抓取不再描述一个连贯的地方,因此无法与任何一个区域中正确对齐的抓取比较。输出相同只会让记录看起来干净,却让它无法用于比较。
对齐检查能替代出口验证吗?
不能。页面能读取自己声明的语言列表和报告的时区,但无法可靠读取自己的公网地址。请在操作方一侧确认出口,并把该值存进抓取记录。
ads.txt 能说明某个广告位是否合法吗?
不能。它声明发布方授权哪些系统在该域名销售库存,不描述创意选择、可计费投放或采集许可。
BotBrowser 能让抓取来自某个特定国家吗?
它会应用代理出口,并从该地址推导时区、区域设置与语言的默认值;文档注明对这三项的手动覆盖需要 ENT Tier1 许可。得到的抓取仍然反映广告系统决定返回的内容,城市级推导也可能是近似的。
来源
- IAB Tech Lab ads.txt
- ads.txt 规范 1.1 版
- MDN Accept-Language
- RFC 9110 HTTP 语义
- BotBrowser GeoIP 数据库
- BotBrowser 时区、区域设置与语言
实践结论
把出口区域、报告的时区与声明的语言列表当作抓取的一个属性。第一次抓取前跑对齐检查,任何代理、配置文件或语言变化后重跑。把三个输入与创意引用、最终落地 URL、观察时间一起保存,只有当三者都与本次运行声称代表的区域一致时,才把该行标记为已对齐。对齐完好却出现差异时,解释在来源一侧,而正是这份记录让那个结论成为可能。