파이썬과 레지덴셜 프록시로 구글 랭크 트래커 구축하기: 개요
SEO 엔지니어와 파이썬 개발자에게 순위 추적은 일상적이지만 까다로운 작업이다. 상용 랭크 트래커는 키워드당 월 $0.50~$2를 부과하고, 원시 데이터 접근을 제한하며, 커스텀 지표 추가를 허용하지 않는 경우가 많다. 파이썬과 레지덴셜 프록시로 구글 랭크 트래커를 구축하면 전체 제어권을 확보하고 비용을 90% 이상 절감할 수 있다.
이 가이드는 2026년 현재 구글 SERP 구조에 맞춘 프로덕션 준비 완료된 랭크 트래커를 구축하는 전체 과정을 다룬다. 핵심은 세 가지다: start=0,10,20 페이지네이션으로 상위 100개 결과 수집, curl_cffi로 Chrome TLS 지문 모방, ProxyHat 레지덴셜 프록시로 도시 수준 지역 타겟팅과 키워드별 고정 세션 적용. 이 조합이 없으면 구글의 봇 탐지가 평균 200ms 이내에 요청을 차단한다.
다음 섹션에서 데이터 모델부터 프로덕션 배포까지 단계별로 살펴본다. 모든 코드는 직접 실행할 수 있으며, ProxyHat 게이트웨이(gate.proxyhat.com:8080)를 사용한다. SERP 스크래핑 파이썬 2026 환경에 최적화된 코드를 제공한다.
데이터 모델 — 키워드, 타겟 도메인, 순위, 그리고 일일 스냅샷의 가치
랭크 트래커의 핵심은 단순한 데이터 모델에서 출발한다. 다음 필드가 최소 요구 사항이다:
- keyword — 추적할 검색어 (예: "best running shoes")
- target_domain — 순위를 확인할 도메인 (예: "nike.com")
- country — 검색 지역 (예: "US", "DE", "JP")
- device — desktop 또는 mobile
- position — 자연 검색 결과 내 순위 (1~100)
- captured_at — UTC 타임스탬프
일일 스냅샷이 일회성 체크보다 나은 이유
일회성 순위 체크는 특정 시점의 스냅샷만 제공한다. 하지만 구글의 순위는 하루에도 여러 번 변동한다. 일일 스냅샷을 저장하면 다음을 할 수 있다:
- 순위 변동성 패턴 파악 — 주말 vs 평일, 알고리즘 업데이트 전후 비교
- 7일/30일 이동 평균으로 노이즈 제거 — 하루 순위가 ±3위 변동해도 트렌드는 안정적
- 알고리즘 업데이트 감지 — 여러 키워드의 순위가 동시에 급변하면 코어 업데이트 신호
SQLite는 키워드 1,000개 × 일일 스냅샷 기준으로 1년에 약 365,000행을 생성하지만, 디스크 사용량은 50 MB 미만이다. CSV 백업을 병행하면 데이터 이식성도 확보된다.
SERP 가져오기 — num=100 폐지 후의 페이지네이션 전략
2025년 9월, 구글은 num=100 파라미터를 공식적으로 폐지했다. 이제 num 값과 무관하게 페이지당 10개 결과만 반환한다. 상위 100개 결과를 수집하려면 start=0,10,20...90으로 10페이지를 순회해야 한다.
Google Search Central 문서에 따르면, 자연 검색 결과는 광고, 지역 팩, 추천 스니펫과 구분되어야 한다. 랭크 트래커는 자연 결과의 순위만 추적해야 한다.
페이지네이션 구현
각 페이지는 독립적인 HTTP 요청이 필요하다. 10페이지 = 10개 요청이므로, 프록시 회전과 지연이 필수적이다. 다음은 기본 curl 예제다:
curl -x "http://user-country-US-city-chicago-session-mykw123:pass@gate.proxyhat.com:8080" \
"https://www.google.com/search?q=best+running+shoes&start=0&gl=us&hl=en" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
start 값을 0, 10, 20...으로 변경하면서 각 페이지의 HTML을 수집한다. 각 요청마다 새 세션 ID를 사용하면 IP가 회전되지만, 같은 키워드 내에서는 고정 세션을 유지하는 것이 안정적이다.
레지덴셜 프록시가 필수인 이유 — TLS 지문과 IP 평판
구글은 단순히 IP 주소만 보지 않는다. curl_cffi 프로젝트가 입증하듯, 구글은 TLS 핸드셰이크의 JA3/JA4 지문을 분석하여 비브라우저 클라이언트를 탐지한다. 표준 requests 라이브러리의 TLS 지문은 Python urllib3의 것이어서 즉시 차단된다.
이중 방어선이 필요하다:
- TLS 지문 모방 —
curl_cffi의impersonate="chrome"옵션으로 실제 Chrome 브라우저의 TLS 핸드셰이크를 재현 - IP 평판 우회 — 레지덴셜 프록시로 실제 ISP IP를 사용. 데이터센터 IP는 구글의 IP 평판 데이터베이스에서 "known bot"으로 분류되어 99% 차단률을 보인다.
프록시 유형 비교
| 프록시 유형 | TLS 지문 통과 | IP 평판 | 지역 타겟팅 | 추천 용도 |
|---|---|---|---|---|
| 데이터센터 | 불가 (IP만으로 차단) | 낮음 (봇으로 분류) | 국가 수준 | 비추천 |
| 레지덴셜 | curl_cffi와 조합 시 가능 | 높음 (실제 ISP) | 도시 수준 | 랭크 트래킹 최적 |
| 모바일 | curl_cffi와 조합 시 가능 | 최고 (4G/5G IP) | 국가 수준 | 모바일 순위 추적 |
ProxyHat 레지덴셜 프록시는 사용자 이름에 지역과 세션을 지정할 수 있다. 예: user-country-US-city-chicago-session-mykw123:pass. 이렇게 하면 시카고 ISP IP로 고정되어, 같은 키워드의 순위가 지역별로 다를 때 정확한 데이터를 얻을 수 있다. 지원 지역 목록에서 전체 국가/도시 코드를 확인할 수 있다.
curl_cffi와 ProxyHat으로 구글 랭크 트래커 프록시 구현하기
이제 실제 구현에 들어간다. curl_cffi로 Chrome TLS 지문을 모방하고, ProxyHat 레지덴셜 프록시로 요청을 보내며, 결과를 파싱하여 SQLite에 저장한다. 파이썬 랭크 트래커 구축의 핵심은 사용자 이름 파라미터로 프록시를 제어하는 것이다.
1단계: 기본 SERP 가져오기
from curl_cffi import requests as cffi_requests
proxy_url = "http://user-country-US-city-chicago-session-mykw123:pass@gate.proxyhat.com:8080"
resp = cffi_requests.get(
"https://www.google.com/search?q=best+running+shoes&start=0&gl=us&hl=en",
proxies={"https": proxy_url},
impersonate="chrome",
timeout=30,
)
print(f"Status: {resp.status_code}, Length: {len(resp.text)}")
impersonate="chrome"이 핵심이다. 이 한 줄로 TLS 지문이 Chrome과 일치하여, 구글의 JA3/JA4 탐지를 우회한다. timeout=30초는 프록시 지연을 고려한 여유 값이다.
2단계: 자연 검색 결과 파싱
import re
def parse_organic_results(html):
"""HTML에서 자연 검색 결과 URL을 추출한다."""
pattern = r'<a href="/url\?q=(https?://[^&"]+)&[^"]*"'
urls = re.findall(pattern, html)
seen = set()
results = []
for u in urls:
if u not in seen:
seen.add(u)
results.append(u)
return results
def find_position(results, target_domain):
"""타겟 도메인의 순위를 찾는다."""
for i, url in enumerate(results, 1):
if target_domain in url:
return i
return None
구글의 HTML 구조는 자주 변경되므로, 정규식 기반 파싱은 보수적으로 작성해야 한다. CSS 선택자(BeautifulSoup)를 병행하면 더 견고하지만, /url?q= 리다이렉트 패턴은 안정적으로 유지되고 있다.
3단계: 상위 100개 수집 및 SQLite 저장
import sqlite3
from datetime import datetime, timezone
DB_PATH = "rank_history.db"
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS rankings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
target_domain TEXT NOT NULL,
country TEXT NOT NULL,
device TEXT NOT NULL,
position INTEGER,
captured_at TEXT NOT NULL
)
""")
conn.commit()
return conn
def track_keyword(keyword, target_domain, country="US", city="chicago"):
conn = init_db()
all_results = []
session_id = keyword.replace(" ", "-")
for start in [0, 10, 20]:
proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
url = f"https://www.google.com/search?q={keyword}&start={start}&gl={country.lower()}&hl=en"
resp = cffi_requests.get(
url,
proxies={"https": proxy_url},
impersonate="chrome",
timeout=30,
)
resp.raise_for_status()
all_results.extend(parse_organic_results(resp.text))
position = find_position(all_results, target_domain)
captured_at = datetime.now(timezone.utc).isoformat()
conn.execute(
"INSERT INTO rankings (keyword, target_domain, country, device, position, captured_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(keyword, target_domain, country, "desktop", position, captured_at)
)
conn.commit()
conn.close()
print(f"[{keyword}] {target_domain} -> position {position}")
return position
track_keyword("best running shoes", "nike.com")
이 코드는 3페이지(상위 30개)를 수집한다. 상위 100개가 필요하면 [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]으로 확장한다. 각 요청마다 2~5초 지연을 추가하는 것이 안전하다.
프로덕션 강화 — 재시도, CAPTCHA 감지, 동시성 제어
프로덕션 환경에서는 네트워크 오류, CAPTCHA, 속도 제한이 일어난다. 다음 패턴들로 이를 처리한다.
지수 백오프 재시도와 CAPTCHA 감지
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rank_tracker")
CAPTCHA_INDICATORS = ["unusual traffic", "captcha", "detected", "verify"]
def detect_captcha(html):
html_lower = html.lower()
return any(ind in html_lower for ind in CAPTCHA_INDICATORS)
def fetch_serp_safe(keyword, country="US", city="chicago", start=0, max_retries=3):
session_id = keyword.replace(" ", "-")
proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
url = f"https://www.google.com/search?q={keyword}&start={start}&gl={country.lower()}&hl=en"
for attempt in range(max_retries):
try:
resp = cffi_requests.get(
url,
proxies={"https": proxy_url},
impersonate="chrome",
timeout=30,
)
if detect_captcha(resp.text):
logger.warning(f"CAPTCHA detected on attempt {attempt+1}")
session_id = f"{session_id}-r{attempt}"
proxy_url = f"http://user-country-{country}-city-{city}-session-{session_id}:pass@gate.proxyhat.com:8080"
time.sleep(2 ** attempt)
continue
resp.raise_for_status()
return resp.text
except Exception as e:
delay = 2 ** attempt
logger.warning(f"Attempt {attempt+1}/{max_retries} failed: {e}. Retrying in {delay}s")
if attempt < max_retries - 1:
time.sleep(delay)
else:
raise
return None
CAPTCHA가 감지되면 세션 ID를 변경하여 새 IP를 할당받고 지수 백오프로 대기한다. 이 패턴은 CAPTCHA가 연속으로 발생해도 최대 3회 재시도하며, 4초, 8초 지연을 적용한다.
동시성 제어
from concurrent.futures import ThreadPoolExecutor, as_completed
def track_multiple_keywords(keywords, target_domain, country="US", city="chicago", max_workers=5):
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(track_keyword, kw, target_domain, country, city): kw
for kw in keywords
}
for future in as_completed(futures):
kw = futures[future]
try:
pos = future.result()
results[kw] = pos
except Exception as e:
logger.error(f"Failed to track '{kw}': {e}")
results[kw] = None
return results
keywords = ["best running shoes", "running shoes review", "marathon shoes"]
results = track_multiple_keywords(keywords, "nike.com", max_workers=5)
for kw, pos in results.items():
print(f"{kw}: position {pos}")
max_workers=5는 보수적인 값이다. ProxyHat 레지덴셜 프록시는 100개 이상의 동시 세션을 지원하지만, 구글의 속도 제한을 피하려면 5~10개 동시 요청이 안전하다. ProxyHat 요금제에서 동시 세션 한도를 확인할 수 있다.
순위 변동성 스무딩
import csv
def export_csv(keyword, output_file="rank_history.csv"):
conn = sqlite3.connect(DB_PATH)
rows = conn.execute(
"SELECT captured_at, position FROM rankings WHERE keyword = ? ORDER BY captured_at",
(keyword,)
).fetchall()
conn.close()
with open(output_file, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["captured_at", "position", "7day_ma"])
positions = [r[1] for r in rows]
for i, (ts, pos) in enumerate(rows):
window = positions[max(0, i-6):i+1]
ma = sum(window) / len(window) if window else None
writer.writerow([ts, pos, f"{ma:.1f}" if ma else ""])
print(f"Exported {len(rows)} rows to {output_file}")
7일 이동 평균은 일일 노이즈를 제거하여 실제 트렌드를 보여준다. 순위가 일시적으로 5위에서 8위로 떨어져도 7일 평균이 6.2위로 안정적이라면 알고리즘 페널티가 아닌 자연스러운 변동으로 판단할 수 있다.
윤리와 한계 — 책임감 있는 랭크 추적
구글 랭크 트래킹은 구글의 서비스 약관과 충돌할 수 있다. 다음 원칙을 지킨다:
- 본인 소유 도메인 추적 — 자신의 웹사이트 순위를 모니터링하는 것이 가장 안전하다.
- 공개 순위만 추적 — 로그인이 필요한 개인화된 결과는 수집하지 않는다.
- 속도 제한 준수 — 키워드당 10페이지 요청 시 2~5초 간격을 유지한다. 하루 1,000키워드 = 약 10,000 요청이며, 이는 합리적인 범위다.
- 저용량에서는 공식 API 사용 — Google Custom Search API는 하루 100건 무료 쿼리를 제공한다. 소규모 추적에는 API가 더 안정적이다.
- robots.txt 확인 — 구글의 robots.txt는 검색 결과 페이지 크롤링을 허용하지만, 과도한 자동화는 권장하지 않는다.
대규모 추적이 필요하면 SERP 추적 유스케이스에서 ProxyHat의 권장 설정을 참고하고, 웹 스크래핑 가이드에서 일반적인 스크래핑 모범 사례를 확인할 수 있다. 자세한 프록시 설정은 ProxyHat 문서를 참조한다.
핵심 요약
파이썬으로 구글 랭크 트래커를 구축하려면:
curl_cffi의impersonate="chrome"으로 TLS 지문을 모방한다 — 표준requests는 즉시 차단된다.- ProxyHat 레지덴셜 프록시(
gate.proxyhat.com:8080)로 도시 수준 지역 타겟팅과 키워드별 고정 세션을 사용한다.num=100이 폐지되었으므로start=0,10,20...로 페이지네이션한다.- 일일 스냅샷을 SQLite에 저장하고 7일 이동 평균으로 순위 변동성을 스무딩한다.
- 지수 백오프 재시도, CAPTCHA 감지, 동시성 제어(
max_workers=5)로 프로덕션 안정성을 확보한다.- 본인 소유 도메인과 공개 순위만 추적하고, 저용량에서는 Google Custom Search API를 우선한다.


