[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-cn-serp-dili-dingwei-zhuaqu-zhinan":3},{"success":4,"payload":5,"related":59,"errors":125},true,{"id":6,"slug":7,"title":8,"excerpt":9,"body_html":10,"meta_title":11,"meta_description":12,"canonical_url":13,"canonical_override":14,"og_title":11,"og_description":12,"og_image_url":15,"faq_json":16,"key_takeaways":14,"allow_index":26,"noindex_google_only":26,"allow_follow":4,"published_at":27,"updated_at":27,"content_updated_at":28,"featured_image_url":29,"featured_image_alt":30,"card_image_url":31,"reading_time_minutes":32,"author":33,"categories":37,"tags":44,"alternates":45,"locale_paths":47,"views":58},198,"serp-dili-dingwei-zhuaqu-zhinan","地理定位SERP抓取最佳实践：从uule到城市级代理的完整指南","掌握地理定位SERP抓取的核心参数uule\u002Fgl\u002Fhl\u002Fgoogle_domain，配合城市级住宅代理实现精确的本地SEO排名追踪。包含完整Python代码示例与生产级重试策略。","\u003Cp>当你在纽约搜索\"附近的咖啡店\"和在洛杉矶搜索同样的关键词时，Google返回的本地包和有机结果完全不同。对于构建本地SEO排名追踪系统的开发者来说，地理定位SERP抓取最佳实践意味着你必须在URL参数和出口IP两个层面同时精确控制地理位置——仅靠国家级定位远远不够。本文将从uule参数构造到城市级住宅代理配置，提供完整的可运行代码示例。\u003C\u002Fp>\n\n\u003Ch2>地理定位SERP抓取最佳实践：为什么国家级定位不够\u003C\u002Fh2>\n\n\u003Cp>很多SEO工程师在抓取SERP时只设置了\u003Ccode>gl=us\u003C\u002Fcode>，以为这样就能获取\"美国\"的搜索结果。但美国横跨多个时区，拥有超过19000个城镇。同一个关键词在纽约、芝加哥和休斯顿的本地包结果可能截然不同。如果你的客户是一家在布鲁克林经营的水管维修公司，你需要的是布鲁克林级别的SERP数据，而不是笼统的\"美国\"数据。\u003C\u002Fp>\n\n\u003Cp>根据 \u003Ca href=\"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\" rel=\"noopener noreferrer\">Google搜索中心文档\u003C\u002Fa>，Google的搜索结果个性化基于多个信号：用户IP地址的地理位置、uule参数、搜索历史、登录状态等。即使你设置了\u003Ccode>gl=us\u003C\u002Fcode>，如果没有配合正确的城市级uule和匹配的住宅IP，你获取的SERP数据可能被Google的地理位置推断机制所覆盖。\u003C\u002Fp>\n\n\u003Ch2>技术背景：同一查询为何返回不同结果\u003C\u002Fh2>\n\n\u003Cp>Google的SERP本地化机制涉及多层信号交叉验证：\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>IP地理定位：Google通过出口IP推断用户所在地区，这是最基础的信号。\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>URL参数\u003C\u002Fstrong>：\u003Ccode>gl\u003C\u002Fcode>（国家）、\u003Ccode>hl\u003C\u002Fcode>（界面语言）、\u003Ccode>uule\u003C\u002Fcode>（精确位置）等参数显式指定搜索意图的地理范围。\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>个人化信号\u003C\u002Fstrong>：搜索历史、登录状态、Cookie等影响结果排序。\u003C\u002Fli>\n  \u003Cli>\u003Cstrong>设备与浏览器\u003C\u002Fstrong>：User-Agent和TLS指纹影响移动端\u002F桌面端结果差异。\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>关键问题是：Google会\u003Cstrong>交叉验证\u003C\u002Fstrong>这些信号。如果你的uule指向纽约但出口IP在德国法兰克福的数据中心，Google可能忽略uule参数，直接根据IP返回德国结果，或者更糟糕地触发CAPTCHA验证。这就是为什么参数和代理IP必须在地理上保持一致。\u003C\u002Fp>\n\n\u003Ch2>核心本地化参数详解\u003C\u002Fh2>\n\n\u003Ch3>gl、hl、google_domain、lr\u003C\u002Fh3>\n\n\u003Cp>这四个参数控制搜索结果的国家和语言维度：\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>\u003Ccode>gl\u003C\u002Fcode>：国家代码（如\u003Ccode>us\u003C\u002Fcode>、\u003Ccode>de\u003C\u002Fcode>、\u003Ccode>jp\u003C\u002Fcode>），影响搜索结果的国家级过滤。\u003C\u002Fli>\n  \u003Cli>\u003Ccode>hl\u003C\u002Fcode>：界面语言（如\u003Ccode>en\u003C\u002Fcode>、\u003Ccode>de\u003C\u002Fcode>、\u003Ccode>ja\u003C\u002Fcode>），影响结果页语言和部分搜索意图理解。\u003C\u002Fli>\n  \u003Cli>\u003Ccode>google_domain\u003C\u002Fcode>：指定Google域名（如\u003Ccode>google.com\u003C\u002Fcode>、\u003Ccode>google.de\u003C\u002Fcode>、\u003Ccode>google.co.jp\u003C\u002Fcode>），不同域名可能返回不同的本地化结果。\u003C\u002Fli>\n  \u003Cli>\u003Ccode>lr\u003C\u002Fcode>：语言限制（如\u003Ccode>lang_en\u003C\u002Fcode>、\u003Ccode>lang_de\u003C\u002Fcode>），过滤仅包含特定语言的页面。\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>这些参数需要协同工作。例如，抓取德国德语结果时，应同时设置\u003Ccode>gl=de&amp;hl=de&amp;google_domain=google.de&amp;lr=lang_de\u003C\u002Fcode>。\u003C\u002Fp>\n\n\u003Ch3>uule参数构造方法\u003C\u002Fh3>\n\n\u003Cp>\u003Ccode>uule\u003C\u002Fcode>（User-Defined Location）是Google搜索中用于指定精确地理位置的参数，可以精确定位到城市甚至区县级别。其构造格式为：\u003C\u002Fp>\n\n\u003Cp>\u003Ccode>uule = 固定前缀 + 长度字符 + Base64编码的规范地名\u003C\u002Fcode>\u003C\u002Fp>\n\n\u003Cp>具体来说：\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>固定前缀：\u003Ccode>w+CAIQICI\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>长度字符：\u003Ccode>chr(65 + len(base64_string))\u003C\u002Fcode>，即base64编码后字符串的长度对应一个ASCII字符\u003C\u002Fli>\n  \u003Cli>规范地名格式：\u003Ccode>城市名,州\u002F省名,国家名\u003C\u002Fcode>（如\u003Ccode>Brooklyn,New York,United States\u003C\u002Fcode>）\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>更多关于Base64编码的技术细节，可参考 \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FGlossary\u002FBase64\" rel=\"noopener noreferrer\">MDN Web Docs\u003C\u002Fa>。\u003C\u002Fp>\n\n\u003Ch3>pws=0：非个性化基线\u003C\u002Fh3>\n\n\u003Cp>添加\u003Ccode>pws=0\u003C\u002Fcode>参数可以禁用搜索结果个性化，确保你获取的是\"干净\"的基线SERP，而非被Google个人化算法干扰的结果。这对于排名追踪系统至关重要——你需要可复现的基线数据，而非因人而异的个性化结果。\u003C\u002Fp>\n\n\u003Ch2>代理IP与参数地理一致性\u003C\u002Fh2>\n\n\u003Cp>如前所述，Google会交叉验证URL参数和IP地理定位。最佳实践是：\u003C\u002Fp>\n\n\u003Col>\n  \u003Cli>确定目标城市（如\"New York\"）\u003C\u002Fli>\n  \u003Cli>构造对应的uule参数\u003C\u002Fli>\n  \u003Cli>设置匹配的gl\u002Fhl\u002Fgoogle_domain\u003C\u002Fli>\n  \u003Cli>通过该城市的住宅代理IP发送请求\u003C\u002Fli>\n\u003C\u002Fol>\n\n\u003Cp>这样所有信号都指向同一地理位置，Google不会产生信号冲突，SERP数据的准确性和可复现性最高。\u003C\u002Fp>\n\n\u003Ch2>代理类型对比：SERP抓取该选哪种？\u003C\u002Fh2>\n\n\u003Ctable>\n  \u003Cthead>\n    \u003Ctr>\n      \u003Cth>代理类型\u003C\u002Fth>\n      \u003Cth>SERP抓取适用性\u003C\u002Fth>\n      \u003Cth>反检测能力\u003C\u002Fth>\n      \u003Cth>延迟\u003C\u002Fth>\n      \u003Cth>成本\u003C\u002Fth>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n  \u003Ctbody>\n    \u003Ctr>\n      \u003Ctd>住宅代理\u003C\u002Ftd>\n      \u003Ctd>★★★★★ 最佳选择\u003C\u002Ftd>\n      \u003Ctd>高（真实ISP IP）\u003C\u002Ftd>\n      \u003Ctd>200-500ms\u003C\u002Ftd>\n      \u003Ctd>中等\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>移动代理\u003C\u002Ftd>\n      \u003Ctd>★★★★ 适合移动端SERP\u003C\u002Ftd>\n      \u003Ctd>极高（运营商IP）\u003C\u002Ftd>\n      \u003Ctd>300-800ms\u003C\u002Ftd>\n      \u003Ctd>最高\u003C\u002Ftd>\n    \u003C\u002Ftr>\n    \u003Ctr>\n      \u003Ctd>数据中心代理\u003C\u002Ftd>\n      \u003Ctd>★★ 容易被识别\u003C\u002Ftd>\n      \u003Ctd>低（已知数据中心IP段）\u003C\u002Ftd>\n      \u003Ctd>50-100ms\u003C\u002Ftd>\n      \u003Ctd>最低\u003C\u002Ftd>\n    \u003C\u002Ftr>\n  \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\n\u003Cp>对于SERP抓取，住宅代理是性价比最优的选择。数据中心IP容易被Google识别并触发CAPTCHA，而住宅IP看起来就像真实用户的流量。\u003C\u002Fp>\n\n\u003Ch2>代码实现：从uule到完整SERP抓取\u003C\u002Fh2>\n\n\u003Ch3>1. ProxyHat代理助手类与uule构造函数\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>import base64\nfrom dataclasses import dataclass\n\n@dataclass\nclass ProxyHatClient:\n    &quot;&quot;&quot;ProxyHat代理配置助手，支持地理定位与粘性会话。&quot;&quot;&quot;\n    username: str\n    password: str\n    gateway: str = &quot;gate.proxyhat.com&quot;\n    http_port: int = 8080\n    socks5_port: int = 1080\n\n    def http_proxy(self, country: str = None, region: str = None, city: str = None,\n                   session: str = None) -&gt; dict:\n        &quot;&quot;&quot;构造HTTP代理字典，支持国家\u002F地区\u002F城市级定位与粘性会话。&quot;&quot;&quot;\n        user = self.username\n        if country:\n            user += f&quot;-country-{country}&quot;\n        if region and city:\n            user += f&quot;-region-{region}-city-{city}&quot;\n        elif region:\n            user += f&quot;-region-{region}&quot;\n        if session:\n            user += f&quot;-sid-{session}&quot;\n        proxy_url = f&quot;http:\u002F\u002F{user}:{self.password}@{self.gateway}:{self.http_port}&quot;\n        return {&quot;http&quot;: proxy_url, &quot;https&quot;: proxy_url}\n\n    def socks5_proxy(self, country: str = None, region: str = None, city: str = None,\n                     session: str = None) -&gt; str:\n        &quot;&quot;&quot;构造SOCKS5代理URL。&quot;&quot;&quot;\n        user = self.username\n        if country:\n            user += f&quot;-country-{country}&quot;\n        if region and city:\n            user += f&quot;-region-{region}-city-{city}&quot;\n        elif region:\n            user += f&quot;-region-{region}&quot;\n        if session:\n            user += f&quot;-sid-{session}&quot;\n        return f&quot;socks5:\u002F\u002F{user}:{self.password}@{self.gateway}:{self.socks5_port}&quot;\n\n\ndef build_uule(canonical_name: str) -&gt; str:\n    &quot;&quot;&quot;构造Google uule参数。\n    \n    格式: w+CAIQICI + 长度字符 + base64(规范地名)\n    长度字符 = chr(65 + len(base64字符串))\n    \n    示例:\n        &gt;&gt;&gt; build_uule(&quot;Brooklyn,New York,United States&quot;)\n        &apos;w+CAIQICI...&apos;\n    &quot;&quot;&quot;\n    prefix = &quot;w+CAIQICI&quot;\n    encoded = base64.b64encode(\n        canonical_name.encode(&quot;utf-8&quot;)\n    ).decode(&quot;ascii&quot;)\n    length_key = chr(65 + len(encoded))\n    return prefix + length_key + encoded\n\n\n# 示例：构造纽约布鲁克林的uule\nuule_brooklyn = build_uule(&quot;Brooklyn,New York,United States&quot;)\nuule_manhattan = build_uule(&quot;New York,New York,United States&quot;)\nprint(f&quot;Brooklyn uule: {uule_brooklyn}&quot;)\nprint(f&quot;Manhattan uule: {uule_manhattan}&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>2. curl命令行示例\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode># 抓取纽约地区的&quot;plumber&quot;搜索结果\n# 使用ProxyHat住宅代理（美国-纽约）\n# 包含uule、gl、hl、pws=0参数\n\ncurl -s \\\n  --proxy &quot;http:\u002F\u002Fuser-country-US-region-new_york-city-new_york_city:pass@gate.proxyhat.com:8080&quot; \\\n  -H &quot;User-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F120.0.0.0 Safari\u002F537.36&quot; \\\n  &quot;https:\u002F\u002Fwww.google.com\u002Fsearch?q=plumber&amp;gl=us&amp;hl=en&amp;uule=w+CAIQICImTmV3IFlvcmssTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==&amp;pws=0&amp;num=10&quot;\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>3. Python curl_cffi：模拟Chrome指纹 + 住宅代理轮转\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>from curl_cffi import requests as cffi_requests\nfrom urllib.parse import urlencode\nimport logging\n\nlogging.basicConfig(level=logging.INFO,\n                    format=&quot;%(asctime)s [%(levelname)s] %(message)s&quot;)\nlogger = logging.getLogger(__name__)\n\n# 初始化ProxyHat客户端\nproxyhat = ProxyHatClient(username=&quot;your_user&quot;, password=&quot;your_pass&quot;)\n\n# 目标城市列表\nTARGET_CITIES = [\n    {&quot;city&quot;: &quot;newyork&quot;,    &quot;uule_name&quot;: &quot;New York,New York,United States&quot;,\n     &quot;gl&quot;: &quot;us&quot;, &quot;hl&quot;: &quot;en&quot;, &quot;domain&quot;: &quot;google.com&quot;},\n    {&quot;city&quot;: &quot;losangeles&quot;, &quot;uule_name&quot;: &quot;Los Angeles,California,United States&quot;,\n     &quot;gl&quot;: &quot;us&quot;, &quot;hl&quot;: &quot;en&quot;, &quot;domain&quot;: &quot;google.com&quot;},\n    {&quot;city&quot;: &quot;chicago&quot;,    &quot;uule_name&quot;: &quot;Chicago,Illinois,United States&quot;,\n     &quot;gl&quot;: &quot;us&quot;, &quot;hl&quot;: &quot;en&quot;, &quot;domain&quot;: &quot;google.com&quot;},\n    {&quot;city&quot;: &quot;berlin&quot;,     &quot;uule_name&quot;: &quot;Berlin,Berlin,Germany&quot;,\n     &quot;gl&quot;: &quot;de&quot;, &quot;hl&quot;: &quot;de&quot;, &quot;domain&quot;: &quot;google.de&quot;},\n]\n\ndef scrape_serp_for_city(keyword: str, city_cfg: dict) -&gt; str:\n    &quot;&quot;&quot;抓取指定城市的SERP HTML。\n    \n    使用curl_cffi模拟Chrome TLS指纹，配合匹配城市的住宅代理。\n    &quot;&quot;&quot;\n    uule = build_uule(city_cfg[&quot;uule_name&quot;])\n    params = {\n        &quot;q&quot;: keyword,\n        &quot;gl&quot;: city_cfg[&quot;gl&quot;],\n        &quot;hl&quot;: city_cfg[&quot;hl&quot;],\n        &quot;uule&quot;: uule,\n        &quot;pws&quot;: &quot;0&quot;,\n        &quot;num&quot;: &quot;10&quot;,\n    }\n    url = f&quot;https:\u002F\u002Fwww.{city_cfg[&apos;domain&apos;]}\u002Fsearch?{urlencode(params)}&quot;\n\n    # 使用与目标城市匹配的住宅代理\n    proxies = proxyhat.http_proxy(\n        country=city_cfg[&quot;gl&quot;].upper(),\n        city=city_cfg[&quot;city&quot;],\n    )\n\n    try:\n        resp = cffi_requests.get(\n            url,\n            proxies=proxies,\n            impersonate=&quot;chrome&quot;,\n            timeout=15,\n        )\n        logger.info(\n            f&quot;[{city_cfg[&apos;city&apos;]}] status={resp.status_code} &quot;\n            f&quot;len={len(resp.text)}&quot;\n        )\n        if resp.status_code == 429:\n            logger.warning(f&quot;[{city_cfg[&apos;city&apos;]}] 触发限流 (429)&quot;)\n            return None\n        return resp.text\n    except Exception as e:\n        logger.error(f&quot;[{city_cfg[&apos;city&apos;]}] 请求失败: {e}&quot;)\n        return None\n\n# 批量抓取\nfor city in TARGET_CITIES:\n    html = scrape_serp_for_city(&quot;plumber near me&quot;, city)\n    if html:\n        # 保存或解析...\n        pass\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>4. 解析有机结果与本地包（selectolax）\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>from selectolax.parser import HTMLParser\nfrom dataclasses import dataclass\n\n@dataclass\nclass OrganicResult:\n    position: int\n    title: str\n    url: str\n    snippet: str\n\n@dataclass\nclass LocalPackResult:\n    name: str\n    rating: float\n    reviews: int\n    address: str\n    phone: str\n\ndef parse_organic_results(html: str) -&gt; list[OrganicResult]:\n    &quot;&quot;&quot;解析Google有机搜索结果。&quot;&quot;&quot;\n    tree = HTMLParser(html)\n    results = []\n    # Google有机结果通常在 div.g 中\n    for i, node in enumerate(tree.css(&quot;div.g&quot;), start=1):\n        title_node = node.css_first(&quot;h3&quot;)\n        link_node = node.css_first(&quot;a&quot;)\n        snippet_node = node.css_first(&quot;div.VwiC3b&quot;)\n        \n        title = title_node.text(strip=True) if title_node else &quot;&quot;\n        url = link_node.attributes.get(&quot;href&quot;, &quot;&quot;) if link_node else &quot;&quot;\n        snippet = snippet_node.text(strip=True) if snippet_node else &quot;&quot;\n        \n        if title and url:\n            results.append(OrganicResult(\n                position=i, title=title, url=url, snippet=snippet\n            ))\n    return results\n\ndef parse_local_pack(html: str) -&gt; list[LocalPackResult]:\n    &quot;&quot;&quot;解析Google本地包（Local Pack）结果。&quot;&quot;&quot;\n    tree = HTMLParser(html)\n    local_results = []\n    # 本地包通常在 div.rllt__link 中\n    for node in tree.css(&quot;div.rllt__link&quot;):\n        name_node = node.css_first(&quot;div.dbg0pd&quot;)\n        rating_node = node.css_first(&quot;span.BTtC6e&quot;)\n        reviews_node = node.css_first(&quot;span.W4E4td&quot;)\n        \n        name = name_node.text(strip=True) if name_node else &quot;&quot;\n        rating = float(rating_node.text(strip=True)) if rating_node else 0.0\n        reviews_text = reviews_node.text(strip=True) if reviews_node else &quot;0&quot;\n        reviews = int(reviews_text.replace(&quot;(&quot;, &quot;&quot;).replace(&quot;)&quot;, &quot;&quot;).replace(&quot;,&quot;, &quot;&quot;))\n        \n        local_results.append(LocalPackResult(\n            name=name, rating=rating, reviews=reviews,\n            address=&quot;&quot;, phone=&quot;&quot;\n        ))\n    return local_results\n\n# 使用示例\nhtml = scrape_serp_for_city(&quot;plumber near me&quot;, TARGET_CITIES[0])\nif html:\n    organic = parse_organic_results(html)\n    local_pack = parse_local_pack(html)\n    \n    print(f&quot;有机结果数: {len(organic)}&quot;)\n    for r in organic[:3]:\n        print(f&quot;  #{r.position}: {r.title}&quot;)\n    \n    print(f&quot;\\n本地包结果数: {len(local_pack)}&quot;)\n    for r in local_pack[:3]:\n        print(f&quot;  {r.name} | 评分{r.rating} ({r.reviews}评论)&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>5. 粘性会话：多页SERP连续抓取\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>from curl_cffi import requests as cffi_requests\nfrom urllib.parse import urlencode\nimport time\n\ndef scrape_multi_page_serp(keyword: str, city_cfg: dict,\n                           max_pages: int = 3) -&gt; list[str]:\n    &quot;&quot;&quot;使用粘性会话抓取多页SERP。\n    \n    关键点：多页抓取必须使用同一出口IP（粘性会话），\n    否则Google可能因IP变化而重置分页或触发CAPTCHA。\n    &quot;&quot;&quot;\n    session_id = f&quot;serp-{city_cfg[&apos;city&apos;]}-{int(time.time())}&quot;\n    proxies = proxyhat.http_proxy(\n        country=city_cfg[&quot;gl&quot;].upper(),\n        city=city_cfg[&quot;city&quot;],\n        session=session_id,  # 粘性会话：同一IP\n    )\n    \n    all_html = []\n    for page in range(max_pages):\n        start = page * 10\n        uule = build_uule(city_cfg[&quot;uule_name&quot;])\n        params = {\n            &quot;q&quot;: keyword,\n            &quot;gl&quot;: city_cfg[&quot;gl&quot;],\n            &quot;hl&quot;: city_cfg[&quot;hl&quot;],\n            &quot;uule&quot;: uule,\n            &quot;pws&quot;: &quot;0&quot;,\n            &quot;num&quot;: &quot;10&quot;,\n            &quot;start&quot;: str(start),\n        }\n        url = f&quot;https:\u002F\u002Fwww.{city_cfg[&apos;domain&apos;]}\u002Fsearch?{urlencode(params)}&quot;\n        \n        try:\n            resp = cffi_requests.get(\n                url, proxies=proxies,\n                impersonate=&quot;chrome&quot;, timeout=15,\n            )\n            if resp.status_code == 200:\n                all_html.append(resp.text)\n                logger.info(f&quot;第{page+1}页抓取成功 (start={start})&quot;)\n            elif resp.status_code == 429:\n                logger.warning(f&quot;第{page+1}页触发限流，停止分页&quot;)\n                break\n            else:\n                logger.warning(f&quot;第{page+1}页异常: {resp.status_code}&quot;)\n                break\n        except Exception as e:\n            logger.error(f&quot;第{page+1}页失败: {e}&quot;)\n            break\n        \n        # 页间延迟，模拟人类浏览行为\n        time.sleep(2 + page)  # 2s, 3s, 4s...\n    \n    return all_html\n\n# 抓取纽约前3页&quot;plumber&quot;结果\npages = scrape_multi_page_serp(&quot;plumber&quot;, TARGET_CITIES[0], max_pages=3)\nprint(f&quot;共抓取 {len(pages)} 页&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch3>6. CAPTCHA\u002F429退避与生产级重试\u003C\u002Fh3>\n\n\u003Cpre>\u003Ccode>import time\nimport random\nfrom curl_cffi import requests as cffi_requests\n\nclass CircuitBreakerOpen(Exception):\n    &quot;&quot;&quot;熔断器开启时抛出。&quot;&quot;&quot;\n\nclass SERPScraper:\n    &quot;&quot;&quot;生产级SERP抓取器，带指数退避、重试和熔断。&quot;&quot;&quot;\n    \n    MAX_RETRIES = 3\n    BASE_BACKOFF = 5  # 秒\n    RATE_LIMIT_THRESHOLD = 5  # 连续429次数阈值\n    \n    def __init__(self, proxyhat: ProxyHatClient):\n        self.proxyhat = proxyhat\n        self._consecutive_429 = 0\n        self._circuit_open = False\n        self._circuit_reset_at = 0\n    \n    def _check_circuit(self):\n        if self._circuit_open:\n            if time.time() &lt; self._circuit_reset_at:\n                raise CircuitBreakerOpen(\n                    f&quot;熔断器开启中，{self._circuit_reset_at - time.time():.0f}s后重置&quot;\n                )\n            self._circuit_open = False\n            self._consecutive_429 = 0\n            logger.info(&quot;熔断器重置，恢复请求&quot;)\n    \n    def _backoff(self, attempt: int) -&gt; float:\n        &quot;&quot;&quot;指数退避 + 随机抖动。&quot;&quot;&quot;\n        delay = self.BASE_BACKOFF * (2 ** attempt) + random.uniform(0, 1)\n        return min(delay, 60)  # 最大60s\n    \n    def fetch_with_retry(self, url: str, proxies: dict,\n                         impersonate: str = &quot;chrome&quot;) -&gt; str | None:\n        &quot;&quot;&quot;带重试的SERP抓取。&quot;&quot;&quot;\n        self._check_circuit()\n        \n        for attempt in range(self.MAX_RETRIES):\n            try:\n                resp = cffi_requests.get(\n                    url, proxies=proxies,\n                    impersonate=impersonate, timeout=15,\n                )\n                \n                if resp.status_code == 200:\n                    self._consecutive_429 = 0\n                    return resp.text\n                \n                elif resp.status_code == 429:\n                    self._consecutive_429 += 1\n                    logger.warning(\n                        f&quot;429限流 (连续{self._consecutive_429}次, &quot;\n                        f&quot;尝试{attempt+1}\u002F{self.MAX_RETRIES})&quot;\n                    )\n                    if self._consecutive_429 &gt;= self.RATE_LIMIT_THRESHOLD:\n                        self._circuit_open = True\n                        self._circuit_reset_at = time.time() + 300\n                        logger.error(&quot;连续限流过多，熔断器开启 (300s)&quot;)\n                        return None\n                    \n                    backoff = self._backoff(attempt)\n                    logger.info(f&quot;退避 {backoff:.1f}s 后重试...&quot;)\n                    time.sleep(backoff)\n                \n                elif resp.status_code == 302:\n                    # 302重定向通常意味着CAPTCHA\n                    logger.warning(f&quot;302重定向 (可能CAPTCHA), 尝试{attempt+1}&quot;)\n                    time.sleep(self._backoff(attempt))\n                \n                else:\n                    logger.warning(f&quot;异常状态码: {resp.status_code}&quot;)\n                    return None\n                    \n            except Exception as e:\n                logger.error(f&quot;请求异常 (尝试{attempt+1}): {e}&quot;)\n                time.sleep(self._backoff(attempt))\n        \n        return None\n\n# 生产使用示例\nscraper = SERPScraper(proxyhat)\nuule = build_uule(&quot;Chicago,Illinois,United States&quot;)\nurl = (f&quot;https:\u002F\u002Fwww.google.com\u002Fsearch?q=plumber&quot;\n       f&quot;&amp;gl=us&amp;hl=en&amp;uule={uule}&amp;pws=0&amp;num=10&quot;)\nproxies = proxyhat.http_proxy(country=&quot;US&quot;, city=&quot;chicago&quot;)\n\nhtml = scraper.fetch_with_retry(url, proxies)\nif html:\n    organic = parse_organic_results(html)\n    print(f&quot;成功获取 {len(organic)} 条有机结果&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\n\n\u003Ch2>常见错误与边界情况\u003C\u002Fh2>\n\n\u003Cul>\n  \u003Cli>\u003Cstrong>uule与IP不匹配\u003C\u002Fstrong>：最常见的错误。设置uule指向纽约但用德国IP发送请求，Google会忽略uule或触发CAPTCHA。始终确保代理IP与uule指向同一地区。\u003C\u002Fli>\n  \u003Cli>忽略pws=0：不设置pws=0会导致结果不可复现，因为Google会根据各种信号个性化结果。\u003C\u002Fli>\n  \u003Cli>多页抓取IP变化：翻页时如果出口IP变化，Google可能重置分页或返回不同结果。使用粘性会话（\u003Ccode>-sid-xxx\u003C\u002Fcode>）保持同一IP。\u003C\u002Fli>\n  \u003Cli>选择器失效：Google频繁更新HTML结构，CSS选择器可能随时失效。建议建立选择器监控机制，当解析结果数量异常时发出告警。\u003C\u002Fli>\n  \u003Cli>请求频率过高：即使使用住宅代理，短时间内大量请求同一关键词仍会触发限流。建议每IP每分钟不超过10-15个请求，并配合随机延迟。\u003C\u002Fli>\n  \u003Cli>忽略移动端SERP：移动端和桌面端SERP结构不同。如果客户关注移动端排名，需单独抓取并使用移动端User-Agent。\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>ProxyHat配置指南\u003C\u002Fh2>\n\n\u003Cp>ProxyHat提供住宅、移动和数据中心代理，覆盖全球多个地理位置。对于地理定位SERP抓取，推荐使用住宅代理并配合城市级定位。\u003C\u002Fp>\n\n\u003Cp>核心连接参数：\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>网关：\u003Ccode>gate.proxyhat.com\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>HTTP端口：\u003Ccode>8080\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>SOCKS5端口：\u003Ccode>1080\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>用户名中嵌入地理参数：\u003Ccode>user-country-US-region-new_york-city-new_york_city\u003C\u002Fcode>\u003C\u002Fli>\n  \u003Cli>粘性会话：\u003Ccode>user-sid-abc123\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cp>完整配置文档请参考 \u003Ca href=\"https:\u002F\u002Fdocs.proxyhat.com\u002F\" rel=\"noopener noreferrer\">ProxyHat官方文档\u003C\u002Fa>。查看支持的地理位置列表请访问 \u003Ca href=\"\u002Fcn\u002Flocations\">代理位置页面\u003C\u002Fa>，了解定价方案请查看 \u003Ca href=\"\u002Fcn\u002Fpricing\">定价页面\u003C\u002Fa>。\u003C\u002Fp>\n\n\u003Cp>更多SERP抓取和排名追踪的使用场景，可参考 \u003Ca href=\"\u002Fcn\u002Fuse-cases\u002Fserp-tracking\">SERP追踪用例\u003C\u002Fa> 和 \u003Ca href=\"\u002Fcn\u002Fuse-cases\u002Fweb-scraping\">网页抓取用例\u003C\u002Fa>。\u003C\u002Fp>\n\n\u003Ch2>公共数据与法律合规提示\u003C\u002Fh2>\n\n\u003Cp>抓取Google SERP前，请务必注意以下合规事项：\u003C\u002Fp>\n\n\u003Cul>\n  \u003Cli>审查Google的服务条款（ToS），了解自动化访问的限制。\u003C\u002Fli>\n  \u003Cli>尊重\u003Ccode>robots.txt\u003C\u002Fcode>指令，虽然Google的robots.txt对\u002Fsearch路径有特殊声明。\u003C\u002Fli>\n  \u003Cli>遵循GDPR、CCPA等数据保护法规，特别是当你处理包含个人信息的本地商家数据时。\u003C\u002Fli>\n  \u003Cli>考虑使用 \u003Ca href=\"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSearch_engine_results_page\" rel=\"noopener noreferrer\">官方API\u003C\u002Fa> 或授权数据提供商作为替代方案。\u003C\u002Fli>\n  \u003Cli>控制抓取频率，避免对目标服务造成过大负载。\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>关键要点\u003C\u002Fh2>\n\n\u003Cblockquote>\n  \u003Cul>\n    \u003Cli>地理定位SERP抓取需要URL参数（uule\u002Fgl\u002Fhl）与代理IP在地理上保持一致，缺一不可。\u003C\u002Fli>\n    \u003Cli>uule参数通过固定前缀+长度字符+Base64规范地名构造，可实现城市级精确定位。\u003C\u002Fli>\n    \u003Cli>始终添加\u003Ccode>pws=0\u003C\u002Fcode>获取非个性化基线SERP，确保数据可复现。\u003C\u002Fli>\n    \u003Cli>多页抓取使用粘性会话保持同一出口IP，避免分页重置或CAPTCHA。\u003C\u002Fli>\n    \u003Cli>住宅代理是SERP抓取的最佳选择，数据中心IP容易被识别和封禁。\u003C\u002Fli>\n    \u003Cli>生产环境必须实现指数退避、重试和熔断机制，应对429限流和CAPTCHA。\u003C\u002Fli>\n    \u003Cli>选择器会随Google页面更新而失效，建立监控告警机制是必要的。\u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Fblockquote>","地理定位SERP抓取最佳实践 | ProxyHat","掌握地理定位SERP抓取最佳实践：详解uule参数构造、gl\u002Fhl\u002Fgoogle_domain配置、城市级住宅代理IP匹配策略，附完整Python代码与生产级重试机制。","\u002Fcn\u002Fblog\u002Fserp-dili-dingwei-zhuaqu-zhinan",null,"https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fserp-dili-dingwei-zhuaqu-zhinan-og.webp",[17,20,23],{"question":18,"answer":19},"为什么地理定位SERP抓取对代理用户很重要？","因为Google会交叉验证URL参数和出口IP的地理位置。如果uule指向纽约但代理IP在德国，Google可能忽略uule参数或触发CAPTCHA。使用与目标地区匹配的住宅代理可以确保所有信号一致，提高SERP数据的准确性和可复现性。对于需要按城市追踪排名的SEO系统，这是不可或缺的基础设施。",{"question":21,"answer":22},"哪种代理类型最适合地理定位SERP抓取？","住宅代理是最佳选择。它们使用真实ISP分配的IP地址，Google难以将其与真实用户流量区分。数据中心IP属于已知云服务商IP段，容易被Google识别并触发CAPTCHA或429限流。移动代理虽然反检测能力更强，但成本更高且速度较慢，适合需要移动端SERP数据的场景。综合性价比，住宅代理最优。",{"question":24,"answer":25},"如何在实现地理定位SERP抓取时避免被封禁？","避免封禁的关键策略包括：使用与目标地区匹配的住宅代理IP；控制请求频率，每IP每分钟不超过10-15个请求；使用curl_cffi等工具模拟真实浏览器TLS指纹；实现指数退避和重试机制应对429限流；多页抓取使用粘性会话保持同一IP；添加随机延迟模拟人类浏览行为；设置熔断器在连续限流时暂停请求。",false,"2026-07-31T16:11:16+00:00","2026-10-03T23:52:59+00:00","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fserp-dili-dingwei-zhuaqu-zhinan-master.webp","Geo-Targeted SERP Scraping Best Practices for Local SEO Data","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fserp-dili-dingwei-zhuaqu-zhinan-card.webp",12,{"name":34,"slug":35,"bio":36,"avatar_url":14,"website_url":14,"social_links":14},"ProxyHat Team","proxyhat-team","The ProxyHat editorial team covers proxies, web scraping, and data extraction.",[38],{"id":39,"slug":40,"name":41,"description":14,"meta_title":42,"meta_description":43,"parent_id":14},3,"serp-he-seo","SERP与SEO追踪","SERP与SEO追踪指南 | ProxyHat 博客","如何使用代理追踪搜索引擎排名和抓取SERP数据。",[],{"en":46,"x-default":46},"\u002Fblog\u002Fgeo-targeted-serp-scraping-best-practices",{"zh":13,"ja":48,"pl":49,"es":50,"it":51,"tr":52,"ko":53,"en":46,"ru":54,"de":55,"fr":56,"pt":57},"\u002Fja\u002Fblog\u002Fgeo-targeted-serp-scraping-guide","\u002Fpl\u002Fblog\u002Fgeotargetowane-scrapowanie-serp-najlepsze-praktyki","\u002Fes\u002Fblog\u002Fscraping-serp-geo-segmentado-mejores-practicas","\u002Fit\u002Fblog\u002Fscraping-serp-geo-targetato-best-practices","\u002Ftr\u002Fblog\u002Fcografi-hedefli-serp-kazima-rehberi","\u002Fko\u002Fblog\u002Fgeo-targeted-serp-scraping-guide","\u002Fru\u002Fblog\u002Fgeo-targeted-serp-scraping-best-practices","\u002Fde\u002Fblog\u002Fgeo-targeted-serp-scraping-leitfaden","\u002Ffr\u002Fblog\u002Fscraping-serp-geocible-meilleures-pratiques","\u002Fpt\u002Fblog\u002Fpraticas-serp-scraping-geo-targeted",59,[60,77,93,110],{"id":61,"slug":62,"title":63,"excerpt":64,"featured_image_url":65,"featured_image_alt":66,"card_image_url":67,"og_image_url":68,"published_at":69,"updated_at":69,"content_updated_at":70,"reading_time_minutes":71,"is_featured":26,"indexable":26,"views":72,"author":73,"categories":74,"tags":76},155,"amazon-guanjianci-paiming-zhuizong-daili-1","使用代理追踪亚马逊关键词排名：开发者完整指南","面向亚马逊卖家和 SEO 开发者的代码实战指南——用住宅代理、Python 和 Playwright\u002Fcurl_cffi 追踪关键词自然排名，解析搜索结果页，分离广告与自然位，并覆盖生产级重试、CAPTCHA 检测与合规注意事项。","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Famazon-guanjianci-paiming-zhuizong-daili-1-master.webp","Amazon Keyword Rank Tracking with Proxies: A Developer's Guide","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Famazon-guanjianci-paiming-zhuizong-daili-1-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Famazon-guanjianci-paiming-zhuizong-daili-1-og.webp","2026-07-16T16:05:31+00:00","2026-10-03T08:21:23+00:00",13,98,{"name":34,"slug":35,"bio":36,"avatar_url":14,"website_url":14,"social_links":14},[75],{"id":39,"slug":40,"name":41,"description":14,"meta_title":42,"meta_description":43,"parent_id":14},[],{"id":78,"slug":79,"title":80,"excerpt":81,"featured_image_url":82,"featured_image_alt":83,"card_image_url":84,"og_image_url":85,"published_at":86,"updated_at":86,"content_updated_at":87,"reading_time_minutes":32,"is_featured":26,"indexable":26,"views":88,"author":89,"categories":90,"tags":92},153,"zhuaqu-google-paa-zidongbuchquan-guanjianci","抓取Google People Also Ask与自动补全做关键词研究：Python实战指南","本指南教你用Python抓取Google自动补全、People Also Ask和相关搜索三大关键词金矿，结合ProxyHat住宅代理实现地理定位与防封锁，构建从种子词到长尾词的自动化扩展流程。","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhuaqu-google-paa-zidongbuchquan-guanjianci-master.webp","Scrape Google People Also Ask and Autocomplete for Keyword Research (Python Guide)","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhuaqu-google-paa-zidongbuchquan-guanjianci-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhuaqu-google-paa-zidongbuchquan-guanjianci-og.webp","2026-07-14T16:06:25+00:00","2026-10-03T23:53:01+00:00",124,{"name":34,"slug":35,"bio":36,"avatar_url":14,"website_url":14,"social_links":14},[91],{"id":39,"slug":40,"name":41,"description":14,"meta_title":42,"meta_description":43,"parent_id":14},[],{"id":94,"slug":95,"title":96,"excerpt":97,"featured_image_url":98,"featured_image_alt":99,"card_image_url":100,"og_image_url":101,"published_at":102,"updated_at":102,"content_updated_at":103,"reading_time_minutes":104,"is_featured":26,"indexable":26,"views":105,"author":106,"categories":107,"tags":109},154,"zhui-zong-google-ai-gailan-pinpai","如何追踪Google AI Overviews中的品牌表现：生成式搜索时代的可见性测量框架","Google AI Overviews已覆盖约36%的信息类查询，传统蓝链点击被压缩，引用份额成为新的排名KPI。本文为SEO负责人和数据PM提供一套从数据采集到ROI计算的AI搜索可见性追踪框架。","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhui-zong-google-ai-gailan-pinpai-master.webp","How to Track Your Brand in Google AI Overviews: A Strategic Guide for SEO & Data Teams","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhui-zong-google-ai-gailan-pinpai-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fzhui-zong-google-ai-gailan-pinpai-og.webp","2026-07-14T10:05:37+00:00","2026-10-03T23:53:00+00:00",10,66,{"name":34,"slug":35,"bio":36,"avatar_url":14,"website_url":14,"social_links":14},[108],{"id":39,"slug":40,"name":41,"description":14,"meta_title":42,"meta_description":43,"parent_id":14},[],{"id":111,"slug":112,"title":113,"excerpt":114,"featured_image_url":115,"featured_image_alt":116,"card_image_url":117,"og_image_url":118,"published_at":119,"updated_at":119,"content_updated_at":103,"reading_time_minutes":32,"is_featured":26,"indexable":26,"views":120,"author":121,"categories":122,"tags":124},152,"python-google-paiming-zhuizong","使用住宅代理在Python中构建Google排名追踪器：完整代码指南","本指南面向SEO工程师和Python开发者，演示如何用curl_cffi和ProxyHat住宅代理构建生产级Google排名追踪器，涵盖数据模型、SERP分页抓取、TLS指纹规避、重试策略与并发控制。","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fpython-google-paiming-zhuizong-master.webp","Build a Google Rank Tracker in Python with Residential Proxies","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fpython-google-paiming-zhuizong-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fzh\u002Fpython-google-paiming-zhuizong-og.webp","2026-07-02T16:04:58+00:00",56,{"name":34,"slug":35,"bio":36,"avatar_url":14,"website_url":14,"social_links":14},[123],{"id":39,"slug":40,"name":41,"description":14,"meta_title":42,"meta_description":43,"parent_id":14},[],[]]