Developer TutorialSERP ScrapingMobile Proxy

How to Scrape Google Search Results with Python + Mobile Proxies (2026)

Google has never been harder to scrape. SearchGuard, the death of num=100, and a DMCA lawsuit against SerpAPI have changed the game. This guide walks you through building a working Google SERP scraper with Python, Playwright, and mobile proxy rotation — including the legal landmines to watch for.

18 min read
Intermediate

Why Google Is the Hardest Site to Scrape in 2026

If you last scraped Google in 2023 or earlier, prepare for a different world. Google has systematically closed every loophole that made lightweight scraping possible, and then went on the legal offensive. Here is what changed:

January 2025SearchGuard Deployment

Google deployed SearchGuard, a bytecode virtual machine with 512 registers and an ARX cipher using rotating magic constants. SearchGuard runs client-side JavaScript challenges that must be solved before any search results are rendered. Simple HTTP requests without a browser engine fail immediately.

Search Engine Land: Inside Google SearchGuard
September 2025num=100 Parameter Removed

Google removed the num=100 parameter that allowed fetching 100 results per page. The maximum is now 10. This forces 10x more requests for the same data volume, dramatically increasing detection surface and bandwidth costs.

Early 2025Non-JavaScript Access Killed

Google effectively killed non-JavaScript access to search results. A bare requests.get() call now fails within roughly 10 queries, returning CAPTCHA pages or empty results. A full browser engine (Playwright, Puppeteer, or similar) is now required for any sustained scraping.

December 2025Google Sues SerpAPI Under DMCA

Google filed suit against SerpAPI for “hundreds of millions” of daily queries that allegedly circumvented Google's technological protection measures. This is the first major DMCA action targeting a scraping service directly, with statutory damages of $200–$2,500 per violation. The case sent shockwaves through the scraping industry.

IPWatchdog: Google Sues SerpAPI for Parasitic Scraping
Announced 2025Custom Search API Closing to New Customers

Google's Custom Search JSON API is closing to new customers by January 2027. Existing users face stricter quotas and increased pricing. The “official” path to Google search data is disappearing, making proxy-based scraping the only scalable option.

The Bottom Line

In 2026, scraping Google requires three things: a real browser engine, high-trust IP addresses, and careful rate limiting. Mobile proxies solve the IP problem better than any other option. This guide shows you how.

Success Rates by Proxy Type

Not all proxies are equal when it comes to Google. The success rate gap between datacenter and mobile proxies has widened significantly since SearchGuard launched. Here is how each type performs against Google in 2026:

Datacenter20-40%

Flagged within 5-10 queries. Google maintains blocklists of known datacenter IP ranges.

Residential85-93%

Decent success but IPs get burned quickly. Shared pools degrade over time as other users abuse them.

Mobile 4G/5G92-97%+

Highest success rate. CGNAT means thousands of real users share each IP, making blocks impractical.

Why Mobile Proxies Win: The CGNAT Advantage

Mobile carriers use Carrier-Grade NAT (CGNAT) to share a single public IPv4 address among thousands of mobile devices. When your scraper uses a mobile proxy IP, it looks identical to any of the thousands of real users browsing Google on their phones from that same IP address.

Google cannot block a CGNAT IP without blocking all those legitimate mobile users — which would mean blocking a significant portion of real search traffic. This structural constraint gives mobile proxies an inherent advantage that no amount of bot detection can overcome.

Proxidize: Best Proxies for Web Scraping (success rate benchmarks)

Setup: Python + Playwright + Mobile Proxy

We use Playwright because it runs a real Chromium browser that executes SearchGuard's JavaScript challenges automatically. Combined with a mobile proxy, this setup produces traffic that is indistinguishable from a real user searching on Chrome.

Step 1: Install Dependencies

terminal
bash
# Create virtual environment
python -m venv google-scraper
source google-scraper/bin/activate

# Install Playwright and browser
pip install playwright
playwright install chromium

# Optional: for advanced parsing
pip install parsel

Step 2: Basic Google SERP Scraper

This scraper launches a headless Chromium browser routed through a PROXIES.SX mobile proxy, navigates to Google, extracts organic results including titles, URLs, and snippets.

google_scraper.py
python
import asyncio
from playwright.async_api import async_playwright

async def scrape_google(query: str, proxy_url: str):
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            proxy={"server": proxy_url},
            headless=True,
        )
        context = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                       "AppleWebKit/537.36 (KHTML, like Gecko) "
                       "Chrome/131.0.0.0 Safari/537.36",
        )
        page = await context.new_page()
        await page.goto(
            f"https://www.google.com/search?q={query}",
            wait_until="domcontentloaded",
        )
        await page.wait_for_timeout(2000)

        results = await page.query_selector_all("div.g")
        data = []
        for r in results:
            title_el = await r.query_selector("h3")
            link_el = await r.query_selector("a")
            snippet_el = await r.query_selector("div[data-sncf]")
            if title_el and link_el:
                title = await title_el.inner_text()
                link = await link_el.get_attribute("href")
                snippet = (
                    await snippet_el.inner_text() if snippet_el else ""
                )
                data.append({
                    "title": title,
                    "url": link,
                    "snippet": snippet,
                })

        await browser.close()
        return data

# Usage with proxies.sx mobile proxy
results = asyncio.run(scrape_google(
    "best mobile proxies 2026",
    "http://user_abc123:pass456@138.201.158.43:8522"
))
for r in results:
    print(f"{r['title']}: {r['url']}")

Important: Proxy URL Format

The proxy URL follows the format http://username:password@host:port. You get your credentials from the PROXIES.SX dashboard at client.proxies.sx after purchasing bandwidth and ports.

Proxy Rotation Strategy

Using a single proxy for all queries is a recipe for getting blocked. Distributing requests across multiple mobile proxy ports gives each IP time to “cool down” between queries, mimicking natural mobile browsing patterns.

rotation.py
python
import random
import asyncio

PROXIES = [
    "http://user1:pass1@138.201.158.43:8522",
    "http://user2:pass2@138.201.158.43:8523",
    "http://user3:pass3@62.30.207.124:8002",
]

async def scrape_with_rotation(queries: list[str]):
    results = {}
    for query in queries:
        proxy = random.choice(PROXIES)
        try:
            data = await scrape_google(query, proxy)
            results[query] = data
        except Exception as e:
            print(f"Failed {query} with {proxy}: {e}")
        await asyncio.sleep(random.uniform(3, 8))
    return results

# Example: scrape multiple keywords
keywords = [
    "best mobile proxies 2026",
    "web scraping tools python",
    "serp api alternative",
    "google search scraper",
]

all_results = asyncio.run(scrape_with_rotation(keywords))
for keyword, serps in all_results.items():
    print(f"\n--- {keyword} ---")
    for r in serps:
        print(f"  {r['title']}: {r['url']}")

For production-grade scraping, consider these enhancements to the rotation strategy:

Weighted rotation

Track success rates per proxy and weight selection toward higher-performing IPs.

Cooldown tracking

Track the last-used time for each proxy and enforce a minimum cooldown period (e.g. 30 seconds).

Failure circuit breaker

After 3 consecutive failures on a proxy, remove it from the pool for 5 minutes.

Session persistence

For paginated results, keep the same proxy for all pages of a single query to avoid mid-session IP changes.

Rate Limits & Best Practices

Even with mobile proxies, hammering Google will get you blocked. The key is to behave like a human user. Here are the tested limits and practices that maintain high success rates:

1-5 queries per minute per IP

This is the safe operating range. Going above 5 QPM on a single IP will trigger CAPTCHAs within minutes. With 3 proxy ports, you can sustain 9-15 QPM across the pool.

Random delays of 3-8 seconds

Fixed intervals are a bot signature. Use random.uniform(3, 8) between requests. For higher-risk queries (e.g., competitive keywords), extend to 5-12 seconds.

Rotate User-Agent strings

Maintain a pool of 5-10 recent Chrome User-Agents. Match the UA to realistic viewport sizes and platform headers. A Chrome 131 UA with a 640x480 viewport is a red flag.

Use different proxy IPs per query

Never send two consecutive queries from the same IP. Each query should use a different proxy port so Google sees each search as coming from a separate mobile user.

Handle 429 with exponential backoff

If you receive a 429 (Too Many Requests) or a CAPTCHA page, back off exponentially: wait 30s, then 60s, then 120s. Switch to a different proxy after the first retry.

backoff.py
python
import asyncio
import random

async def scrape_with_backoff(query: str, proxies: list[str], max_retries: int = 3):
    """Scrape Google with exponential backoff on failure."""
    for attempt in range(max_retries):
        proxy = proxies[attempt % len(proxies)]
        try:
            data = await scrape_google(query, proxy)
            if data:  # Got results
                return data
            # Empty results likely means CAPTCHA page
            raise Exception("Empty results — likely CAPTCHA")
        except Exception as e:
            wait = (2 ** attempt) * 15 + random.uniform(0, 10)
            print(f"Attempt {attempt+1} failed: {e}")
            print(f"Waiting {wait:.1f}s before retry...")
            await asyncio.sleep(wait)

    print(f"All {max_retries} attempts failed for: {query}")
    return None

Frequently Asked Questions

Ready to Scrape Google at Scale?

Get started with PROXIES.SX mobile proxies. Real 4G/5G carrier IPs across 17+ countries, 92-97% success rate on Google, and setup in under 60 seconds. Start with our free trial: 1GB bandwidth + 2 ports.

Related Articles