How to Scrape Google Scholar with Python
- Google Scholar's robots.txt disallows
/scholarand/searchfor all bots, so the public results path is off-limits to well-behaved crawlers. - I ran requests + BeautifulSoup against
/scholaron June 12, 2026: a single query returned HTTP 200 and 10 parseable results (title, authors, year, citation count). - The blocking is real and fast: in a 5-request burst from one IP my first call hit HTTP 429 and redirected to the
/sorry/CAPTCHA. - There is no official Scholar API. DIY works for a handful of queries, then needs proxy rotation and CAPTCHA solving to scale.
- A scraper API (ChocoData's universal endpoint) runs the browser, rotates IPs, and returns the same HTML from one call.
I tried to scrape Google Scholar the simple way and pasted the real output below. Short version: a single requests query returned HTTP 200 with 10 fully parseable results, but a 5-request burst from one IP tripped Google’s 429 CAPTCHA on the very first call. So DIY works for a few queries and falls over the moment you scale. This guide covers whether you can scrape Scholar, what each result holds, how the blocking fires, a working Python parser with real output, and the scraper-API path. For fundamentals, see my web scraping guide and the Python walkthrough.
Can you legally scrape Google Scholar?
Scraping public Scholar pages is a legal gray area, and you should read the source documents before writing code. Scholar’s robots.txt is the clearest technical signal. I fetched https://scholar.google.com/robots.txt on June 12, 2026 and it disallows the two paths you would actually scrape:
User-agent: *
Disallow: /search
Disallow: /index.html
Disallow: /scholar
Disallow: /citations?
Allow: /citations?user=
robots.txt is a crawling convention, not a statute, so it does not by itself create legal liability. The weight comes from elsewhere:
| Source | What it governs | Practical takeaway |
|---|---|---|
| Google Terms of Service | Contract between you and Google | The terms restrict automated access; breaking them is a contract matter, not automatically a crime |
robots.txt (Disallow: /scholar) | Crawler etiquette | Scholar explicitly asks bots off /scholar; ignoring it weakens any “good faith” defense |
| hiQ v. LinkedIn (9th Cir., 2022) | Public-data scraping under the US CFAA | Accessing publicly available pages is generally not CFAA “unauthorized access” |
The hiQ ruling leaned toward scrapers for openly accessible data and held that scraping public pages is generally not a CFAA violation. It did not resolve breach-of-contract claims, and it is US-specific. A separate point matters for Scholar: the bibliographic facts themselves (title, authors, year, citation count) are facts, and facts are not copyrightable under Feist v. Rural Telephone (US Supreme Court, 1991). My rule of thumb: scrape only public result pages, never log in, rate-limit hard, store facts rather than copying abstracts wholesale, and keep author personal data out of what you retain. See scraping without getting blocked for the technical side. None of this is legal advice; if the data feeds a commercial product, talk to a lawyer.
What data can you extract from a Google Scholar result?
Each Scholar result is a single block (div.gs_r.gs_or.gs_scl) carrying bibliographic fields plus citation signals. Knowing the layout tells you what to target and what an API should return as JSON.
| Field | Lives in | Notes |
|---|---|---|
| Title | h3.gs_rt | Sometimes prefixed [PDF], [HTML], [BOOK], [CITATION] |
| Result URL | h3.gs_rt a[href] | The publisher or PDF link; absent for citation-only entries |
| Authors / venue / year | div.gs_a | One comma-joined string, e.g. “WX Zhao, K Zhou… - Frontiers of Computer…, 2023” |
| Snippet | div.gs_rs | Truncated abstract fragment, not the full text |
| Cited-by count | div.gs_fl a containing “Cited by” | The single most useful signal; links to the citing-papers list |
| Related / versions | div.gs_fl a | ”Related articles”, “All N versions” |
| PDF link | div.gs_ggs a[href] | Direct file when Scholar found one |
| BibTeX / EndNote | ”Cite” link to gs_cit endpoint | Clean structured citation, one extra request each |
The div.gs_a string is the messy part. Authors, venue, and year arrive as one line that you split on the en-dash separators, and the format varies by source.
How does Google Scholar block scrapers?
Scholar fronts a rate limiter that watches IP, request cadence, and headers, then redirects suspect traffic to a CAPTCHA. I confirmed this in my own test. When I fired 5 rapid queries from a single IP with no delay, the first request returned HTTP 429 and its final URL landed on Google’s /sorry/ CAPTCHA path. Here is the raw signal from that burst:
req 0: status=429 len=3292 blocked_signal=True final_url_has_sorry=True
req 1: status=200 len=167295 blocked_signal=False
req 2: status=200 len=166618 blocked_signal=False
req 3: status=200 len=168024 blocked_signal=False
req 4: status=200 len=166825 blocked_signal=False
The defenses, in the order you hit them:
| Layer | Trigger | What you see |
|---|---|---|
| Rate limiting | Too many requests per IP in a short window | HTTP 429, redirect to /sorry/index?continue=... |
| CAPTCHA wall | Sustained automated pattern | reCAPTCHA challenge page; results never load |
| IP reputation | Datacenter / known-proxy ranges | Faster CAPTCHA, sometimes from the first request |
| Header checks | Missing or bot-like User-Agent | Higher chance of the /sorry/ redirect |
| Pagination heat | Deep &start= paging at speed | Blocks escalate faster than first-page queries |
The behavior is probabilistic, not a clean threshold. One slow query usually succeeds; a tight burst tripped the 429 immediately. That is the core reason DIY scraping does not scale here without proxies.
Method 1: DIY scraping with Python (the tested result)
A plain requests + BeautifulSoup parser works for a single Scholar query, and I have the real output to prove it. The selectors are stable: each result is a div.gs_r.gs_or.gs_scl, the title sits in h3.gs_rt, and the citation count is the div.gs_fl link whose text starts with “Cited by”.
import requests
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36"
),
"Accept-Language": "en-US,en;q=0.9",
}
def scrape_scholar(query, start=0):
url = "https://scholar.google.com/scholar"
params = {"q": query, "hl": "en", "start": start}
r = requests.get(url, params=params, headers=HEADERS, timeout=25)
# Detect the rate-limit / CAPTCHA redirect before parsing.
if r.status_code == 429 or "/sorry/" in r.url:
raise RuntimeError(f"Blocked: HTTP {r.status_code} -> {r.url}")
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
results = []
for block in soup.select("div.gs_r.gs_or.gs_scl"):
title_el = block.select_one("h3.gs_rt")
meta_el = block.select_one("div.gs_a")
cited = None
for a in block.select("div.gs_fl a"):
if a.get_text(strip=True).startswith("Cited by"):
cited = a.get_text(strip=True)
results.append({
"title": " ".join(title_el.get_text().split()) if title_el else None,
"link": title_el.a["href"] if (title_el and title_el.a) else None,
"meta": " ".join(meta_el.get_text().split()) if meta_el else None,
"cited_by": cited,
})
return results
if __name__ == "__main__":
for row in scrape_scholar("large language models")[:3]:
print(row["title"][:60])
print(" ", row["meta"][:70])
print(" ", row["cited_by"])
I ran this against Scholar on June 12, 2026. The single query returned HTTP 200 and parsed cleanly. Real output, trimmed to three rows:
HTTP 200
A survey on evaluation of large language models
Y Chang, X Wang, J Wang, Y Wu, L Yang... - ACM transactions on...,
Cited by 6125
A survey of large language models
WX Zhao, K Zhou, J Li, T Tang, Z Dong, Y Hou... - Frontiers of Comp...,
Cited by 8848
ChatGPT for good? On opportunities and challenges of large language mo
E Kasneci, K Sessler, S Kuechemann, M Bannert... - Learning and ind...,
Cited by 10620
That is genuine Scholar data: titles, author/venue lines, and live citation counts. The catch is what you saw in the blocking section. The same code, fired in a tight loop, tripped a 429 on the first request. To run more than a handful of queries you need to add, at minimum:
- A delay between requests (several seconds, randomized). Scholar’s
robots.txtsets no crawl-delay, yet the rate limiter clearly rewards slower cadence. - Rotating residential proxies, because a single IP gets a CAPTCHA fast.
- A CAPTCHA fallback, since once
/sorry/fires the IP is parked until the challenge is solved. - Header rotation and retry-with-backoff on every 429.
At that point you are building a small scraping infrastructure project, which is the trade-off the next method removes.
Method 2: Scraping Google Scholar with a scraper API
A scraper API turns the whole problem into one HTTP call that returns the page, which is why teams skip the proxy-and-CAPTCHA stack. The API runs the request from a clean residential IP, rotates on failure, clears the /sorry/ challenge, and hands back the HTML you parse with the same BeautifulSoup selectors from Method 1.
ChocoData (chocodata.com) is one example I checked against its own docs. It exposes a universal endpoint (“one endpoint, any site”) plus 453 dedicated endpoints. For Scholar, the universal endpoint is the route, since there is no dedicated Scholar endpoint to assume. The documented request shape:
import requests
from bs4 import BeautifulSoup
API_KEY = "YOUR_API_KEY" # free tier: 1,000 requests/month, no card
resp = requests.get(
"https://chocodata.com/api/v1/universal",
params={
"url": "https://scholar.google.com/scholar?q=large+language+models&hl=en",
"render": "true", # run the headless browser
"api_key": API_KEY,
},
timeout=60,
)
resp.raise_for_status()
# The API handles the IP rotation and CAPTCHA; you parse the returned HTML
# with the exact selectors from Method 1.
soup = BeautifulSoup(resp.text, "html.parser")
results = soup.select("div.gs_r.gs_or.gs_scl")
print(len(results), "results")
The endpoint pattern is GET /api/v1/universal?url=... for any page, and GET /api/v1/{site}/{resource}?api_key=... for dedicated endpoints. Per ChocoData’s site, the service handles residential proxy rotation, CAPTCHAs, failed-request retries, and optional JavaScript rendering. I have not independently benchmarked their Scholar success rate, so treat the parsing and success-rate claims as vendor-stated until you test on the free tier. The honest pitch is narrow: you are paying the API to absorb the 429-and-CAPTCHA problem that killed the DIY loop, not for magic parsing.
Which method should you choose?
Match the method to your query volume, since that is the variable Scholar punishes. Below a dozen lookups, DIY is free and works, as my test showed. Past that, the blocking cost dominates.
| Factor | DIY (requests) | Scraper API |
|---|---|---|
| First-query success | Yes (HTTP 200, 10 results in my test) | Yes |
| Scaling past a burst | Fails: 429 / /sorry/ CAPTCHA | API absorbs blocking |
| Proxies | You source and rotate them | Included |
| CAPTCHA handling | You build it | Included |
| Maintenance | You track selector and block changes | Vendor tracks blocking; you keep selectors |
| Cost | Free + proxy bill | Per-request, free tier to start |
| Best for | A few queries, learning, one-off pulls | Recurring jobs, many queries, citation tracking |
My take: write the Method 1 parser first because it teaches you the DOM and proves the selectors, then reach for an API the moment you need consistent volume. The selectors are identical across both, so the migration is a one-line change to where you fetch the HTML. If you are building the parser from scratch, my BeautifulSoup guide covers the selector patterns, and the Scrapy guide covers running this at scale with built-in throttling and retries.
FAQ
No. Google publishes APIs for Search (Custom Search JSON), Maps, and YouTube, but Scholar has never had a public API. The Scholar metrics and profile pages are the only structured surfaces, and they are not a query API. For programmatic search results you either parse the HTML yourself or use a scraper API.
There is no published number, and it is not fixed. In my own June 2026 test a burst of 5 rapid requests from a single residential IP tripped a 429 on the first call, then recovered. Spacing requests several seconds apart and rotating IPs pushes the limit far higher, which is exactly what a scraper API automates.
Yes, each result has a 'Cite' link that exposes BibTeX, EndNote, and RefMan formats via a gs_cit endpoint. It is cleaner than parsing the gs_a author line, but it costs an extra request per result and is rate-limited the same way the search page is, so it does not dodge the blocking problem.