# Scraping Google AI Overviews in 2026: Extract Summary Text, Source References, and Structured Data with Python > Source: https://scrape.do/blog/scrape-google-ai-overview/ Published: 2026-03-25 · Updated: 2026-09-07 · Authors: Selman Gökçe · Categories: Scraping Use Cases Google AI Overviews show up on a large share of search queries now, and that share has kept climbing since the feature rolled out. I called that one, and I'm still betting it goes up :) They sit above organic results, pull content from multiple sources, and deliver a synthesized answer before the user ever clicks a link. All these "AI visibility" tools you see out there scrape these overviews and a few more AI sources to generate their reports. **But AI Overviews are significantly harder to scrape than any other SERP element.** Google's [documentation on AI Overviews](https://support.google.com/websearch/answer/9351707) explains their intent, but says nothing about the rendering pipeline that makes extraction difficult. They render asynchronously through JavaScript, sometimes load seconds after the initial page, and occasionally don't appear at all for the same query on consecutive requests. Two working approaches handle this: a Playwright-based browser scraper that renders the full DOM and extracts text blocks with source references, and a [SERP API](https://scrape.do/documentation/google-scraper-api/search/) that returns the same data as structured JSON without any browser overhead. If you are working on broader [Google search result scraping](https://scrape.do/blog/scraping-google-search-results/), AI Overviews are one of several SERP elements you can extract with the same tooling. You can find the complete scripts in the [GitHub repository](https://github.com/scrape-do/scrapedo-scrapers/tree/main/google-ai-overview-scraper). ## How AI Overviews Work on Google SERPs ![Google SERP showing a fully rendered AI Overview for "how does photosynthesis work"](/uploads/blog/scrape-google-ai-overview-aio-present-on-serp.png) Before writing any scraping logic, understanding how AI Overviews load and where they sit in the DOM is essential. Their behavior is not consistent, and the approach that works depends on which state the overview is in. ### Three States of AI Overviews An AI Overview for any given query exists in one of three states: **Fully rendered in initial HTML.** The AIO content is present in the first server response. A standard HTTP request with JavaScript rendering could capture these. But this is the minority of cases. **Deferred (async-loaded).** The AIO container exists in the initial HTML as an empty shell, but the actual summary text and references load asynchronously after JavaScript execution. This is the most common state. Raw HTTP requests return an empty container with no usable content. ![Google SERP showing the deferred "Searching" state before the AI Overview has finished generating](/uploads/blog/scrape-google-ai-overview-aio-deferred-state.png) **Absent.** No AIO appears for the query at all. The scraper needs to detect this and exit gracefully rather than hanging on a timeout. The deferred state is the primary challenge. Most AIO scraping attempts fail because they send a single HTTP request and get back an empty container. The `state` distinction also matters for the SERP API approach: a `complete` state means the full content is available in the response, while a `deferred` state means Google has not finished generating the overview. In that case, a follow-up request to a separate async endpoint is needed to fetch the completed content once it is ready. That async request costs 5 additional credits and uses a single-use session key that expires after 60 seconds. ### DOM Structure of an AI Overview ![Chrome DevTools showing the AI Overview DOM structure with annotated selectors](/uploads/blog/scrape-google-ai-overview-aio-dom-structure.png) The AI Overview lives inside a container identified by the "AI Overview" heading text. From there, the content container uses the CSS class `Kevs9`. Inside that container: - **Summary text** appears in `div.Y3BBE` elements. Each one holds a paragraph of the AI-generated answer. - **Source references** render as `li.jydCyd` cards. Each card contains a title in `div.Nn35F` and an external link in an `a[href]` element. - **Inline citation badges** show the source name and a count (e.g., "Science News Explores +3"). These sit inside `span.wJwe6c` elements alongside the text they cite. The container may include an expand/collapse toggle for longer summaries, but the full text is in the DOM regardless of collapse state. No click interaction is needed to extract it. ### Why Raw HTTP Falls Short A raw HTTP request with `super=true` and JavaScript rendering enabled can occasionally capture fully-rendered AIOs. But for deferred AIOs (the majority), the response contains the container element with the text blocks and references empty. The content loads via a secondary async call that only fires in a live browser context. This makes raw HTTP scraping unreliable for production use. The success rate varies by query type and shifts as Google updates its rendering pipeline. Browser-based scraping with Playwright handles both states by waiting for the async content to appear in the live DOM. The same async rendering challenge applies to other Google verticals like [Google Shopping](https://scrape.do/blog/google-shopping-scraping/) and [Google Maps](https://scrape.do/blog/google-maps-scraping/), where product cards and place details load after the initial page response. ## Scraping AI Overviews with Playwright Playwright launches a real Chromium instance that renders JavaScript, waits for async content, and provides full DOM access. Combined with [Scrape.do](https://scrape.do) as a proxy, the browser traffic routes through [rotating residential IPs](https://scrape.do/blog/rotating-proxies-everything-you-need-to-know/), bypassing Google's bot detection while Playwright handles the rendering. ### Prerequisites The scraper uses Playwright for browser automation and standard library modules for JSON handling: ``` pip install playwright && playwright install chromium ``` A Scrape.do account provides the API token used for proxy authentication. Free tier available at [scrape.do/register](https://scrape.do/register/). ![Scrape.do dashboard showing the API token field](/uploads/blog/scrape-do-token.png) ### Proxy Configuration and Browser Launch The script configures Scrape.do as an HTTP proxy. The token goes in the `username` field, and `super=true` in the `password` field enables residential proxy features: ```python import json import urllib.parse from playwright.sync_api import sync_playwright token = "" query = "how does photosynthesis work" encoded_query = urllib.parse.quote_plus(query) google_urls = [ f"https://www.google.com/search?q={encoded_query}&hl=en&gl=us", f"https://www.google.com/search?q={encoded_query}&hl=en&gl=us" f"&uule=w+CAIQICIYV2VzdCBOZXcgWW9yaywgTmV3IEplcnNleQ", ] proxy_config = { "server": "http://proxy.scrape.do:8080", "username": token, "password": "super=true", } ``` The `google_urls` list contains two variants: a standard Google search URL and one with a US geolocation `uule` parameter. AI Overviews trigger more consistently from US IP ranges, so the script tries the standard URL first and falls back to the geocoded version if no AIO is found. Playwright launches Chromium in headless mode with the proxy config. The browser context sets `ignore_https_errors=True` (required for proxy TLS), a desktop viewport, and a standard Chrome user agent: ```python with sync_playwright() as p: browser = p.chromium.launch(headless=True, proxy=proxy_config) context = browser.new_context( ignore_https_errors=True, viewport={"width": 1280, "height": 900}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36", ) page = context.new_page() ``` ### Detecting the AI Overview The script navigates to the Google SERP, waits for the page to settle (8 seconds for async content), then checks whether the "AI Overview" heading exists in the DOM: ```python for attempt, url in enumerate(google_urls): page.goto(url, timeout=60000, wait_until="networkidle") page.wait_for_timeout(8000) aio_found = page.evaluate("""() => { const headings = document.querySelectorAll( 'h1, h2, div.Fzsovc, div.YzCcne' ); for (const h of headings) { if (h.textContent.trim() === 'AI Overview') return true; } return false; }""") if aio_found: break ``` Detection searches for the literal text "AI Overview" across heading elements and known container classes. This avoids confusing the AIO with Featured Snippets or People Also Ask sections, which use different container structures but can share some parent selectors like `#Odp5De`. If the first URL does not trigger an AIO, the loop retries with the US geocoded URL. If neither works, the script exits. ### Getting the Content Container Once the AIO is detected, the script walks up the DOM from the heading element to find the `Kevs9` content container: ```python aio_container = page.evaluate_handle("""() => { const headings = document.querySelectorAll( 'h1, h2, div.Fzsovc, div.YzCcne' ); for (const h of headings) { if (h.textContent.trim() === 'AI Overview') { let el = h; for (let i = 0; i < 10; i++) { el = el.parentElement; if (!el) break; if (el.classList.contains('Kevs9')) return el; } return h.parentElement?.parentElement || h.parentElement; } } return null; }""") ``` The function finds the heading, then walks up to 10 parent levels looking for the `Kevs9` class. This class wraps both the summary text and the reference cards. If `Kevs9` is not found (Google may rename it), the fallback returns the heading's grandparent as a reasonable approximation. ### Extracting Text Blocks Text extraction runs as injected JavaScript on the container handle. The primary targets are `div.Y3BBE` elements, which hold one paragraph each: ```python text_blocks = page.evaluate("""(container) => { const blocks = []; const seen = new Set(); const skip = [/^Show (more|all|less)$/i, /^AI Overview$/i, /not available/i, /try again later/i]; for (const div of container.querySelectorAll('div.Y3BBE')) { const clone = div.cloneNode(true); clone.querySelectorAll('style, .WTfRgd, .wJwe6c') .forEach(e => e.remove()); let text = clone.textContent.trim(); text = text.replace(/\\.[A-Za-z0-9_]+\\{[^}]*\\}/g, '').trim(); if (text.length > 15 && !seen.has(text) && !skip.some(p => p.test(text))) { seen.add(text); blocks.push({ type: 'paragraph', text }); } } return blocks; }""", aio_container) ``` Before extracting text from each `Y3BBE` div, the function clones the element and strips out inline `