Google Finance is one of the most widely used places to read real-time market data, and its quote pages hold exactly the structured numbers that drive price tracking, portfolio monitoring, and research: the live price, the day's change in value and percent, the previous close, and the headline market metrics like market cap, P/E ratio, and the day and year trading ranges. For anyone watching a basket of tickers, that public market data is the raw material, and copying it by hand across dozens of symbols is slow and error-prone.

This guide shows you how to scrape Google Finance with Python the reliable way. You build a small, runnable scraper that fetches rendered quote pages through the Crawling API, parses the fields you want with BeautifulSoup, loops over several tickers, and exports clean JSON. The whole walkthrough stays scoped to public market data, which is factual rather than personal, and the legality section near the end is not boilerplate, so read it before you point this at any real volume.

What you will build

A Python script that takes a list of Google Finance quote URLs (one per ticker, in the SYMBOL:EXCHANGE form Google uses), fetches each rendered page through the Crawling API, and extracts a structured record per stock. The running example uses GOOGL, AAPL, and MSFT on NASDAQ. We pull these fields:

  • Title the company name shown at the top of the quote page.
  • Price the current quoted price for the stock.
  • Change the absolute price change and its percentage for the session.
  • Previous close the prior session's closing price.
  • Market data the key metrics block: market cap, P/E ratio, and the day and year ranges where present.

Why a plain request fails on Google Finance

Request a Google Finance quote URL with a bare HTTP client and you get a response with status 200 but only a fraction of the data in the body. Two things work against you. First, Google Finance fills its quote pages in the browser through JavaScript, so the initial HTML is a thin shell. The clearest tell is the price change percentage: pull it out of that first response and you get None, because that value renders client-side after load. Second, Google flags automated traffic quickly. Datacenter IPs and request patterns that do not look like a real browser get rate-limited, IP-blocked, or challenged before they reach the rendered content.

So a working Google Finance scraper needs two things in one request: a browser that actually renders the page, and an IP the platform reads as a real visitor. You can build that yourself with a headless browser and a pool of rotating residential proxies, but keeping that stack healthy is most of the work. The Crawling API folds both into a single call: send it the URL with a JavaScript token, it renders the page behind a trusted IP and returns finished HTML to parse. For the broader background on rendered targets, see how to crawl JavaScript websites.

Why the JS token

Crawlbase offers two token types. The normal token fetches static HTML; the JavaScript (JS) token renders the page in a real browser first. Google Finance fills its price, change percentage, and metrics client-side, so you need the JS token here. The normal token returns the same thin shell a plain fetch would, with the change percentage missing, and there is little useful to parse out of it.

Prerequisites

You need a few things in place before writing any code. None of them take long.

Basic Python. You should be comfortable writing and running a Python script and installing packages with pip. If you are new to the parsing side, the BeautifulSoup guide is a good companion to this tutorial.

Python 3.8 or later. Confirm your version with python --version. If you do not have it, install it from python.org or through a distribution like Anaconda, and make sure Python is on your PATH.

A Crawlbase account and JS token. Sign up, open your dashboard, and copy your JavaScript (JS) token from the account docs page. Crawlbase includes 1,000 free requests to start, which is plenty for working through this guide, and you pay only for successful requests. Treat the token like a password: it authenticates your requests, so keep it out of version control.

Set up the project

Create a virtual environment so project dependencies stay isolated, then install the libraries the scraper needs.

bash
python --version

python -m venv google_finance_env
source google_finance_env/bin/activate

pip install crawlbase beautifulsoup4

On Windows, activate the environment with google_finance_env\Scripts\activate instead of the source line. Two dependencies do the work: crawlbase is the official client for the Crawling API, and beautifulsoup4 parses the returned HTML so you can pull out individual fields by CSS selector. The json module ships with the standard library, so there is nothing more to install for the export step.

Step 1: Fetch a rendered Google Finance page

Start by getting a finished page. Import the CrawlingAPI class, initialize it with your JS token, and request a quote URL. Google Finance loads its values asynchronously, so pass ajax_wait and page_wait to hold for the dynamic content before the page is captured. Checking the Crawlbase pc_status before you parse keeps failures loud instead of silent.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

OPTIONS = {
    "ajax_wait": "true",
    "page_wait": 5000,
}

def crawl(page_url):
    response = api.get(page_url, OPTIONS)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['pc_status']}")
    return None

if __name__ == "__main__":
    quote_url = "https://www.google.com/finance/quote/GOOGL:NASDAQ"
    html = crawl(quote_url)
    print(html[:500] if html else "No HTML returned")

The two wait options matter for a client-rendered target like Google Finance. ajax_wait tells the API to wait for asynchronous content to finish loading, and page_wait holds for a fixed number of milliseconds after load so late-rendering values appear before the page is captured. Five seconds is a reasonable start; raise it if the change percentage comes back empty. Run the script with python google_finance_scraper.py and you should see real quote-page markup, not the shell a plain request returns. That confirms rendering works before you write a single selector.

Crawlbase Crawling API

Google Finance needs a rendered page behind a trusted IP, in one call, which is exactly what the ajax_wait and page_wait options above set up. The Crawling API takes a JS token, runs the page in a real browser so the change percentage actually loads, rotates through residential IPs server-side, and hands you finished HTML, so you skip running a headless fleet and a proxy pool yourself. Point it at a public quote page on the free tier first.

Step 2: Parse the price, change, and title

With finished HTML in hand, load it into BeautifulSoup and pull the headline fields. Google Finance keeps the company name, the current price, and the session change in the page's main region, each in a generated class. The selectors below come straight from the live quote layout: the title sits in zzDege, the price in AHmHk, and the change percentage in JwB6zf. Each lookup is guarded so a missing field returns None instead of crashing the run.

python
from bs4 import BeautifulSoup

def get_price(soup):
    el = soup.find("main")
    if not el:
        return None
    price = el.find("div", "AHmHk")
    return price.get_text(strip=True) if price else None

def get_change_percentage(soup):
    main = soup.find("main")
    if not main:
        return None
    change = main.find("div", "JwB6zf")
    return change.get_text(strip=True) if change else None

def get_stock_title(soup):
    main = soup.find("main")
    if not main:
        return None
    title = main.find("div", "zzDege")
    return title.get_text(strip=True) if title else None

The JwB6zf element is the one to watch. On a thin, non-rendered response it is missing entirely and get_change_percentage returns None; once the page is rendered through the JS token, it carries the session change in value and percent. That single field is the simplest proof that rendering is doing its job.

Selectors drift

Google's generated class names like AHmHk, JwB6zf, and zzDege change without notice. Treat the selectors here as a starting template, not a contract. When a field comes back None on a clearly rendered page, re-inspect the live quote in your browser's dev tools and update the class. Periodic selector maintenance is normal for any production scraper, not a sign something is broken.

Step 3: Parse the market data block

Below the headline numbers, Google Finance lists the key metrics as label and value rows: previous close, day range, year range, market cap, P/E ratio, and more. Each row is a gyFHrc container holding a label in mfs7Fc and a value in P6K39c. Walking every row into a dictionary keeps the parser resilient: whatever metrics a given ticker exposes show up as keys, and you read Previous close, Market cap, P/E ratio, Day range, and Year range straight off that dictionary.

python
def get_market_data(soup):
    rows = soup.find_all("div", {"class": "gyFHrc"})
    data = {}
    for row in rows:
        label = row.find("div", {"class": "mfs7Fc"})
        value = row.find("div", {"class": "P6K39c"})
        if label and value:
            data[label.get_text(strip=True)] = value.get_text(strip=True)
    return data

KEEP = ["Previous close", "Day range", "Year range", "Market cap", "P/E ratio"]

def select_market_fields(market_data):
    return {key: market_data[key] for key in KEEP if key in market_data}

The get_market_data helper captures every label and value pair on the page, so you do not hardcode a selector per metric. select_market_fields then narrows that to the scope here: previous close, the day and year ranges, market cap, and P/E ratio. Because the filter checks if key in market_data, a ticker that omits one of these simply leaves that key out of the record rather than failing. Keep the full get_market_data output if you later want avg volume or primary exchange too.

Step 4: Assemble the full script

Now wire the pieces into one runnable script: fetch each quote page, parse the headline fields and the market block, and export the records to JSON. The extract_quote function brings the per-field helpers together into a single record, and main loops over a list of SYMBOL:EXCHANGE URLs so you can pull several tickers in one run.

python
import json
import time
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

OPTIONS = {
    "ajax_wait": "true",
    "page_wait": 5000,
}

KEEP = ["Previous close", "Day range", "Year range", "Market cap", "P/E ratio"]

def crawl(page_url):
    response = api.get(page_url, OPTIONS)
    if response["headers"]["pc_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['pc_status']}")
    return None

def text_in_main(soup, class_name):
    main = soup.find("main")
    if not main:
        return None
    el = main.find("div", class_name)
    return el.get_text(strip=True) if el else None

def get_market_data(soup):
    rows = soup.find_all("div", {"class": "gyFHrc"})
    data = {}
    for row in rows:
        label = row.find("div", {"class": "mfs7Fc"})
        value = row.find("div", {"class": "P6K39c"})
        if label and value:
            data[label.get_text(strip=True)] = value.get_text(strip=True)
    return data

def extract_quote(html, url):
    soup = BeautifulSoup(html, "html.parser")
    market = get_market_data(soup)
    return {
        "url": url,
        "title": text_in_main(soup, "zzDege"),
        "price": text_in_main(soup, "AHmHk"),
        "change": text_in_main(soup, "JwB6zf"),
        "previous_close": market.get("Previous close"),
        "market_data": {key: market[key] for key in KEEP if key in market},
    }

def main():
    urls = [
        "https://www.google.com/finance/quote/GOOGL:NASDAQ",
        "https://www.google.com/finance/quote/AAPL:NASDAQ",
        "https://www.google.com/finance/quote/MSFT:NASDAQ",
    ]
    records = []
    for url in urls:
        html = crawl(url)
        if html:
            records.append(extract_quote(html, url))
        time.sleep(2)

    with open("finance_data.json", "w") as f:
        json.dump(records, f, indent=2)
    print(f"Saved {len(records)} quotes")

if __name__ == "__main__":
    main()

The single text_in_main helper replaces the three near-identical title, price, and change functions from earlier: it finds the main region, looks up one generated class, and returns the stripped text or None. extract_quote assembles one record per ticker, pulling previous close out of the market dictionary and nesting the rest under market_data. The two-second sleep paces the run so you are not hammering the site. Edit the urls list to scrape any other symbols.

What the output looks like

Run the full script with python google_finance_scraper.py and you get a clean structured record per ticker, ready for analysis, a database, or a spreadsheet. The values below are illustrative; your run returns whatever the live quote shows.

json
[
  {
    "url": "https://www.google.com/finance/quote/GOOGL:NASDAQ",
    "title": "Alphabet Inc Class A",
    "price": "$163.79",
    "change": "+1.01 (0.62%)",
    "previous_close": "$162.78",
    "market_data": {
      "Previous close": "$162.78",
      "Day range": "$163.09 - $167.12",
      "Year range": "$103.71 - $174.71",
      "Market cap": "2.04T USD",
      "P/E ratio": "25.54"
    }
  },
  {
    "url": "https://www.google.com/finance/quote/AAPL:NASDAQ",
    "title": "Apple Inc",
    "price": "$169.30",
    "change": "-0.61 (0.36%)",
    "previous_close": "$170.33",
    "market_data": {
      "Previous close": "$170.33",
      "Day range": "$169.11 - $172.71",
      "Year range": "$164.08 - $199.62",
      "Market cap": "2.61T USD",
      "P/E ratio": "26.34"
    }
  }
]

Notice the change field now carries a real value instead of null. On a plain, non-rendered fetch it comes back empty because Google paints it client-side; routing the request through the JS token is what makes it appear. From here the records drop straight into pandas or any spreadsheet for tracking price moves, comparing P/E across a watchlist, or feeding a price-intelligence workflow.

Scaling to a watchlist and staying unblocked

The script above pulls three tickers, but the shape scales to a full watchlist by extending the urls list. As the run grows, a few habits keep it healthy, and they apply to any hard target.

  • Pace your requests. Hammering quotes in a tight loop is the fastest way to get throttled or challenged. The two-second sleep above is the floor, not the ceiling; widen it for larger watchlists and vary your symbols instead of refetching one path at full speed.
  • Lean on rotation. A pool of residential IPs spreads requests across many real-user addresses so no single one trips a rate limit. The Crawling API handles this for you; if you roll your own stack, this is the part to get right.
  • Read the status codes. A run that starts returning non-200 pc_status values is telling you the current rate or IP tier is no longer enough. Treat that as a signal to back off, not noise to ignore.

For larger jobs, the async Crawler queues requests and delivers results to a webhook, which suits refreshing many tickers on a schedule without holding open connections. For the broader playbook, see how to scrape websites without getting blocked. The same render-plus-trusted-IP approach carries over to other market sources too, such as scraping crypto prices from CoinMarketCap.

Whether scraping Google Finance is allowed depends on Google's terms of service, your jurisdiction, and what you do with the data. Google's terms restrict automated access to its services, so scraping can run against those terms regardless of how careful your tooling is. None of the code here changes that; it only makes the technical part work. Read Google's Terms of Service and the robots.txt for the Google Finance paths, and treat both as the boundary for what you collect and how often you request it.

The data this guide targets is firmly on the public, non-personal side of the line: stock prices, the session change, previous close, market cap, P/E ratio, and the day and year ranges are factual market figures, not anyone's personal information. That keeps the work narrower than scraping sites that carry user content, but it does not put you above Google's terms. Stay on quote pages that any visitor can load without an account, keep your request volume low enough that you are not straining Google's servers, and do not redistribute the underlying market feeds wholesale, since exchanges and data vendors license those figures and that licensing follows the numbers.

This guide is deliberately scoped to public quote pages because that is the line that keeps the work defensible. It does not cover anything behind a login, paywalled or premium data, or any attempt to bypass authentication or rate limits. If your project needs guaranteed uptime, redistribution rights, or higher volume than polite scraping allows, the right path is a licensed feed: use an official market-data API or a licensed exchange feed for production. That is the correct route for commercial or bulk use, not a more aggressive scraper.

Recap

Key takeaways

  • Google Finance is client-side rendered. A plain request returns a thin shell with the price change percentage missing, so you must render the page before you parse it.
  • You need rendering and a trusted IP together. The Crawling API with a JS token does both in one call; ajax_wait and page_wait control how long it waits for the values to load.
  • Map fields to real classes. Title sits in zzDege, price in AHmHk, and the session change in JwB6zf; the metrics block is a set of gyFHrc rows pairing an mfs7Fc label with a P6K39c value.
  • Loop and export. Pass a list of SYMBOL:EXCHANGE quote URLs, pace the run with short sleeps, and write one record per ticker to JSON with the market data nested.
  • Stay on public market data. Respect Google's ToS and robots.txt, keep to factual public quotes, and use an official market-data API or licensed feed for production or redistribution.

Frequently Asked Questions (FAQs)

Why is the price change percentage null on a plain request?

Because Google Finance renders that value client-side with JavaScript. The initial HTML is a shell, and the JwB6zf element that holds the session change only appears after the page's scripts run in a browser. A raw HTTP request returns status 200 with the change missing, which is why the field comes back None. Rendering the page first, which the Crawling API's JS token handles, makes the value present so BeautifulSoup can read it.

Do I need the normal token or the JS token for Google Finance?

The JS token. The normal token fetches static HTML, which on Google Finance is the same thin shell a plain fetch returns, with the change percentage absent. The JS token renders the page in a real browser before handing back the HTML, so the price, change, and metrics block are all present when you parse them.

What data can I scrape from a Google Finance quote page?

Public market fields: the company title, the current price, the session change in value and percent, the previous close, and the metrics block, which includes market cap, P/E ratio, and the day and year ranges where present. These are factual public quotes that any visitor can load, not personal data. Stay on that scope and use a licensed feed if you need redistribution rights.

My selectors return None. What changed?

Almost certainly Google's markup. Its generated class names (AHmHk for price, JwB6zf for change, zzDege for title, and gyFHrc / mfs7Fc / P6K39c for the metrics rows) change without notice, so selectors that worked last month can break. Re-inspect a live quote in your browser's dev tools and update the class strings. Periodic selector maintenance is normal for any production scraper.

How do I scrape multiple stocks at once?

Google Finance addresses each stock as SYMBOL:EXCHANGE in the quote URL, for example GOOGL:NASDAQ. Build a list of those URLs and loop over it, calling the same extract_quote function on each rendered page, as the main function above shows. Keep a short sleep between requests to pace the run, and extend the list to cover your full watchlist.

Is there an official Google Finance API for production use?

Google does not offer a public, supported market-data API for Google Finance, so for production or commercial use the dependable path is a licensed market-data provider or an official exchange feed rather than scraping the web page. Those feeds come with uptime guarantees and redistribution terms that scraping a public page does not, which matters once data accuracy or reuse rights are part of the requirement.

Start Building

Crawl any site at scale, without fighting infrastructure.

Crawlbase handles proxies, fingerprints, and CAPTCHAs so your team ships data pipelines instead of maintaining crawl plumbing. 1,000 requests free, no card required.

Self-serve · No sales call required · Enterprise crawl volumes available