[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-ko-geo-targeted-serp-scraping-guide":3},{"success":4,"payload":5,"related":56,"errors":123},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":23,"noindex_google_only":23,"allow_follow":4,"published_at":24,"updated_at":24,"content_updated_at":25,"featured_image_url":26,"featured_image_alt":27,"card_image_url":28,"reading_time_minutes":29,"author":30,"categories":34,"tags":41,"alternates":42,"locale_paths":44,"views":55},198,"geo-targeted-serp-scraping-guide","지역 타겟팅 SERP 스크래핑 모범 사례: uule부터 도시별 프록시까지","같은 키워드도 도시마다 SERP가 다릅니다. uule 파라미터 생성, gl·hl·google_domain 설정, 도시별 residential proxy 로테이션, CAPTCHA 백오프까지 코드 중심으로 정리한 가이드입니다.","\u003Cp>같은 키워드라도 서울, 뉴욕, 베를린에서 검색하면 Google SERP 결과가 다릅니다. 로컬 팩(Local Pack), 유기적 순위, 광고 배치가 모두 지역에 따라 변하기 때문입니다. 이 가이드는 지역 타겟팅 SERP 스크래핑 모범 사례를 코드 중심으로 정리하여, SEO 엔지니어와 랭크 트래커 개발자가 도시 단위 정밀도로 SERP 데이터셋을 구축할 수 있도록 돕습니다.\u003C\u002Fp>\u003Cp>국가 수준 타겟팅만으로는 로컬 SEO 분석에 한계가 있습니다. \"뉴욕의 배관공\"과 \"시애틀의 배관공\"은 같은 \u003Ccode>gl=us\u003C\u002Fcode>로 구분할 수 없습니다. 도시\u002F지역구 단위의 정밀 타겟팅이 필요하며, 이를 위해 \u003Ccode>uule\u003C\u002Fcode> 파라미터와 residential proxy의 조합이 필수적입니다.\u003C\u002Fp>\u003Ch2>지역 타겟팅 SERP 스크래핑 모범 사례: 왜 같은 검색어가 지역마다 다른 결과를 반환하는가\u003C\u002Fh2>\u003Cp>Google은 사용자의 IP 주소, 검색 언어, 계정 설정, 위치 정보를 종합하여 검색 결과를 개인화합니다. \u003Ca href=\"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FGoogle_Search\" rel=\"noopener noreferrer\">Google 검색 알고리즘\u003C\u002Fa>은 지역적 신호를 강하게 반영하므로, 동일 키워드라도 검색자의 물리적 위치에 따라 로컬 팩 구성, 비즈니스 등록 정보, 지도 결과가 크게 달라집니다.\u003C\u002Fp>\u003Cp>예를 들어 \"plumber near me\"를 뉴욕에서 검색하면 뉴욕 지역 배관업체가 로컬 팩에 노출되지만, 로스앤젤레스에서 검색하면 완전히 다른 업체 목록이 나타납니다. 국가 수준(\u003Ccode>gl=us\u003C\u002Fcode>)만 설정하면 이 두 도시의 차이를 구분할 수 없습니다. 도시\u002F지역구 정밀도가 필요한 이유입니다.\u003C\u002Fp>\u003Ch3>국가 수준 vs 도시 수준 타겟팅\u003C\u002Fh3>\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>타겟팅 수준\u003C\u002Fth>\u003Cth>파라미터\u003C\u002Fth>\u003Cth>정밀도\u003C\u002Fth>\u003Cth>적용 사례\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>국가\u003C\u002Ftd>\u003Ctd>\u003Ccode>gl=us\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>국가 전체\u003C\u002Ftd>\u003Ctd>국가별 랭크 추적\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>도시\u003C\u002Ftd>\u003Ctd>\u003Ccode>uule=...\u003C\u002Fcode>\u003C\u002Ftd>\u003Ctd>도시\u002F지역구\u003C\u002Ftd>\u003Ctd>로컬 SEO 분석\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>도시 + IP 일치\u003C\u002Ftd>\u003Ctd>\u003Ccode>uule\u003C\u002Fcode> + residential proxy\u003C\u002Ftd>\u003Ctd>최고 정밀도\u003C\u002Ftd>\u003Ctd>프랜차이즈 SEO, 경쟁 분석\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Ch2>핵심 현지화 파라미터 이해\u003C\u002Fh2>\u003Cp>Google SERP의 지역 타겟팅은 여러 파라미터의 조합으로 이루어집니다. 각 파라미터의 역할을 정확히 이해하고 서로 일치시켜야 합니다.\u003C\u002Fp>\u003Ch3>gl, hl, google_domain, lr\u003C\u002Fh3>\u003Cul>\u003Cli>\u003Cstrong>\u003Ccode>gl\u003C\u002Fcode>\u003C\u002Fstrong> — 검색 결과의 국가를 지정합니다 (예: \u003Ccode>gl=us\u003C\u002Fcode>, \u003Ccode>gl=de\u003C\u002Fcode>, \u003Ccode>gl=jp\u003C\u002Fcode>).\u003C\u002Fli>\u003Cli>\u003Cstrong>\u003Ccode>hl\u003C\u002Fcode>\u003C\u002Fstrong> — Google 인터페이스 언어를 설정합니다 (예: \u003Ccode>hl=en\u003C\u002Fcode>, \u003Ccode>hl=ko\u003C\u002Fcode>, \u003Ccode>hl=ja\u003C\u002Fcode>).\u003C\u002Fli>\u003Cli>\u003Cstrong>\u003Ccode>google_domain\u003C\u002Fcode>\u003C\u002Fstrong> — 검색 요청을 보낼 Google 도메인입니다 (예: \u003Ccode>google.com\u003C\u002Fcode>, \u003Ccode>google.de\u003C\u002Fcode>, \u003Ccode>google.co.jp\u003C\u002Fcode>).\u003C\u002Fli>\u003Cli>\u003Ccode>lr\u003C\u002Fcode> — 검색 결과의 언어를 제한합니다 (예: \u003Ccode>lr=lang_en\u003C\u002Fcode>, \u003Ccode>lr=lang_ko\u003C\u002Fcode>).\u003C\u002Fli>\u003C\u002Ful>\u003Cp>이 파라미터들은 서로 일치해야 합니다. \u003Ccode>gl=us\u003C\u002Fcode>인데 \u003Ccode>google_domain=google.de\u003C\u002Fcode>로 요청을 보내면 Google이 신호 충돌을 감지하여 결과 품질이 떨어지거나 CAPTCHA가 트리거될 수 있습니다.\u003C\u002Fp>\u003Ch3>uule 파라미터: 도시 단위 정밀 타겟팅\u003C\u002Fh3>\u003Cp>\u003Ccode>uule\u003C\u002Fcode>은 Google의 지리적 위치 인코딩 파라미터입니다. canonical place name을 base64로 인코딩하여 특정 도시\u002F지역의 SERP를 요청할 수 있습니다. 형식은 다음과 같습니다:\u003C\u002Fp>\u003Cpre>\u003Ccode>uule = &quot;w+CAIQICI&quot; + length_key + base64(canonical_name)\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>여기서 \u003Ccode>length_key\u003C\u002Fcode>는 base64 인코딩된 문자열의 길이를 단일 ASCII 문자로 표현한 것입니다. Python으로 구현하면 다음과 같습니다:\u003C\u002Fp>\u003Cpre>\u003Ccode>import base64\r\n\r\ndef build_uule(canonical_name: str) -&gt; str:\r\n    &quot;&quot;&quot;Build Google uule parameter from a canonical place name.\r\n    \r\n    Args:\r\n        canonical_name: e.g. &quot;New York,New York,United States&quot;\r\n    Returns:\r\n        uule string, e.g. &quot;w+CAIQICIkTmV3IFlvcmssTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==&quot;\r\n    &quot;&quot;&quot;\r\n    encoded = base64.b64encode(canonical_name.encode(&quot;utf-8&quot;)).decode(&quot;ascii&quot;)\r\n    # length_key: offset 63 maps base64 length to a printable ASCII char\r\n    length_key = chr(len(encoded) + 63)\r\n    return f&quot;w+CAIQICI{length_key}{encoded}&quot;\r\n\r\n# Example: pin search to Brooklyn, NY\r\nuule = build_uule(&quot;Brooklyn,New York,United States&quot;)\r\nprint(uule)\r\n# w+CAIQICIkQnJvb2tseW4sTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>\u003Cstrong>주의:\u003C\u002Fstrong> canonical name은 \"City,State,Country\" 형식을 따르며, Google의 canonical name 데이터베이스와 정확히 일치해야 합니다. 쉼표 뒤에는 공백이 없습니다. 잘못된 이름은 결과를 왜곡하거나 무시됩니다.\u003C\u002Fp>\u003Ch3>pws=0: 비개인화 기준선\u003C\u002Fh3>\u003Cp>\u003Ccode>pws=0\u003C\u002Fcode> 파라미터는 개인화된 검색 결과를 비활성화합니다. SERP 스크래핑에서는 일관된 비개인화 기준선이 필요하므로 항상 \u003Ccode>pws=0\u003C\u002Fcode>을 추가하는 것이 모범 사례입니다.\u003C\u002Fp>\u003Ch2>Python으로 지역 타겟팅 SERP 스크래핑 구현하기\u003C\u002Fh2>\u003Cp>이제 실전 코드를 살펴보겠습니다. \u003Ccode>curl_cffi\u003C\u002Fcode> 라이브러리를 사용해 Chrome 브라우저를 모방(impersonate)하고, ProxyHat residential proxy를 통해 요청을 보냅니다.\u003C\u002Fp>\u003Ch3>curl 예제: uule + gl\u002Fhl + 프록시\u003C\u002Fh3>\u003Cpre>\u003Ccode># Geo-targeted SERP request via ProxyHat residential proxy\r\ncurl -s &quot;https:\u002F\u002Fwww.google.com\u002Fsearch?q=plumber&amp;gl=us&amp;hl=en&amp;pws=0&amp;uule=w+CAIQICIkQnJvb2tseW4sTmV3IFlvcmssVW5pdGVkIFN0YXRlcw==&quot; \\\r\n  --proxy &quot;http:\u002F\u002Fuser-country-US-region-new_york-city-new_york_city:PASSWORD@gate.proxyhat.com:8080&quot; \\\r\n  -H &quot;User-Agent: Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F131.0.0.0 Safari\u002F537.36&quot; \\\r\n  -H &quot;Accept-Language: en-US,en;q=0.9&quot;\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>Python: uule 생성 + curl_cffi Chrome 모방 + residential proxy 로테이션\u003C\u002Fh3>\u003Cpre>\u003Ccode>import base64\nimport itertools\nfrom curl_cffi import requests as cffi_requests\n\nPROXYHAT_GATEWAY = &quot;gate.proxyhat.com:8080&quot;\nPROXYHAT_USER = &quot;YOUR_USER&quot;\nPROXYHAT_PASS = &quot;YOUR_PASS&quot;\n\n# --- Proxy rotation per locale ---\nproxy_cycle = itertools.cycle([\n    (&quot;US&quot;, &quot;new_york&quot;,        &quot;new_york_city&quot;, &quot;New York,New York,United States&quot;),\n    (&quot;US&quot;, &quot;california&quot;,      &quot;los_angeles&quot;,   &quot;Los Angeles,California,United States&quot;),\n    (&quot;US&quot;, &quot;illinois&quot;,        &quot;chicago&quot;,       &quot;Chicago,Illinois,United States&quot;),\n    (&quot;DE&quot;, &quot;state_of_berlin&quot;, &quot;berlin&quot;,        &quot;Berlin,Berlin,Germany&quot;),\n    (&quot;JP&quot;, &quot;tokyo&quot;,           &quot;tokyo&quot;,         &quot;Tokyo,Tokyo,Japan&quot;),\n])\n\ndef build_uule(canonical_name: str) -&gt; str:\n    encoded = base64.b64encode(canonical_name.encode(&quot;utf-8&quot;)).decode(&quot;ascii&quot;)\n    length_key = chr(len(encoded) + 63)\n    return f&quot;w+CAIQICI{length_key}{encoded}&quot;\n\ndef scrape_serp(query: str, country: str, region: str, city_slug: str, canonical: str) -&gt; str:\n    uule = build_uule(canonical)\n    proxy_url = (\n        f&quot;http:\u002F\u002F{PROXYHAT_USER}-country-{country}-region-{region}-city-{city_slug}:&quot;\n        f&quot;{PROXYHAT_PASS}@{PROXYHAT_GATEWAY}&quot;\n    )\n    \n    params = {\n        &quot;q&quot;: query,\n        &quot;gl&quot;: country.lower(),\n        &quot;hl&quot;: &quot;en&quot; if country == &quot;US&quot; else &quot;de&quot; if country == &quot;DE&quot; else &quot;ja&quot;,\n        &quot;pws&quot;: &quot;0&quot;,\n        &quot;uule&quot;: uule,\n        &quot;num&quot;: &quot;20&quot;,\n    }\n    \n    resp = cffi_requests.get(\n        &quot;https:\u002F\u002Fwww.google.com\u002Fsearch&quot;,\n        params=params,\n        proxies={&quot;http&quot;: proxy_url, &quot;https&quot;: proxy_url},\n        impersonate=&quot;chrome&quot;,\n        timeout=15,\n    )\n    resp.raise_for_status()\n    return resp.text\n\n# Rotate across locales\nfor country, region, city_slug, canonical in proxy_cycle:\n    html = scrape_serp(&quot;plumber near me&quot;, country, region, city_slug, canonical)\n    print(f&quot;[{country}\u002F{city_slug}] {len(html)} bytes received&quot;)\n    break  # first request only; remove for full rotation\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3>Selectolax로 유기적 결과 및 로컬 팩 파싱\u003C\u002Fh3>\u003Cpre>\u003Ccode>from selectolax.parser import HTMLParser\r\nfrom dataclasses import dataclass\r\nfrom typing import List\r\n\r\n@dataclass\r\nclass OrganicResult:\r\n    position: int\r\n    title: str\r\n    url: str\r\n    snippet: str\r\n\r\n@dataclass\r\nclass LocalPackResult:\r\n    name: str\r\n    rating: str\r\n    address: str\r\n\r\ndef parse_organic(html: str) -&gt; List[OrganicResult]:\r\n    tree = HTMLParser(html)\r\n    results: List[OrganicResult] = []\r\n    \r\n    for i, node in enumerate(tree.css(&quot;div.g&quot;), start=1):\r\n        title_node = node.css_first(&quot;h3&quot;)\r\n        link_node = node.css_first(&quot;a[href]&quot;)\r\n        snippet_node = node.css_first(&quot;div[data-sncf], div.VwiC3b, span.aCOpRe&quot;)\r\n        \r\n        if not title_node or not link_node:\r\n            continue\r\n        \r\n        results.append(OrganicResult(\r\n            position=i,\r\n            title=title_node.text(strip=True),\r\n            url=link_node.attributes.get(&quot;href&quot;, &quot;&quot;),\r\n            snippet=snippet_node.text(strip=True) if snippet_node else &quot;&quot;,\r\n        ))\r\n    return results\r\n\r\ndef parse_local_pack(html: str) -&gt; List[LocalPackResult]:\r\n    tree = HTMLParser(html)\r\n    results: List[LocalPackResult] = []\r\n    \r\n    for node in tree.css(&quot;div[data-cid], div.eTBCqc&quot;):\r\n        name_node = node.css_first(&quot;span.OSrXXb, div.dbg0p&quot;)\r\n        rating_node = node.css_first(&quot;span.MWvNee, span.BTtC6e&quot;)\r\n        addr_node = node.css_first(&quot;span.text, div.W4r3Bb&quot;)\r\n        \r\n        if not name_node:\r\n            continue\r\n        \r\n        results.append(LocalPackResult(\r\n            name=name_node.text(strip=True),\r\n            rating=rating_node.text(strip=True) if rating_node else &quot;&quot;,\r\n            address=addr_node.text(strip=True) if addr_node else &quot;&quot;,\r\n        ))\r\n    return results\r\n\r\n# Usage\r\norganic = parse_organic(html)\r\nlocal_pack = parse_local_pack(html)\r\n\r\nfor r in organic[:5]:\r\n    print(f&quot;#{r.position} {r.title} — {r.url}&quot;)\r\nfor lp in local_pack[:3]:\r\n    print(f&quot;  📍 {lp.name} | ⭐ {lp.rating} | {lp.address}&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>\u003Cstrong>주의:\u003C\u002Fstrong> Google의 HTML 구조는 자주 변경됩니다. 위 셀렉터는 2025년 기준으로 작동하지만, 주기적인 업데이트가 필요합니다. CSS 셀렉터에 의존하기보다 \u003Ccode>data-*\u003C\u002Fcode> 속성을 우선 활용하는 것이 안정적입니다.\u003C\u002Fp>\u003Ch3>Sticky Session: 다중 페이지 SERP 수집\u003C\u002Fh3>\u003Cp>같은 도시의 2페이지, 3페이지를 수집할 때는 동일한 IP를 유지해야 합니다. IP가 바뀌면 Google이 새 세션으로 간주하여 페이지네이션 컨텍스트가 끊깁니다. ProxyHat의 sticky session 기능을 사용하면 됩니다:\u003C\u002Fp>\u003Cpre>\u003Ccode>import base64\nimport time\nfrom curl_cffi import requests as cffi_requests\n\ndef build_uule(canonical_name: str) -&gt; str:\n    encoded = base64.b64encode(canonical_name.encode(&quot;utf-8&quot;)).decode(&quot;ascii&quot;)\n    return f&quot;w+CAIQICI{chr(len(encoded) + 63)}{encoded}&quot;\n\ndef scrape_multi_page(query: str, canonical: str, country: str,\n                      region: str, city_slug: str, max_pages: int = 3):\n    &quot;&quot;&quot;Scrape multiple SERP pages with a sticky session (same IP).&quot;&quot;&quot;\n    session_id = f&quot;serp-{country}-{city_slug}-001&quot;\n    proxy_url = (\n        f&quot;http:\u002F\u002FYOUR_USER-country-{country}-region-{region}-city-{city_slug}&quot;\n        f&quot;-sid-{session_id}:YOUR_PASS@gate.proxyhat.com:8080&quot;\n    )\n    uule = build_uule(canonical)\n    \n    all_results = []\n    for page in range(max_pages):\n        start = page * 10  # Google uses start=0,10,20...\n        params = {\n            &quot;q&quot;: query,\n            &quot;gl&quot;: country.lower(),\n            &quot;hl&quot;: &quot;en&quot;,\n            &quot;pws&quot;: &quot;0&quot;,\n            &quot;uule&quot;: uule,\n            &quot;start&quot;: str(start),\n        }\n        resp = cffi_requests.get(\n            &quot;https:\u002F\u002Fwww.google.com\u002Fsearch&quot;,\n            params=params,\n            proxies={&quot;http&quot;: proxy_url, &quot;https&quot;: proxy_url},\n            impersonate=&quot;chrome&quot;,\n            timeout=15,\n        )\n        if resp.status_code == 429:\n            print(f&quot;  [!] 429 on page {page+1}, backing off...&quot;)\n            time.sleep(30 * (page + 1))\n            continue\n        resp.raise_for_status()\n        all_results.append(resp.text)\n        print(f&quot;  Page {page+1}: {len(resp.text)} bytes&quot;)\n        time.sleep(2)  # polite delay\n    \n    return all_results\n\npages = scrape_multi_page(\n    &quot;plumber near me&quot;,\n    &quot;New York,New York,United States&quot;,\n    &quot;US&quot;, &quot;new_york&quot;, &quot;new_york_city&quot;\n)\nprint(f&quot;Collected {len(pages)} pages&quot;)\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>CAPTCHA \u002F 429 백오프 및 재시도 전략\u003C\u002Fh2>\u003Cp>대량 SERP 스크래핑에서 429(Too Many Requests)와 503(CAPTCHA) 응답은 불가피합니다. 지수 백오프와 회로 차단기 패턴을 적용해야 합니다:\u003C\u002Fp>\u003Cpre>\u003Ccode>import time\r\nimport random\r\nfrom functools import wraps\r\n\r\ndef retry_with_backoff(max_retries=5, base_delay=2, max_delay=120):\r\n    &quot;&quot;&quot;Exponential backoff with jitter for SERP scraping.&quot;&quot;&quot;\r\n    def decorator(func):\r\n        @wraps(func)\r\n        def wrapper(*args, **kwargs):\r\n            for attempt in range(max_retries):\r\n                try:\r\n                    result = func(*args, **kwargs)\r\n                    if result.get(&quot;status_code&quot;) in (429, 503):\r\n                        delay = min(\r\n                            base_delay * (2 ** attempt) + random.uniform(0, 3),\r\n                            max_delay\r\n                        )\r\n                        print(f&quot;  [!] {result[&apos;status_code&apos;]} — retry in &quot;\r\n                              f&quot;{delay:.1f}s (attempt {attempt+1}\u002F{max_retries})&quot;)\r\n                        time.sleep(delay)\r\n                        continue\r\n                    return result\r\n                except Exception as e:\r\n                    delay = min(\r\n                        base_delay * (2 ** attempt) + random.uniform(0, 3),\r\n                        max_delay\r\n                    )\r\n                    print(f&quot;  [!] Error: {e} — retry in {delay:.1f}s&quot;)\r\n                    time.sleep(delay)\r\n            raise RuntimeError(f&quot;Max retries ({max_retries}) exceeded&quot;)\r\n        return wrapper\r\n    return decorator\r\n\r\n# Circuit breaker: stop after N consecutive failures per country\r\nclass CountryCircuitBreaker:\r\n    def __init__(self, threshold=5, reset_timeout=300):\r\n        self.failures = {}\r\n        self.threshold = threshold\r\n        self.reset_timeout = reset_timeout  # 300 seconds = 5 minutes\r\n    \r\n    def record_failure(self, country: str):\r\n        self.failures[country] = self.failures.get(country, 0) + 1\r\n    \r\n    def record_success(self, country: str):\r\n        self.failures[country] = 0\r\n    \r\n    def is_open(self, country: str) -&gt; bool:\r\n        return self.failures.get(country, 0) &gt;= self.threshold\r\n\r\nbreaker = CountryCircuitBreaker(threshold=5, reset_timeout=300)\r\n\r\n# Per-country proxy pool with fallback\r\nPROXY_POOLS = {\r\n    &quot;US&quot;: [&quot;newyork&quot;, &quot;losangeles&quot;, &quot;chicago&quot;, &quot;houston&quot;, &quot;phoenix&quot;],\r\n    &quot;DE&quot;: [&quot;berlin&quot;, &quot;munich&quot;, &quot;hamburg&quot;, &quot;frankfurt&quot;],\r\n    &quot;JP&quot;: [&quot;tokyo&quot;, &quot;osaka&quot;, &quot;yokohama&quot;, &quot;nagoya&quot;],\r\n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>이 패턴을 사용하면 한 국가의 프록시 풀에서 연속 5회 실패 시 해당 국가 요청을 5분간 중단하고, 다른 국가 풀로 전환할 수 있습니다. \u003Ccode>99.9% uptime\u003C\u002Fcode>을 목표로 하는 서비스에서는 이러한 회로 차단기가 필수적입니다.\u003C\u002Fp>\u003Ch2>Node.js 예제: ProxyHat으로 지역 타겟팅 SERP 수집\u003C\u002Fh2>\u003Cpre>\u003Ccode>const https = require(&quot;https&quot;);\nconst { HttpsProxyAgent } = require(&quot;https-proxy-agent&quot;);\n\nfunction buildUule(canonicalName) {\n    const encoded = Buffer.from(canonicalName, &quot;utf-8&quot;).toString(&quot;base64&quot;);\n    const lengthKey = String.fromCharCode(encoded.length + 63);\n    return `w+CAIQICI${lengthKey}${encoded}`;\n}\n\nasync function scrapeSerp(query, country, region, citySlug, canonical) {\n    const uule = buildUule(canonical);\n    const proxyUrl = `http:\u002F\u002FYOUR_USER-country-${country}-region-${region}-city-${citySlug}:YOUR_PASS@gate.proxyhat.com:8080`;\n    const agent = new HttpsProxyAgent(proxyUrl);\n    \n    const params = new URLSearchParams({\n        q: query,\n        gl: country.toLowerCase(),\n        hl: &quot;en&quot;,\n        pws: &quot;0&quot;,\n        uule: uule,\n        num: &quot;20&quot;,\n    });\n    \n    return new Promise((resolve, reject) =&gt; {\n        const url = `https:\u002F\u002Fwww.google.com\u002Fsearch?${params.toString()}`;\n        https.get(url, {\n            agent,\n            headers: {\n                &quot;User-Agent&quot;: &quot;Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36&quot;,\n                &quot;Accept-Language&quot;: &quot;en-US,en;q=0.9&quot;,\n            },\n        }, (res) =&gt; {\n            let data = &quot;&quot;;\n            res.on(&quot;data&quot;, (chunk) =&gt; data += chunk);\n            res.on(&quot;end&quot;, () =&gt; resolve({ status: res.statusCode, body: data }));\n            res.on(&quot;error&quot;, reject);\n        });\n    });\n}\n\n\u002F\u002F Execute\nscrapeSerp(&quot;plumber near me&quot;, &quot;US&quot;, &quot;new_york&quot;, &quot;new_york_city&quot;, &quot;New York,New York,United States&quot;)\n    .then(r =&gt; console.log(`[${r.status}] ${r.body.length} bytes`))\n    .catch(e =&gt; console.error(e));\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch2>ProxyHat으로 지역 타겟팅 프록시 설정하기\u003C\u002Fh2>\u003Cp>ProxyHat은 residential, mobile, datacenter 프록시를 제공하며, 사용자 이름에 국가 및 도시 타겟팅 플래그를 포함할 수 있습니다. \u003Ca href=\"\u002Fko\u002Flocations\">지원 위치 목록\u003C\u002Fa>에서 사용 가능한 국가와 도시를 확인하세요.\u003C\u002Fp>\u003Ch3>프록시 유형별 비교\u003C\u002Fh3>\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>프록시 유형\u003C\u002Fth>\u003Cth>신뢰도\u003C\u002Fth>\u003Cth>평균 지연\u003C\u002Fth>\u003Cth>지역 정밀도\u003C\u002Fth>\u003Cth>추천 용도\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Residential\u003C\u002Ftd>\u003Ctd>높음\u003C\u002Ftd>\u003Ctd>~200ms\u003C\u002Ftd>\u003Ctd>도시\u002F지역구\u003C\u002Ftd>\u003Ctd>SERP 스크래핑 (최우선)\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Mobile\u003C\u002Ftd>\u003Ctd>최고\u003C\u002Ftd>\u003Ctd>~300ms\u003C\u002Ftd>\u003Ctd>도시\u003C\u002Ftd>\u003Ctd>모바일 SERP, 민감 대상\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Datacenter\u003C\u002Ftd>\u003Ctd>낮음\u003C\u002Ftd>\u003Ctd>~50ms\u003C\u002Ftd>\u003Ctd>국가\u003C\u002Ftd>\u003Ctd>대량 비 SERP 수집\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>SERP 스크래핑에서는 residential 프록시가 가장 안정적인 선택입니다. Google은 datacenter IP를 쉽게 감지하므로, \u003Ccode>gate.proxyhat.com:8080\u003C\u002Fcode>을 통해 residential IP를 사용하는 것이 핵심입니다. \u003Ca href=\"\u002Fko\u002Fpricing\">프록시 가격\u003C\u002Fa>은 트래픽 기반이며, SERP 수집에 최적화된 플랜을 확인할 수 있습니다.\u003C\u002Fp>\u003Cp>ProxyHat SDK를 사용하면 프록시 관리를 더 간단하게 할 수 있습니다. \u003Ca href=\"https:\u002F\u002Fdocs.proxyhat.com\u002F\" rel=\"noopener noreferrer\">ProxyHat 문서\u003C\u002Fa>에서 SDK 설치 및 사용법을 참조하세요. \u003Ca href=\"\u002Fko\u002Fuse-cases\u002Fweb-scraping\">웹 스크래핑 사용 사례\u003C\u002Fa>와 \u003Ca href=\"\u002Fko\u002Fuse-cases\u002Fserp-tracking\">SERP 추적 사용 사례\u003C\u002Fa> 페이지에서 더 많은 예제를 확인할 수 있습니다.\u003C\u002Fp>\u003Ch2>흔한 실수와 엣지 케이스\u003C\u002Fh2>\u003Ch3>1. 파라미터 불일치\u003C\u002Fh3>\u003Cp>\u003Ccode>gl=us\u003C\u002Fcode>인데 IP가 독일에서 나오면 Google이 신호 충돌을 감지합니다. 항상 \u003Ccode>gl\u003C\u002Fcode>, \u003Ccode>google_domain\u003C\u002Fcode>, \u003Ccode>uule\u003C\u002Fcode>, 그리고 프록시 IP 지역이 일치해야 합니다. ProxyHat 사용자 이름에 \u003Ccode>-country-US-region-new_york-city-new_york_city\u003C\u002Fcode> 플래그를 추가하면 IP 지역과 파라미터를 자동으로 일치시킬 수 있습니다.\u003C\u002Fp>\u003Ch3>2. uule canonical name 오타\u003C\u002Fh3>\u003Cp>\"New York City, NY, USA\"가 아니라 \"New York,New York,United States\" 형식을 사용해야 합니다. Google의 canonical name 데이터베이스와 정확히 일치해야 하며, 쉼표 뒤에는 공백이 없습니다.\u003C\u002Fp>\u003Ch3>3. 개인화 신호 누락\u003C\u002Fh3>\u003Cp>\u003Ccode>pws=0\u003C\u002Fcode>을 빼면 검색 이력과 계정 상태에 따라 결과가 달라집니다. 비개인화 기준선이 필요하면 항상 포함하세요.\u003C\u002Fp>\u003Ch3>4. robots.txt 및 ToS 준수\u003C\u002Fh3>\u003Cp>Google의 \u003Ca href=\"https:\u002F\u002Fdevelopers.google.com\u002Fcrawling\u002Fdocs\u002Frobots-txt\u002Frobots-txt-spec\" rel=\"noopener noreferrer\">robots.txt 가이드라인\u003C\u002Fa>을 확인하고, Google 서비스 약관을 준수해야 합니다. 공개 데이터 수집이더라도 대량 자동화 요청은 ToS 위반으로 간주될 수 있으므로, 적정 요청 속도(예: 요청 간 2초 이상)를 유지하고 상업적 이용 전 법무 검토를 권장합니다.\u003C\u002Fp>\u003Ch3>5. HTML 구조 변경\u003C\u002Fh3>\u003Cp>Google은 주기적으로 SERP HTML 구조를 변경합니다. 셀렉터 기반 파서는 월 1회 이상 검증해야 하며, \u003Ccode>data-*\u003C\u002Fcode> 속성 기반 파싱이 더 안정적입니다. 100개 이상의 동시 세션을 유지하는 대규모 수집에서는 HTML 변경 모니터링을 자동화하는 것이 좋습니다.\u003C\u002Fp>\u003Ch2>핵심 요약\u003C\u002Fh2>\u003Cblockquote>\u003Cul>\u003Cli>uule + gl\u002Fhl + pws=0 조합으로 도시 단위 비개인화 SERP를 수집하세요.\u003C\u002Fli>\u003Cli>residential proxy를 사용하고 \u003Ccode>-country-XX-region-RR-city-YY\u003C\u002Fcode> 플래그로 IP 지역을 uule과 일치시키세요.\u003C\u002Fli>\u003Cli>sticky session으로 다중 페이지 수집 시 동일 IP를 유지하세요.\u003C\u002Fli>\u003Cli>지수 백오프 + 회로 차단기로 429\u002F503 응답에 대응하세요.\u003C\u002Fli>\u003Cli>canonical name은 Google 데이터베이스와 정확히 일치해야 합니다.\u003C\u002Fli>\u003Cli>robots.txt와 ToS를 항상 준수하세요.\u003C\u002Fli>\u003C\u002Ful>\u003C\u002Fblockquote>","지역 타겟팅 SERP 스크래핑 모범 사례 | ProxyHat","지역 타겟팅 SERP 스크래핑 모범 사례를 코드 중심으로 정리합니다. uule 파라미터 생성, gl·hl·google_domain 설정, 도시별 residential proxy 로테이션, CAPTCHA 429 백오프 전략까지 실전 코드로 확인하세요.","\u002Fko\u002Fblog\u002Fgeo-targeted-serp-scraping-guide",null,"https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgeo-targeted-serp-scraping-guide-og.webp",[17,20],{"question":18,"answer":19},"지역 타겟팅 SERP 스크래핑에 어떤 프록시 유형이 가장 적합한가?","residential 프록시가 가장 적합합니다. datacenter IP는 Google이 쉽게 감지하여 차단률이 높고, mobile 프록시는 신뢰도가 높지만 지연이 ~300ms로 상대적으로 느립니다. residential 프록시는 ~200ms 지연으로 도시\u002F지역구 단위 타겟팅을 지원하며 차단률이 낮아 SERP 스크래핑의 최우선 선택입니다. ProxyHat의 gate.proxyhat.com:8080 게이트웨이에서 -country-XX-region-RR-city-YY 플래그로 도시를 지정할 수 있습니다.",{"question":21,"answer":22},"지역 타겟팅 SERP 스크래핑에서 차단을 어떻게 피하는가?","차단을 피하려면 지수 백오프 재시도, 회로 차단기 패턴, 국가별 프록시 풀 분리, 요청 간 2초 이상 지연, Chrome 브라우저 모방(impersonate), sticky session으로 다중 페이지 일관성 유지를 조합해야 합니다. 429나 503 응답 시 즉시 30~120초 백오프하고, 한 국가에서 연속 5회 실패 시 5분간 해당 국가 요청을 중단하는 회로 차단기를 적용하는 것이 효과적입니다.",false,"2026-07-31T16:11:16+00:00","2026-10-03T23:52:59+00:00","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgeo-targeted-serp-scraping-guide-master.webp","Geo-Targeted SERP Scraping Best Practices for Local SEO Data","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgeo-targeted-serp-scraping-guide-card.webp",12,{"name":31,"slug":32,"bio":33,"avatar_url":14,"website_url":14,"social_links":14},"ProxyHat Team","proxyhat-team","The ProxyHat editorial team covers proxies, web scraping, and data extraction.",[35],{"id":36,"slug":37,"name":38,"description":14,"meta_title":39,"meta_description":40,"parent_id":14},3,"serp-wa-seo","SERP 및 SEO 추적","SERP 및 SEO 추적 가이드 | ProxyHat 블로그","프록시를 사용한 검색 엔진 순위 추적 및 SERP 데이터 수집 가이드.",[],{"en":43,"x-default":43},"\u002Fblog\u002Fgeo-targeted-serp-scraping-best-practices",{"zh":45,"ja":46,"pl":47,"es":48,"it":49,"tr":50,"ko":13,"en":43,"ru":51,"de":52,"fr":53,"pt":54},"\u002Fcn\u002Fblog\u002Fserp-dili-dingwei-zhuaqu-zhinan","\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","\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,[57,74,90,107],{"id":58,"slug":59,"title":60,"excerpt":61,"featured_image_url":62,"featured_image_alt":63,"card_image_url":64,"og_image_url":65,"published_at":66,"updated_at":66,"content_updated_at":67,"reading_time_minutes":68,"is_featured":23,"indexable":23,"views":69,"author":70,"categories":71,"tags":73},155,"amazon-keyword-rank-tracker-proxy-guide","프록시를 활용한 아마존 키워드 순위 추적 개발자 가이드","아마존 검색 결과는 자체적인 SERP 알고리즘을 따릅니다. 레지덴셜 프록시와 Python을 활용해 키워드별 유기적 순위를 추적하고, 스폰서드 광고를 분리하며, 차단을 회피하는 방법을 코드 중심으로 설명합니다.","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Famazon-keyword-rank-tracker-proxy-guide-master.webp","Amazon Keyword Rank Tracking with Proxies: A Developer's Guide","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Famazon-keyword-rank-tracker-proxy-guide-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Famazon-keyword-rank-tracker-proxy-guide-og.webp","2026-07-16T16:05:31+00:00","2026-10-03T08:21:21+00:00",13,98,{"name":31,"slug":32,"bio":33,"avatar_url":14,"website_url":14,"social_links":14},[72],{"id":36,"slug":37,"name":38,"description":14,"meta_title":39,"meta_description":40,"parent_id":14},[],{"id":75,"slug":76,"title":77,"excerpt":78,"featured_image_url":79,"featured_image_alt":80,"card_image_url":81,"og_image_url":82,"published_at":83,"updated_at":83,"content_updated_at":84,"reading_time_minutes":29,"is_featured":23,"indexable":23,"views":85,"author":86,"categories":87,"tags":89},153,"google-paa-autocomplete-scraping-python-1","구글 PAA와 자동완성 스크래핑으로 키워드 리서치하기 (Python 가이드)","구글 자동완성, People Also Ask, Related Searches를 스크래핑하여 키워드 리서치 파이프라인을 구축하는 개발자 가이드입니다. httpx와 Playwright를 활용한 실행 가능한 Python 예제와 residential proxy 설정을 함께 다룹니다.","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-paa-autocomplete-scraping-python-1-master.webp","Scrape Google People Also Ask and Autocomplete for Keyword Research (Python Guide)","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-paa-autocomplete-scraping-python-1-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-paa-autocomplete-scraping-python-1-og.webp","2026-07-14T16:06:25+00:00","2026-10-03T08:29:54+00:00",124,{"name":31,"slug":32,"bio":33,"avatar_url":14,"website_url":14,"social_links":14},[88],{"id":36,"slug":37,"name":38,"description":14,"meta_title":39,"meta_description":40,"parent_id":14},[],{"id":91,"slug":92,"title":93,"excerpt":94,"featured_image_url":95,"featured_image_alt":96,"card_image_url":97,"og_image_url":98,"published_at":99,"updated_at":99,"content_updated_at":100,"reading_time_minutes":101,"is_featured":23,"indexable":23,"views":102,"author":103,"categories":104,"tags":106},154,"google-ai-overview-teuraeging-gaideu","구글 AI 오버뷰에서 브랜드 가시성 추적 전략 가이드","구글 AI 오버뷰가 검색 결과의 판도를 바꾼 지금, 브랜드 인용 점유율을 측정하는 것이 새로운 순위 KPI입니다. 이 가이드는 데이터 PM과 SEO 리드를 위한 실전 추적 파이프라인 구축 전략을 다룹니다.","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-ai-overview-teuraeging-gaideu-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\u002Fko\u002Fgoogle-ai-overview-teuraeging-gaideu-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-ai-overview-teuraeging-gaideu-og.webp","2026-07-14T10:05:37+00:00","2026-10-03T08:29:51+00:00",10,66,{"name":31,"slug":32,"bio":33,"avatar_url":14,"website_url":14,"social_links":14},[105],{"id":36,"slug":37,"name":38,"description":14,"meta_title":39,"meta_description":40,"parent_id":14},[],{"id":108,"slug":109,"title":110,"excerpt":111,"featured_image_url":112,"featured_image_alt":113,"card_image_url":114,"og_image_url":115,"published_at":116,"updated_at":116,"content_updated_at":117,"reading_time_minutes":29,"is_featured":23,"indexable":23,"views":118,"author":119,"categories":120,"tags":122},152,"google-rank-tracker-python-residential-proxy","파이썬으로 레지덴셜 프록시 기반 구글 랭크 트래커 만들기","구글이 num=100을 제거한 2026년 환경에서 curl_cffi와 ProxyHat 레지덴셜 프록시를 활용해 파이썬 랭크 트래커를 구축하는 방법을 코드 중심으로 설명합니다. 데이터 모델, SERP 페이지네이션, 프로덕션 강화까지.","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-rank-tracker-python-residential-proxy-master.webp","Build a Google Rank Tracker in Python with Residential Proxies","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-rank-tracker-python-residential-proxy-card.webp","https:\u002F\u002Fstatic.proxyhat.com\u002Fstorage\u002Fblog-covers\u002Fko\u002Fgoogle-rank-tracker-python-residential-proxy-og.webp","2026-07-02T16:04:58+00:00","2026-10-03T23:53:00+00:00",56,{"name":31,"slug":32,"bio":33,"avatar_url":14,"website_url":14,"social_links":14},[121],{"id":36,"slug":37,"name":38,"description":14,"meta_title":39,"meta_description":40,"parent_id":14},[],[]]