Создать трекер позиций Google на Python с резидентными прокси: руководство 2026

Полное руководство для разработчиков: модель данных, пагинация SERP после удаления num=100, обход TLS-фингерпринтинга JA3/JA4, curl_cffi + ProxyHat, хранение в SQLite и продакшн-надёжность.

Build a Google Rank Tracker in Python with Residential Proxies
В этой статье

Зачем создавать трекер позиций Google на Python с резидентными прокси

Если вы SEO-инженер или Python-разработчик, вам нужен надёжный способ отслеживать позиции сайта в выдаче Google. Готовые SaaS-решения стоят от $50 до $500 в месяц и часто ограничивают частоту и географию проверок. Решение — создать трекер позиций Google на Python с резидентными прокси: полный контроль над данными, частотой запросов и локацией без зависимости от сторонних сервисов.

Проблема в том, что Google активно блокирует автоматизированные запросы. Дата-центр прокси отклоняются ещё на этапе TLS-рукопожатия из-за отпечатков JA3/JA4 и низкого IP-рейтинга. Решение — резидентные прокси с геотаргетингом по городам и TLS-имперсонацией браузера через curl_cffi.

В этом руководстве вы построите продакшн-готовый трекер ранжирования на Python: от модели данных до конкурентной обработки с ретраями и CAPTCHA-детекцией.

Модель данных: почему ежедневные срезы SERP лучше разовых проверок

Разовая проверка позиций даёт снимок на момент запроса. Google обновляет индекс непрерывно — позиции колеблются в течение дня. Ежедневные срезы SERP позволяют отследить тренды, выявить аномальные скачки и сгладить волатильность.

Модель данных трекера позиций:

keyword        -- отслеживаемый запрос, напр. "купить прокси"
target_domain  -- домен, позиции которого отслеживаем, напр. "proxyhat.com"
country         -- код страны геотаргетинга, напр. "US"
device          -- "desktop" или "mobile"
position        -- позиция в органической выдаче (1-100)
url            -- URL найденной страницы
captured_at    -- временная метка среза SERP

Эта модель хранится в SQLite — достаточно для 10 000+ ключевых слов при ежедневной проверке. Для больших объёмов переходите на PostgreSQL с партицированием по дате.

Почему именно ежедневные срезы? Google обновляет выдачу несколько раз в сутки. Разовая проверка может попасть на момент кратковременного скачка или провала. Серия срезов за 7-14 дней позволяет вычислить медианную позицию и отфильтровать шум.

Пагинация SERP после удаления num=100 в сентябре 2025

До сентября 2025 года можно было передать параметр num=100 и получить до 100 результатов на одной странице. Google удалил этот параметр, и теперь выдача жёстко ограничена 10 результатами на страницу. Для сбора топ-100 нужно делать 10 запросов с параметром start=0,10,20,...,90.

Это увеличивает нагрузку в 10 раз — вместо одного запроса на ключевое слово вы делаете 10. Без прокси это означает мгновенный бан IP. С резидентными прокси и ротацией IP между запросами вы распределяете нагрузку.

import urllib.parse

def build_serp_urls(keyword: str, max_results: int = 100) -> list[str]:
    base = "https://www.google.com/search"
    urls = []
    for start in range(0, max_results, 10):
        params = urllib.parse.urlencode({
            "q": keyword,
            "start": start,
            "num": 10,
            "hl": "en",
            "gl": "US",
        })
        urls.append(f"{base}?{params}")
    return urls

# Пример: 10 URL для топ-100
urls = build_serp_urls("buy proxies")
print(f"Сгенерировано {len(urls)} URL для пагинации")

Каждый URL — отдельный запрос через прокси. Важно: между запросами для одного ключевого слова используйте разные IP-адреса, чтобы Google не связал их в одну сессию.

Почему нужны резидентные прокси с геотаргетингом по городам

Google использует несколько уровней защиты от ботов:

  • TLS-фингерпринтинг (JA3/JA4) — анализ параметров TLS-рукопожатия. Стандартные HTTP-клиенты Python (requests, httpx) имеют отпечаток, отличный от браузера. Google отклоняет такие запросы.
  • IP-репутация — дата-центр IP-адреса помечены как бот-сети. Резидентные IP от интернет-провайдеров имеют высокий уровень доверия.
  • Геотаргетинг — результаты Google зависят от местоположения пользователя. Для точного трекинга нужен IP из целевого города.

Сравнение типов прокси для трекинга позиций:

ПараметрДата-центрРезидентныеМобильные
IP-репутацияНизкаяВысокаяВысокая
Скорость~50 мс~200 мс~500 мс
Блокировка GoogleЧасто (>50%)Редко (<5%)Очень редко
ГеотаргетингСтранаГородГород / ASN
ЦенаНизкаяСредняяВысокая

Для трекинга позиций оптимальны резидентные прокси с геотаргетингом по городам. Они обеспечивают баланс скорости, надёжности и точности локации. Подробности о доступных локациях — на странице покрытия ProxyHat.

Практическая реализация: curl_cffi + ProxyHat

Теперь соберём всё вместе. Мы используем curl_cffi с параметром impersonate='chrome' для имитации TLS-отпечатка Chrome и ProxyHat для ротации резидентных IP с геотаргетингом.

Базовый запрос через curl

# curl с ProxyHat резидентным прокси, геотаргетинг США/Чикаго
curl -x "http://user-country-US-city-chicago-session-key001:pass@gate.proxyhat.com:8080" \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
  "https://www.google.com/search?q=buy+proxies&num=10&hl=en&gl=US"

Класс ProxyHatClient — обёртка над прокси-конфигурацией

from dataclasses import dataclass
from urllib.parse import quote

@dataclass
class ProxyHatClient:
    username: str = "user"
    password: str = "pass"
    host: str = "gate.proxyhat.com"
    port: int = 8080

    def proxy_url(self, country: str = None, city: str = None,
                  session: str = None) -> str:
        """Создаёт URL прокси с геотаргетингом и липкой сессией."""
        parts = [self.username]
        if country:
            parts.append(f"country-{country}")
        if city:
            parts.append(f"city-{city}")
        if session:
            parts.append(f"session-{session}")
        user = "-".join(parts)
        return f"http://{user}:{self.password}@{self.host}:{self.port}"

    def proxies(self, country: str = None, city: str = None,
                session: str = None) -> dict:
        """Возвращает dict прокси для requests/curl_cffi."""
        url = self.proxy_url(country, city, session)
        return {"http": url, "https": url}

Получение SERP через curl_cffi

from curl_cffi import requests as cffi_requests
from curl_cffi.requests import AsyncSession
import asyncio
import re

async def fetch_serp_page(url: str, proxy_client: ProxyHatClient,
                          country: str, city: str,
                          session_id: str) -> str:
    """Получает HTML страницы SERP через резидентный прокси."""
    proxy_url = proxy_client.proxy_url(country, city, session_id)
    try:
        resp = await cffi_requests.get(
            url,
            proxy=proxy_url,
            impersonate="chrome",
            timeout=30,
            allow_redirects=True,
        )
        if resp.status_code == 200:
            return resp.text
        elif resp.status_code == 429:
            raise Exception("Rate limited (429)")
        else:
            raise Exception(f"HTTP {resp.status_code}")
    except Exception as e:
        raise Exception(f"SERP fetch failed: {e}")

# Пример запуска
async def main():
    client = ProxyHatClient(username="user", password="pass")
    url = "https://www.google.com/search?q=buy+proxies&num=10&hl=en&gl=US"
    html = await fetch_serp_page(url, client, "US", "chicago", "kw-001")
    print(f"Получено {len(html)} байт HTML")

asyncio.run(main())

Парсинг органических результатов

import re
from dataclasses import dataclass

@dataclass
class OrganicResult:
    position: int
    title: str
    url: str
    domain: str

def parse_organic_results(html: str, start_rank: int = 1) -> list[OrganicResult]:
    """Парсит органические результаты из HTML SERP.
    Пропускает рекламу, featured snippets и другие SERP-фичи."""
    results = []
    # Ищем блоки органических результатов
    # Google использует div с классом, содержащим 'g' для органики
    pattern = re.compile(
        r'<a href="/url\?q=(https?://[^&]+)"[^>]*>(.*?)</a>',
        re.DOTALL
    )
    matches = pattern.findall(html)
    seen_domains = set()
    rank = start_rank
    for url, title_html in matches:
        # Пропускаем Google-сервисы и рекламу
        if "google.com" in url or "googleads" in url:
            continue
        # Извлекаем домен
        domain_match = re.match(r'https?://([^/]+)', url)
        if not domain_match:
            continue
        domain = domain_match.group(1)
        if domain in seen_domains:
            continue
        seen_domains.add(domain)
        # Очищаем заголовок от HTML-тегов
        title = re.sub(r'<[^>]+>', '', title_html).strip()
        if not title:
            continue
        results.append(OrganicResult(
            position=rank,
            title=title,
            url=url,
            domain=domain
        ))
        rank += 1
    return results

def find_domain_position(results: list[OrganicResult],
                          target_domain: str) -> int | None:
    """Находит позицию целевого домена в результатах."""
    for r in results:
        if target_domain in r.domain:
            return r.position
    return None

Хранение истории в SQLite

import sqlite3
from datetime import datetime, timezone

def init_db(db_path: str = "rank_tracker.db") -> sqlite3.Connection:
    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,
            url TEXT,
            captured_at TEXT NOT NULL
        )
    """)
    conn.execute("""
        CREATE INDEX IF NOT EXISTS idx_kw_domain
        ON rankings(keyword, target_domain, country, device)
    """)
    conn.commit()
    return conn

def save_ranking(conn: sqlite3.Connection, keyword: str,
                 target_domain: str, country: str,
                 device: str, position: int | None,
                 url: str | None) -> None:
    conn.execute(
        """INSERT INTO rankings
           (keyword, target_domain, country, device, position, url, captured_at)
           VALUES (?, ?, ?, ?, ?, ?, ?)""",
        (keyword, target_domain, country, device, position, url,
         datetime.now(timezone.utc).isoformat())
    )
    conn.commit()

# Экспорт в CSV для отчётов
import csv

def export_csv(conn: sqlite3.Connection, output_path: str = "rankings.csv") -> None:
    rows = conn.execute("SELECT * FROM rankings ORDER BY captured_at DESC").fetchall()
    with open(output_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["id", "keyword", "domain", "country",
                        "device", "position", "url", "captured_at"])
        writer.writerows(rows)

Продакшн-надёжность: ретраи, CAPTCHA, конкурентность

Трекер позиций работает в условиях нестабильности: прокси могут отключаться, Google может показывать CAPTCHA, страницы могут загружаться медленно. Вот что нужно для продакшна.

Ретраи с экспоненциальной задержкой

import asyncio
import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rank_tracker")

def retry_with_backoff(max_retries: int = 3, base_delay: float = 1.0):
    """Декоратор для ретраев с экспоненциальной задержкой."""
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return await func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        logger.error(f"Финальная ошибка после {max_retries} попыток: {e}")
                        return None
                    delay = base_delay * (2 ** attempt)
                    logger.warning(
                        f"Попытка {attempt + 1}/{max_retries} провалена: {e}. "
                        f"Повтор через {delay:.1f}с"
                    )
                    await asyncio.sleep(delay)
            return None
        return wrapper
    return decorator

def detect_captcha(html: str) -> bool:
    """Детектирует CAPTCHA и блокировки Google."""
    captcha_markers = [
        "detected unusual traffic",
        "captcha",
        "g-recaptcha",
        "sorry/index",
        "Our systems have detected",
    ]
    html_lower = html.lower()
    return any(marker.lower() in html_lower for marker in captcha_markers)

Полный трекер с конкурентностью и сглаживанием волатильности

import asyncio
from datetime import datetime, timezone
from curl_cffi.requests import AsyncSession
import sqlite3

async def track_keyword(
    keyword: str,
    target_domain: str,
    country: str,
    city: str,
    proxy_client: ProxyHatClient,
    conn: sqlite3.Connection,
    semaphore: asyncio.Semaphore,
) -> int | None:
    """Отслеживает позицию домена для одного ключевого слова."""
    async with semaphore:
        session_id = f"{keyword[:20]}-{datetime.now().strftime('%Y%m%d')}"
        all_results = []

        for page_start in range(0, 100, 10):
            url = (
                f"https://www.google.com/search?q={keyword}"
                f"&start={page_start}&num=10&hl=en&gl={country}"
            )
            # Разный session_id для каждой страницы = разный IP
            page_session = f"{session_id}-p{page_start//10}"

            @retry_with_backoff(max_retries=3, base_delay=2.0)
            async def fetch_page(u=url, s=page_session):
                html = await fetch_serp_page(
                    u, proxy_client, country, city, s
                )
                if detect_captcha(html):
                    raise Exception("CAPTCHA detected")
                return html

            html = await fetch_page()
            if html is None:
                logger.warning(f"Страница start={page_start} не получена для '{keyword}'")
                continue

            page_results = parse_organic_results(html, start_rank=page_start + 1)
            all_results.extend(page_results)
            await asyncio.sleep(2)  # Задержка между страницами

        position = find_domain_position(all_results, target_domain)
        save_ranking(conn, keyword, target_domain, country,
                     "desktop", position, None)
        logger.info(f"'{keyword}' -> {target_domain}: позиция {position}")
        return position

async def run_tracker(keywords: list[str], target_domain: str,
                      country: str = "US", city: str = "chicago",
                      max_concurrent: int = 10):
    """Запускает трекер для списка ключевых слов."""
    client = ProxyHatClient(username="user", password="pass")
    conn = init_db()
    semaphore = asyncio.Semaphore(max_concurrent)

    tasks = [
        track_keyword(kw, target_domain, country, city,
                      client, conn, semaphore)
        for kw in keywords
    ]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    success = sum(1 for r in results if r is not None and not isinstance(r, Exception))
    logger.info(f"Успешно: {success}/{len(keywords)} ключевых слов")
    export_csv(conn)
    conn.close()

# Запуск
keywords = ["buy proxies", "residential proxies", "best proxy service"]
asyncio.run(run_tracker(keywords, "proxyhat.com", "US", "chicago", 5))

Сглаживание волатильности: вместо отображения одной позиции вычисляйте медиану за последние 7 дней. Это фильтрует суточные скачки и даёт стабильную картину тренда.

def get_smoothed_position(conn: sqlite3.Connection,
                          keyword: str, domain: str,
                          days: int = 7) -> float | None:
    """Возвращает медианную позицию за последние N дней."""
    rows = conn.execute(
        """SELECT position FROM rankings
           WHERE keyword=? AND target_domain=?
           AND captured_at >= datetime('now', ?)
           AND position IS NOT NULL
           ORDER BY captured_at DESC""",
        (keyword, domain, f'-{days} days')
    ).fetchall()
    if not rows:
        return None
    positions = sorted([r[0] for r in rows])
    mid = len(positions) // 2
    if len(positions) % 2 == 0:
        return (positions[mid - 1] + positions[mid]) / 2
    return float(positions[mid])

Конкурентность: используйте asyncio.Semaphore для ограничения одновременных запросов. Рекомендуемый лимит — 10-20 параллельных запросов на один прокси-пул, чтобы не превышать 200 запросов/мин.

Этика и ограничения трекинга позиций

Трекинг позиций — это серая зона. Google's Terms of Service запрещают автоматизированные запросы. Однако отслеживание позиций собственного сайта и публично доступных данных широко практикуется в SEO-индустрии.

Рекомендации:

  • Соблюдайте лимиты — не более 200 запросов/мин с одного IP-пула.
  • Отслеживайте свои домены — это снижает этические риски.
  • Используйте официальный API при низком объёме — Google Custom Search API даёт 100 бесплатных запросов/день. Для трекинга 10-20 ключевых слов этого достаточно. Подробнее — в документации ProxyHat.
  • Уважайте robots.txt — проверяйте директивы перед скрапингом.
  • GDPR/CCPA — не собирайте персональные данные из SERP.

Для масштабирования трекинга сверьтесь с тарифами ProxyHat — резидентные прокси начинаются от доступных планов. Подробнее о применении прокси для скрапинга — в разделе веб-скрапинг и SERP-трекинг. Информацию о подключении и параметрах смотрите в официальной документации.

Ключевые выводы

Ежедневные срезы SERP с медианным сглаживанием за 7 дней дают стабильную картину позиций. Разовые проверки — это шум.

  • Модель данных: keyword + target_domain + country + device + position + captured_at в SQLite — достаточно для 10 000+ ключевых слов.
  • Пагинация: после удаления num=100 в сентябре 2025 нужно 10 запросов на ключевое слово через start=0,10,...,90.
  • Прокси: только резидентные с геотаргетингом по городам. Дата-центр IP блокируются Google на этапе TLS.
  • TLS-имперсонация: используйте curl_cffi с impersonate='chrome' — это имитирует отпечаток Chrome и проходит проверки JA3/JA4.
  • Липкие сессии: параметр session- в username ProxyHat закрепляет IP за ключевым словом — важно для консистентности геолокации.
  • Продакшн: ретраи с экспоненциальной задержкой (3 попытки, базовая задержка 2 с), детекция CAPTCHA, лимит конкурентности 10-20 потоков.
  • Этика: отслеживайте свои домены, соблюдайте лимиты, рассмотрите официальный API при малом объёме.

Готовы начать? Настройте геотаргетинг ProxyHat и запустите первый трекер позиций за 15 минут.

Часто задаваемые вопросы

Что такое трекер позиций Google на Python с резидентными прокси?

Это скрипт на Python, который автоматически запрашивает страницы выдачи Google через резидентные прокси-серверы, парсит органические результаты и записывает позицию целевого домена для заданных ключевых слов. Резидентные прокси обеспечивают высокий IP-рейтинг и геотаргетинг, что позволяет получать точные результаты без блокировок. Данные сохраняются в SQLite или CSV для анализа трендов.

Почему для трекинга позиций нужны именно резидентные прокси?

Google блокирует дата-центр IP-адреса на этапе TLS-рукопожатия из-за низкого IP-рейтинга и отпечатков JA3/JA4. Резидентные прокси используют IP-адреса реальных интернет-провайдеров, что обеспечивает высокий уровень доверия Google. Кроме того, резидентные прокси поддерживают геотаргетинг по городам, что критически важно для точного трекинга локальных позиций.

Какой тип прокси лучше для трекера позиций Google?

Оптимальный выбор — резидентные прокси с геотаргетингом по городам. Они обеспечивают баланс скорости (~200 мс), надёжности (менее 5% блокировок) и точности локации. Мобильные прокси ещё надёжнее, но дороже и медленнее (~500 мс). Дата-центр прокси не подходят — Google блокирует их более чем в 50% случаев. Для трекинга своих доменов при малом объёме можно использовать официальный Google Custom Search API.

Как избежать блокировок при создании трекера позиций?

Используйте curl_cffi с impersonate='chrome' для имитации TLS-отпечатка браузера. Ротируйте IP-адреса между запросами через липкие сессии ProxyHat. Ограничьте конкурентность до 10-20 параллельных потоков и не превышайте 200 запросов в минуту. Добавьте детекцию CAPTCHA и ретраи с экспоненциальной задержкой (3 попытки, базовая задержка 2 секунды). Между запросами для одного ключевого слова делайте паузу 2-3 секунды.

Как хранить историю позиций в Python?

Оптимальное решение — SQLite с таблицей rankings, содержащей поля keyword, target_domain, country, device, position, url и captured_at. Создайте индекс по keyword, target_domain, country и device для быстрых запросов. Для отчётов экспортируйте данные в CSV. При объёме более 10 000 ключевых слов рассмотрите переход на PostgreSQL с партицированием по дате. Для сглаживания волатильности вычисляйте медиану за последние 7 дней.

Готовы начать?

Резидентные, ISP и мобильные прокси в 148+ странах. Создайте бесплатный аккаунт.

Создать бесплатный аккаунт
← Вернуться в Блог