数据抓取

匹配住宅代理的地理位置、时区和语言环境以保持会话清洁

德国出口节点却报告纽约时区,这是真实访客不会出现的矛盾。所有语言环境信号都应从出口国家推导,才不会出现偏差。

Chris Collins

Chris Collins

2026年8月27日 · 1 分钟阅读

你设置了一个德国出口节点,地址的地理定位也正确,但目标网站仍然把这次会话当作可疑访问,或者向你提供本应发给其他国家访问者的内容。IP 是对的。但请求中其他一切信息,描述的仍然是另一个国家的访问者。

位置不是单一信号,而是一组信号的集合,真实访问者会从同一个地点产生所有这些信号,因为它们都来自同一台设备、同一个国家。而爬虫则是从不同来源拼凑这些信号:国家来自代理参数,时区来自服务器的时钟,语言区域来自某个库的默认设置,语言标头则来自硬编码的内容。当这些信息互相矛盾时,这种矛盾比任何单一数值都更容易被检测出来,而且它还可能改变你所采集到的内容。

必须一致的信号

有六项信息会告诉网站你所在的位置,值得逐一列出,因为大多数爬虫只控制其中一两项。

IP 地址是主要信号,也是你的代理所设置的信号。Accept-Language 是表达语言偏好的 HTTP 标头,许多网站会直接根据它来提供内容。时区可以通过浏览器中的 JavaScript 日期 API 观察到,更精确地说,是通过 Intl API 解析出的时区。语言区域涵盖 navigator.language 以及 Intl API 解析出的格式约定,这决定了日期、数字和货币的显示方式。货币和单位,如果网站允许客户端进行提示的话。以及 DNS 解析,这看起来无关,实则不然:如果你的客户端在远程出口的同时本地解析主机名,那么解析过程会发生在你的实际位置,从而可能给你返回一个地区错误的端点,这就是 DNS 泄漏 问题。

对于纯 HTTP 工作而言,只有前两项和 DNS 是可见的,这也是为什么标头文章将 Accept-Language 视为主要配对项。而对于浏览器自动化,这六项全部可观察,不一致通常也正是在这里出现的。

从单一权威来源推导一切

解决方法是结构性的,而不是一份检查清单。如果国家在一处设置,时区在另一处设置,那么一旦有人新增一个市场,它们就会开始出现偏差。将每个市场定义一次,包含它所隐含的所有信号,然后整个会话都从这条记录中推导出来。

MARKETS = {
    "de": {"lang": "de-DE,de;q=0.9,en;q=0.8", "locale": "de-DE",
           "tz": "Europe/Berlin",   "currency": "EUR"},
    "us": {"lang": "en-US,en;q=0.9", "locale": "en-US",
           "tz": "America/New_York", "currency": "USD"},
    "jp": {"lang": "ja-JP,ja;q=0.9,en;q=0.8", "locale": "ja-JP",
           "tz": "Asia/Tokyo",      "currency": "JPY"},
    "br": {"lang": "pt-BR,pt;q=0.9,en;q=0.8", "locale": "pt-BR",
           "tz": "America/Sao_Paulo", "currency": "BRL"},
}

def proxy_for(country, session=None):
    user = f"customer-USERNAME-country-{country}"
    if session:
        user += f"-sid-{session}-ttl-600"
    url = f"http://{user}:PASSWORD@p.shifter.io:443"
    return {"http": url, "https": url}

现在,一个市场就是一个单独的参数,不存在国家和时区可能不一致的代码路径,因为没有人会分别设置它们。

应用到浏览器会话中

浏览器自动化框架将时区和语言区域作为上下文选项公开,这正是设置它们的正确位置:它们在任何页面脚本运行之前就已生效,因此页面无法观察到设备的真实数值。

# Playwright: 代理、时区和语言区域全部从同一条市场记录中推导
m = MARKETS[country]
context = browser.new_context(
    proxy={"server": "http://p.shifter.io:443",
           "username": f"customer-USERNAME-country-{country}-sid-{sid}",
           "password": "PASSWORD"},
    locale=m["locale"],                  # navigator.language 和 Intl 格式化
    timezone_id=m["tz"],                 # Intl 解析出的时区和 Date 偏移量
    extra_http_headers={"Accept-Language": m["lang"]},
)

在上下文层面设置这些参数,而不是事后修补属性,这一点很重要,因为一个被修补过的 Intl.DateTimeFormat,如果与 Date 偏移量不一致,本身就是一个可被检测出的矛盾,而检测脚本通常会交叉核查这两者。

精确度,以及你需要多少精确度

对大多数工作而言,国家级别的精细度已经足够,因为时区和语言区域基本上是按国家划分的。有三种情况需要更谨慎处理。

拥有多个时区的国家,使用全国统一的默认值对部分人口来说是错误的。一个美国出口节点在多个时区中都是合理的,所以如果你在进行城市级别定向工作,应该根据城市而不是国家来推导时区;如果不是这种情况,就选择与最大用户比例相匹配的时区,并保持一致,而不是随机切换。

拥有多个官方语言的国家需要慎重选择:一个瑞士或加拿大的出口节点在多种语言区域下都是合理的,正确的做法通常是选择与你所采集内容相匹配的那个语言区域,并保持恒定不变。

区域性格式差异更为细微,通常不值得刻意追求,但如果你要呈现某个语言区域,应让 Intl API 来处理格式化,而不是手工编写日期和数字格式,因为那可能与该语言区域实际产生的格式不符。

会话生命周期内的一致性

一个会话就是一段故事,而故事在讲述过程中不能中途改变。如果一个固定会话在多步骤流程中保持同一个地址,那么该流程中的每个请求都必须携带相同的语言、时区和语言区域。在流程中途改变其中任何一项,描述的就是一个在点击搜索和查看结果之间跨国移动的访问者,这比任何静态的不一致都更为明显的异常。

这正是从单一市场记录进行推导的价值再次体现的地方:会话标识符和语言区域组合来自同一个地方,并持续相同的时长。将它们显式绑定在一起,这样释放会话就会释放整个身份,而新会话则会开始一个全新的、内部一致的身份。同样的逻辑也适用于反检测浏览器中设备指纹与网络身份的配对。

验证你是否做对了

不要假设任何事情,要检查两个层面。

首先,是浏览器对自身情况的报告。运行一个页面,读取解析出的时区、navigator.language 以及一个格式化的日期,确认它们与你预期的市场相匹配。这能立即捕获配置错误。

JSON.stringify({
  tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
  lang: navigator.language,
  langs: navigator.languages,
  offset: new Date().getTimezoneOffset(),
})

其次,也是更为重要的,是目标网站的实际反应。抓取一个对地理位置敏感的页面,确认货币、语言和地区性内容是否与当地访问者所看到的一致。网站自身的行为才是真正的判定标准,因为地理定位数据库和目标网站的判断并不总是一致,而这正是测试位置准确性的用途所在。如果地址的地理定位是正确的,但内容却是错的,首先要怀疑的是 DNS 解析。

结论

位置是一组信号的集合,网站是将它们整体解读的。在代理上设置一个国家,却让时区、语言区域和语言保留服务器的默认设置,会产生一个根本不可能存在的访问者,这既是一个检测信号,也是一个悄然产生错误数据的来源。将每个市场定义一次,包含它所隐含的所有信号,让代理参数和浏览器上下文都从这一条记录中推导出来,使它们不会产生偏差;在上下文层面设置时区和语言区域,而不是加载后再修补;在会话的整个生命周期内保持整套信号恒定不变;并在两个层面进行验证:浏览器报告的内容,以及目标网站实际提供的内容。这样,你的流量所透露的位置信息,就只会是你自己选定的那一个。

地理位置本身来自住宅代理,真正的家庭级地址,支持国家和城市级定向,所以你的会话所声称的位置,就是你真正正在从其出口的位置,再加上适合在多个市场运行同一任务的按 GB 计费方案。

准备好开始了吗?

试用 Shifter 住宅代理,205M+ 个 IP,195+ 个国家,低至 $0.75/GB。

立即开始