How to Scrape Google Search Results in 2026 (Python + Proxy Guide)

Python code on screen scraping Google search results with proxy rotation in 2026
TL;DR

Scraping Google search results in 2026 is harder than it was two years ago. Google began requiring full JavaScript execution for most search access in early 2025. Simple requests.get() calls no longer return real results. Here is what the working approaches actually are.

  • For small projects and quick keyword lookups: the googlesearch-python library wraps Google scraping with minimal setup. Not suitable for high volume, but useful for one-off scripts.
  • For medium-scale scraping: Playwright with a rotating residential proxy. Playwright renders the full page including JavaScript, which Google now requires. The proxy provides fresh IPs per request.
  • For geo-targeted results (which is almost every real use case in 2026): you need both the URL parameters gl, hl, and a matching country proxy IP. Parameters alone are not sufficient.
  • Google's CSS selectors change regularly. Always build fallback selector strategies. Hardcoding one selector will break, usually on a weekend.
  • When you should skip DIY scraping entirely: if your use case is commercial keyword rank tracking, use a SERP API. The maintenance cost of keeping a DIY scraper working against Google's detection is significant at scale.
  • TorchProxies residential proxies integrate with Playwright in two lines of configuration. 120M+ IPs across 195+ countries covers every geo-targeting use case.

How to scrape Google search results is a question I get asked more than almost any other scraping topic. The answer changed significantly in 2025 when Google shifted to requiring JavaScript execution for real search access, which killed most of the simple HTTP-based scraping approaches that worked before. The good news is that working methods still exist. The three that are actually reliable in 2026 are the googlesearch-python library for small scale, Playwright with rotating residential proxies for medium scale, and SERP APIs for large commercial deployments. This guide covers all three with working code, explains the URL parameter system for geo-targeting, and is honest about when you should not build a DIY scraper at all. Let's get into it.

💻
From the Field
I built a rank tracking scraper in late 2024 that worked well through Q4. By March 2025 it was returning garbage. Not blocked, not CAPTCHAs, just... the wrong HTML. Google was serving a JavaScript shell to my requests client and the actual results were loading asynchronously. I did not realize this for two weeks because the scraper was technically returning 200 status codes and valid HTML. The HTML just did not contain any search results. Switching to Playwright fixed it immediately. The lesson was: always verify that the HTML you are receiving actually contains results, not just that the request succeeded. A 200 response from Google's search endpoint tells you nothing about whether you got real data.

Why Google Scraping Got Harder in 2025

Understanding what changed matters because it tells you which approaches are wasting your time. In early 2025, Google shifted to requiring full JavaScript execution for standard search access, moving away from the pattern where a plain HTTP GET request returned a usable HTML response with embedded search results.

What this means practically: if you send a requests.get('https://www.google.com/search?q=test') today, you will receive an HTML response. But that response is a page shell with JavaScript includes, not a page with search results in it. The actual results load after JavaScript runs. Your scraper is getting a technically valid HTTP 200 response that contains no useful data. This is what makes it confusing, you do not get an error, you just get the wrong thing.

2025
Year Google shifted to JS-required SERP delivery
3
Reliable DIY scraping approaches that still work in 2026
4+
Detection signals Google evaluates per request
gl / hl
URL parameters required for accurate geo-targeted results

Google's detection system in 2026 evaluates four main signals: IP address reputation and origin, JavaScript execution capability (can the client actually run JS challenges?), TLS fingerprinting (does the TLS handshake look like a real browser or a Python library?), and behavioral signals (request timing, interaction patterns). Solving only some of these is not enough for reliable access. The thing that catches most scrapers is TLS fingerprinting: even with a real browser User-Agent header, the TLS handshake from a standard Python requests session is identifiably different from a Chromium-based browser.

🔎
SERP: Search Engine Results Page
The page Google returns after a search query. In 2026, Google SERPs contain organic results, AI-generated overviews (SGE), People Also Ask sections, featured snippets, knowledge panels, local results, shopping results, and more. For scraping, "SERP data" typically refers to organic results: title, URL, and description for each ranked page. The SERP HTML structure changes regularly as Google runs A/B tests on its layout.

The practical consequence: the three approaches below are structured around which detection signals each one addresses. googlesearch-python handles the basics well for low volume. Playwright handles the JS execution and TLS problem because it runs a real browser. Residential proxies handle the IP reputation problem for all three approaches.


Google's Search URL Parameters: What You Need to Know

Before writing any scraper code, understand the URL parameters. These control what results you get, how many, where they come from, and in what language. This is worth getting right before you scale.

Parameter What It Controls Example Notes
q The search query q=best+proxy+provider URL-encode spaces as + or %20
gl Geolocation: country whose results to prioritize gl=us, gl=de, gl=jp ISO 3166-1 alpha-2 country codes. Critical for geo-targeted scraping
hl Interface language hl=en, hl=de, hl=ko BCP 47 language codes. Affects UI language and some result ordering
cr Country restrict: limits results to pages from a country cr=countryUS, cr=countryDE More restrictive than gl; use when you need results exclusively from that country
start Pagination offset (zero-indexed) start=10 (page 2), start=20 (page 3) Default is 0. Google returns 10 results per page
tbs Time-based filters tbs=qdr:d (past day), tbs=qdr:w (past week) Useful for news monitoring and freshness-restricted queries
safe SafeSearch setting safe=active, safe=off Affects content filtering for certain queries
lr Language restrict: limits results to pages in a language lr=lang_en, lr=lang_ja Combine with hl and gl for precise targeting

The most important thing about geo-targeting: URL parameters alone are not sufficient. gl=de tells Google you want German results, but Google's system also weights results based on the IP address making the request. A request with gl=de coming from a US IP will produce different results than the same request from a German IP. For accurate country-specific SERP data, you need both the parameter and a proxy IP from that country. This is what actually means in practice for the target markets: US, UK, Germany, Indonesia, Korea, Japan, India, Netherlands, Hong Kong each need a matching proxy IP for accurate data.

Google Search URL Construction for Geo-Targeting
from urllib.parse import urlencode

def build_google_url(query, country='us', lang='en', page=1):
    params = {
        'q': query,
        'gl': country,      # Country for results
        'hl': lang,         # Interface language
        'start': (page - 1) * 10,  # Pagination
        'ie': 'UTF-8',
        'oe': 'UTF-8',
    }
    return f'https://www.google.com/search?{urlencode(params)}'

# Examples for target markets
us_url   = build_google_url('proxy provider', country='us', lang='en')
de_url   = build_google_url('proxy anbieter', country='de', lang='de')
jp_url   = build_google_url('プロキシ',       country='jp', lang='ja')
kr_url   = build_google_url('프록시 서비스',  country='kr', lang='ko')
in_url   = build_google_url('proxy service', country='in', lang='en')

Approach 1: googlesearch-python Library (Small Projects)

For scripts that need Google results occasionally and do not need to run at high volume, the googlesearch-python library is the simplest option. It wraps the scraping logic and returns URLs as a Python generator. Install it with pip install googlesearch-python.

googlesearch-python: Basic Usage with Proxy
from googlesearch import search

# Basic usage - returns top 10 results for a query
for url in search("best residential proxies 2026", num_results=10):
    print(url)

# With a rotating proxy (recommended even at small scale)
proxy = 'http://username:[email protected]:31112'

for url in search(
    "web scraping tools",
    num_results=20,
    lang="en",
    proxy=proxy,
    ssl_verify=False  # Required when proxy handles SSL
):
    print(url)

# With language and region targeting
for url in search(
    "web scraping",
    num_results=10,
    lang="de",     # German results
    region="de",   # From Germany
    proxy=proxy,
    ssl_verify=False
):
    print(url)

The limitation is this: googlesearch-python returns only URLs, not titles or descriptions. If you need the full organic result data (rank position, title, meta description, domain), you need Approach 2 or 3. Use this library when you only need the URLs themselves, for example for a quick SEO backlink research script or a simple competitor URL discovery tool.

Rate Limiting with googlesearch-python
The library has a built-in sleep_interval parameter. Set it to at least 2 to 3 seconds between requests. Without delays, Google will return CAPTCHAs or block the IP after a small number of queries. Even at low volume, this library requires a proxy to avoid rate limits on any sustained use. Source: PyPI googlesearch-python documentation.

Approach 2: Playwright + Rotating Residential Proxies (Recommended)

This is the approach that actually holds up in 2026. Playwright launches a real Chromium browser, which handles JavaScript execution, TLS fingerprinting, and the various browser challenge checks that Google uses. Pair it with rotating residential proxies for IP-level protection and you have a scraper that addresses all four of Google's detection signals.

Install the dependencies:

Installation
pip install playwright beautifulsoup4
playwright install chromium

Complete Playwright SERP Scraper

Playwright Google SERP Scraper with TorchProxies Rotating Residential
import asyncio
import json
import time
from urllib.parse import urlencode
from playwright.async_api import async_playwright
from bs4 import BeautifulSoup


# TorchProxies rotating residential credentials
PROXY_HOST = 'residential.torchproxies.com'
PROXY_PORT = '31112'
PROXY_USER = 'your_username'
PROXY_PASS = 'your_password'


def build_google_url(query, country='us', lang='en', page=1):
    params = {
        'q': query,
        'gl': country,
        'hl': lang,
        'start': (page - 1) * 10,
        'ie': 'UTF-8',
    }
    return f'https://www.google.com/search?{urlencode(params)}'


def parse_serp_html(html):
    """Parse Google SERP HTML with fallback selector strategies."""
    soup = BeautifulSoup(html, 'html.parser')
    results = []

    # Fallback selector strategies - Google changes HTML regularly
    selector_strategies = [
        {'container': '.tF2Cxc', 'title': 'h3.LC20lb', 'url': '.yuRUbf a', 'desc': '.VwiC3b'},
        {'container': '.g',       'title': 'h3',          'url': '.yuRUbf a', 'desc': '.VwiC3b'},
        {'container': '.MjjYud',  'title': 'h3',          'url': 'a',          'desc': "[data-sncf='1']"},
    ]

    for strategy in selector_strategies:
        containers = soup.select(strategy['container'])
        if len(containers) >= 3:  # Need at least 3 results to consider it valid
            for i, c in enumerate(containers, 1):
                title_el = c.select_one(strategy['title'])
                url_el   = c.select_one(strategy['url'])
                desc_el  = c.select_one(strategy['desc'])
                if title_el and url_el:
                    href = url_el.get('href', '')
                    if href.startswith('http'):  # Skip Google internal links
                        results.append({
                            'position': i,
                            'title': title_el.text.strip(),
                            'url': href,
                            'description': desc_el.text.strip() if desc_el else '',
                        })
            break  # Stop after first strategy that returns results

    return results


async def scrape_serp(query, country='us', lang='en', pages=1):
    all_results = []

    async with async_playwright() as pw:
        browser = await pw.chromium.launch(
            headless=True,
            args=[
                '--no-sandbox',
                '--disable-dev-shm-usage',
                '--disable-blink-features=AutomationControlled',
            ]
        )

        # One context per request - rotating proxy assigns fresh IP each time
        for page_num in range(1, pages + 1):
            context = await browser.new_context(
                proxy={
                    'server': f'http://{PROXY_HOST}:{PROXY_PORT}',
                    'username': PROXY_USER,
                    'password': PROXY_PASS,
                },
                # Match locale to target country
                locale=f'{lang}-{country.upper()}',
                timezone_id='America/New_York' if country == 'us' else 'Europe/Berlin',
            )

            # Remove webdriver flag
            await context.add_init_script(
                "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
            )

            page = await context.new_page()
            url = build_google_url(query, country, lang, page_num)

            await page.goto(url, wait_until='networkidle', timeout=30000)
            await page.wait_for_timeout(2000)  # Let JS finish rendering

            html = await page.content()
            page_results = parse_serp_html(html)
            all_results.extend(page_results)

            await context.close()

            if page_num < pages:
                await asyncio.sleep(3)  # Delay between pages

        await browser.close()

    return all_results


# Run it
if __name__ == '__main__':
    results = asyncio.run(
        scrape_serp("residential proxy provider", country='us', lang='en', pages=2)
    )
    print(json.dumps(results, indent=2))
    print(f'\nScraped {len(results)} results')

A few things worth noting in this code. Creating a new browser context per request rather than reusing one context across all pages ensures that each request gets a fresh proxy IP assignment when using rotating proxies. The wait_for_timeout(2000) after page load gives JavaScript time to finish rendering the search results before we grab the HTML. Skipping this step is a common mistake and returns incomplete results.

The navigator.webdriver removal is important. Playwright sets this property to true by default, which is a reliable bot detection signal. Clearing it removes one detection vector. I have not personally tested every Google market with this specific configuration, so treat it as a starting point and adjust based on what you observe in production.


Geo-Targeting Google Results by Country in 2026

This is the section most guides skip. For rank tracking across multiple markets, the geo-targeting setup is the most operationally important part. Getting it wrong means your data is inaccurate regardless of how well your scraper handles detection.

Target Market gl parameter hl parameter Proxy Country Notes
United States gl=us hl=en US residential IP City-level targeting available for local SEO
United Kingdom gl=uk hl=en UK residential IP Use google.co.uk domain for most accurate results
Germany gl=de hl=de DE residential IP google.de domain for German SERP
Japan gl=jp hl=ja JP residential IP Google holds ~76% search market share in Japan as of 2025
South Korea gl=kr hl=ko KR residential IP Note: Naver has ~60% Korean search market share; Google is secondary
India gl=in hl=en IN residential IP Google holds ~97% search share in India
Indonesia gl=id hl=id ID residential IP Google dominant; google.co.id for local domain results
Netherlands gl=nl hl=nl NL residential IP Dutch Google results differ from German results significantly
Hong Kong gl=hk hl=zh-HK HK residential IP Traditional Chinese; different from mainland China results
Canada gl=ca hl=en CA residential IP French Canadian: hl=fr for Quebec targeting

TorchProxies Plan X provides 120M+ IPs across 195+ countries with city-level targeting available. For rank tracking across all ten of the above markets from a single proxy subscription, this is the practical solution. The alternative is managing ten separate proxy provider accounts for each country, which adds significant operational overhead.

Matching Timezone to Proxy Country
In the Playwright example above, the timezone_id in the browser context should match the proxy country. Google (and anti-bot systems generally) check for inconsistencies between the browser's reported timezone and the IP's geographic location. A Tokyo IP with a New York timezone is a detectable inconsistency. For production scrapers, build a mapping from country code to canonical timezone: US → America/New_York, DE → Europe/Berlin, JP → Asia/Tokyo, KR → Asia/Seoul, IN → Asia/Kolkata, ID → Asia/Jakarta, NL → Europe/Amsterdam, HK → Asia/Hong_Kong.
Plan X Hybrid Proxy
120M+ IPs. 195+ countries. City-level targeting.
One plan covers all ten target markets above. No rate limits, rotating sessions, free trial with no credit card required. From $5/GB.
Explore Plan X

Handling CAPTCHAs and Blocks

Google uses two main blocking mechanisms: soft blocks (CAPTCHA challenges that still return HTTP 200 but with a verification form) and hard blocks (HTTP 429 or 503 responses). Handling them correctly makes the difference between a scraper that recovers gracefully and one that silently returns garbage data for hours.

Detecting a CAPTCHA in Your Scraper

CAPTCHA Detection and Graceful Handling
def is_captcha_page(html):
    """Check if Google returned a CAPTCHA page instead of results."""
    captcha_signals = [
        'Our systems have detected unusual traffic',
        'g-recaptcha',
        'recaptcha/api.js',
        'Sorry, we could not process your request',
    ]
    return any(signal in html for signal in captcha_signals)


def is_empty_serp(results):
    """Check if parsing returned no results (possible silent block)."""
    return len(results) == 0


# In your scraping loop:
html = await page.content()

if is_captcha_page(html):
    print("CAPTCHA detected. Rotating proxy and retrying after delay.")
    await context.close()
    await asyncio.sleep(10)
    # Retry with a new context (new proxy IP assigned automatically)
    continue

results = parse_serp_html(html)

if is_empty_serp(results):
    print("Empty results. Possible silent block or selector change.")
    # Log the HTML for inspection, do not silently continue

The empty results check is the more important of the two. CAPTCHAs are obvious. Silent blocks are invisible and dangerous for data quality. A scraper that returns zero results but continues running will produce a dataset full of missing entries that are hard to spot after the fact. Always validate that the parse returned a reasonable number of results before moving on.

Reducing CAPTCHA Frequency

  • Use residential proxies, not datacenter proxies. Google's detection distinguishes between residential and datacenter IPs. Datacenter IPs trigger CAPTCHA significantly faster. The same query from a residential IP and a datacenter IP will hit CAPTCHA at completely different request rates.
  • Add random delays between requests. Use random.uniform(2, 5) seconds between page requests rather than a fixed interval. Fixed intervals are more detectable as automated behavior.
  • Rotate browser contexts between queries. A new browser context means a new browser fingerprint state, which resets session-level tracking. Creating a fresh context per query costs a few hundred milliseconds but significantly reduces fingerprint accumulation signals.
  • Vary the User-Agent per context. Maintain a small pool of realistic Chrome user agent strings and randomly select one per browser context.

Fallback Selector Strategies for Google's Changing HTML

Google regularly changes its SERP HTML structure through A/B tests and platform updates. A scraper with a single hardcoded selector will break without warning. The fallback strategy in the code above is the right pattern, but it is worth explaining why each selector exists in the pool.

CSS Selector What It Targets Stability Notes
.tF2Cxc Individual organic result container (2024-2026 standard) High Most reliable container class in current SERP layout
.g Classic result container (legacy, still active) Medium Google's long-standing selector; still works in most layouts
h3.LC20lb Result title High Title class has been stable since 2022
.yuRUbf a Result URL link High Consistent URL container across most SERP variants
.VwiC3b Result description/snippet Medium Changes more frequently than title or URL selectors
[data-sncf='1'] Alternative description container (2025+ variant) Medium Data attribute approach; more stable than class-based if Google changes class names
.MjjYud Organic results section wrapper Medium Use as parent container if tF2Cxc fails

The validation check in the parser (if len(containers) >= 3) is deliberate. If a selector matches fewer than 3 containers, it is either the wrong selector or a non-results page. Falling through to the next strategy in the list handles Google's A/B test variants automatically without manual intervention.

Monitor Your Selectors Weekly
At high volume, add a monitoring step that logs which selector strategy successfully parsed each response. If you start seeing the fallback strategies being used more than the primary one, Google has updated its HTML and your primary selector needs updating. This takes about ten minutes to add to the scraper and saves hours of debugging when the inevitable change happens.

When to Use a SERP API Instead (Anti-Commercial Honesty)

I want to be straightforward about this. There are situations where building and maintaining a DIY Google scraper is the wrong approach, and using a dedicated SERP API is the right one. This is worth getting right before you invest engineering time.

DIY Scraper + Proxies Is the Right Choice When
You need custom data structures that SERP APIs do not provide. You need to scrape specific SERP features (maps, shopping, images) not covered by a standard API. You have in-house engineering capacity to maintain the scraper. Your volume is moderate and predictable.
In these cases: The Playwright + rotating residential proxy approach above covers most needs. TorchProxies from $4/GB with no rate limits is the proxy layer.
🛑
SERP API Is the Right Choice When
You need 100,000+ queries per month reliably. You need guaranteed uptime SLAs. You do not have engineering capacity to maintain the scraper against Google's weekly HTML changes. You need structured JSON output without building a parser.
In these cases: SerpApi, Scrapfly, or similar SERP APIs handle the infrastructure. They cost more per query but eliminate maintenance overhead entirely. The engineering time saved typically justifies the cost differential at scale.

The honest calculation: a DIY scraper requires ongoing maintenance every time Google changes something, which happens frequently. I would not estimate less than a few hours per month in maintenance time for a production Google scraper in 2026. At low query volumes, that maintenance overhead is a significant cost relative to the scraping cost. At very high volumes, it is negligible. The break-even point depends entirely on your engineering cost and query volume.

TorchProxies is the proxy infrastructure layer regardless of which approach you choose. DIY scrapers need rotating residential proxies to function reliably against Google. SERP API providers run their own proxy infrastructure. If you build your own scraper, TorchProxies handles the IP rotation so you can focus on the parsing layer.

Ready to Scrape Google at Scale?

120M+ rotating residential IPs across 195+ countries. Country-matched geo-targeting, no rate limits, free trial. Drop-in integration with Playwright in two lines.

Start Free Trial

✓ 195+ countries · ✓ No rate limits · ✓ From $5/GB · ✓ Free trial, no credit card


Common Mistakes When Scraping Google in 2026

Mistake 01
Using requests.get() Without a Browser Context
Since early 2025, plain HTTP requests to Google return a JavaScript shell, not results. You get HTTP 200 and valid HTML, but the HTML contains no search results. Always verify your HTML contains actual results before parsing.
Mistake 02
Using gl= Without a Matching Country Proxy IP
URL parameters alone do not guarantee country-specific results. Google weights the proxy IP's location alongside the gl parameter. For accurate geo-targeted data, proxy country must match gl value.
Mistake 03
Hardcoding a Single CSS Selector
Google A/B tests its HTML structure continuously. A scraper with one hardcoded selector will silently return empty results when Google changes its layout. Build a fallback strategy with at least three selector approaches.
Mistake 04
Not Checking for Empty Results
CAPTCHAs are obvious. Silent blocks are not. A scraper that returns zero results but continues without alerting is the most dangerous failure mode for data quality. Always validate result count per query.
Mistake 05
Reusing the Same Browser Context Across Queries
A single persistent browser context accumulates session signals over many requests. Create a new context per query or per small batch to reset session-level tracking and ensure each proxy IP assignment is clean.
Mistake 06
Timezone Mismatch Between Browser Profile and Proxy IP
A Tokyo proxy IP with a New York browser timezone is a reliable detection signal. Set the Playwright context timezone_id to match the proxy country. This is one of the more subtle detection vectors and often gets overlooked.

The Bottom Line

Scraping Google search results in 2026 requires a browser-based approach. Simple HTTP requests do not work reliably anymore. Playwright with rotating residential proxies is the pattern that addresses all of Google's detection signals: JavaScript execution, TLS fingerprinting, IP reputation, and behavioral patterns. The URL parameter system gives you precise geo-targeting across every major market. Fallback selector strategies keep the scraper running through Google's regular HTML changes.

For small scripts, googlesearch-python with a proxy is enough. For production multi-market SERP tracking, the Playwright approach above is the right baseline. For very high volume commercial deployments, evaluate SERP APIs against the maintenance cost of running your own infrastructure before committing to a DIY build.

Quick Reference: Google SERP Scraping in 2026
Use Playwright, Not requests Google requires JS execution since 2025. Plain HTTP requests return a shell, not results. Verify HTML contains actual results before parsing.
Geo-Targeting: gl + Country IP URL parameters (gl=us, hl=en) plus a matching country proxy IP. Both required for accurate country-specific SERP data.
Fallback Selector Strategies Build a chain: .tF2Cxc, then .g, then .MjjYud. Stop at the first strategy that returns 3+ results. Never hardcode a single selector.
New Browser Context Per Query Fresh context per request resets session-level tracking and ensures clean proxy IP assignment with rotating proxies.
Match Timezone to Proxy Country Set Playwright context timezone_id to match proxy geolocation. Mismatch is a reliable detection signal.
When DIY Is Not Worth It 100K+ queries/month or limited engineering capacity: evaluate SERP APIs. Maintenance overhead for a DIY Google scraper is non-trivial.

FAQs

There are three main approaches depending on scale. For small projects: the googlesearch-python library with pip install googlesearch-python handles basics with minimal setup but only returns URLs. For medium-scale scraping: Playwright with a rotating residential proxy renders the full JavaScript-loaded page before parsing. For large commercial deployments: dedicated SERP APIs like SerpApi or Scrapfly handle infrastructure but cost more per query. Since early 2025, plain requests.get() calls to Google no longer return real search results, only a JavaScript shell. A browser context is required.
Google's Terms of Service prohibit automated scraping. However, multiple court rulings established that scraping publicly available data does not violate the Computer Fraud and Abuse Act in the United States. The 2022 hiQ v. LinkedIn Ninth Circuit ruling is the most relevant precedent. Practically, scraping public Google results is widely used for SEO analysis and research. Terms of Service violations can result in IP blocks. For commercial applications requiring guaranteed data access, a licensed SERP API is the legally cleanest path. Always consult your legal advisors for specific use cases.
Google detects scrapers through four main signals: IP address reputation (datacenter ranges are flagged immediately), JavaScript execution capability (real browsers handle challenges; HTTP clients cannot), TLS fingerprinting (Python's requests library has a distinct TLS handshake pattern from Chrome), and behavioral signals (request timing, missing interaction patterns). Rotating residential proxies address the IP signal. Playwright addresses the JS execution and TLS signals. Realistic delays and varied behavior address the behavioral signals. All four require attention for reliable access at any meaningful scale.
Residential proxies are the best choice for Google scraping. They originate from real home internet connections assigned by ISPs, which Google's detection treats as legitimate users. Datacenter proxies are easily identified and trigger blocks quickly. Mobile proxies offer the highest trust scores but cost more at scale. For geo-targeted Google scraping, the proxy IP must be from the target country. TorchProxies Plan X provides 120M+ residential and ISP IPs across 195+ countries with city-level targeting, which covers every major SERP monitoring market.
Use Google's geolocation URL parameters: gl for the country (e.g., gl=us, gl=de, gl=jp), hl for the interface language (e.g., hl=en, hl=de, hl=ja). Example URL: https://www.google.com/search?q=your+query&gl=de&hl=de. Critically, you also need a proxy IP from the same country. URL parameters alone do not fully control geo-targeting: Google personalizes results based on both the parameters and the IP location. For accurate country-specific SERP data, both must match.
Google returns 10 organic results per page by default. To paginate, use the start parameter: start=0 for page 1, start=10 for page 2, start=20 for page 3. Add a delay between page requests to avoid rate limits. The num parameter for setting results per page was deprecated for non-API access for most request types in late 2025. For deeper pagination, iterate through start values with realistic delays. Google's results quality degrades significantly beyond the first 30 to 40 results for most queries.
BeautifulSoup can parse the HTML once retrieved, but getting the HTML is the challenge since 2025. A plain requests.get() to Google returns a JavaScript shell without real results. You need Playwright or Selenium to render the page first, then pass the rendered HTML to BeautifulSoup. The workflow is: Playwright navigates to the URL and waits for JavaScript to render, then page.content() returns the fully rendered HTML, which you pass to BeautifulSoup(html, 'html.parser'). BeautifulSoup handles the parsing; Playwright handles the rendering.
Google changes its HTML structure regularly through A/B tests, so always implement multiple fallback selectors. The most stable in 2026: organic result containers use .tF2Cxc (primary) or .g (legacy fallback), titles use h3.LC20lb, URLs use .yuRUbf a, and descriptions use .VwiC3b or [data-sncf='1']. Build your parser to try each selector strategy in order and stop at the first one that returns at least 3 results. Never hardcode a single selector, as Google's layout changes without warning.