๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Guides

How to Scrape Goodreads Book Data: Ratings, Reviews

Scrape Goodreads book data after the API shutdown: pull the book-page JSON-LD, Apollo GraphQL reviews, ratings, author pages, and Listopia lists reliably.

S SparkProxy 0 23 min read
Share
How to Scrape Goodreads Book Data: Ratings, Reviews

Most tutorials on how to scrape Goodreads book data were written for an API that no longer exists. Goodreads stopped issuing new developer keys in December 2020 and wound its public API down, so if you want book ratings, review text, author bibliographies, or Listopia rankings today, scraping the live site is the route that's left. Here's the useful part: every Goodreads book page still ships a clean schema.org Book block, a full Apollo GraphQL cache, and stable /book/show/ URLs you can build a pipeline around. This guide walks the reliable extraction paths for books, ratings, reviews, author pages, and lists, the exact fields each one returns, and where the legal line sits so you don't cross it by accident.

What you can scrape from Goodreads and the ID system

Before writing code, learn how Goodreads names things, because one distinction shapes the whole pipeline: a book is one specific edition (a hardcover, a paperback, an ebook), while a work is the abstract title across all its editions. Ratings and reviews aggregate at the work level, so the review query in section 7 needs a work ID, not the edition ID in the URL.

The URLs themselves are stable and predictable:

EntityURL patternExample
Book (edition)`/book/show/``/book/show/2767052-the-hunger-games`
Author`/author/show/``/author/show/153394.Suzanne_Collins`
Listopia list`/list/show/``/list/show/1.Best_Books_Ever`
Genre shelf`/shelf/show/``/shelf/show/fantasy`
Series`/series/``/series/73758-the-hunger-games`
Search`/search?q=``/search?q=hunger+games`

The numeric prefix is the real ID. The slug after the hyphen is cosmetic, so https://www.goodreads.com/book/show/2767052 loads the same page as the pretty URL. Book 2767052 is The Hunger Games by Suzanne Collins, and it's the working example throughout this guide.

Here's the data you can realistically collect, and the best source for each:

DataBest sourceNotes
Title, author, page count, ISBN, formatBook JSON-LD or Open LibraryIn the initial HTML, and free from Open Library
Average rating, rating count, review countBook JSON-LD `aggregateRating``ratingValue`, `ratingCount`, `reviewCount`
Star distribution (the 5-star histogram)`__NEXT_DATA__` / GraphQLNot present in JSON-LD
Review text, reviewer, star scoreGraphQL `getReviews`Paginated, and the text is copyrighted
Author bibliography, average ratingAuthor page`/author/show/` book links
Ranked book lists and vote countsListopia`/list/show/`, server-rendered HTML

The pattern to internalize: Goodreads hands you structured JSON on the book page. Parsing rendered HTML with CSS selectors is the fallback for the older pages (lists, author, shelves), not the first move for books.


Why scraping is the only route now: the retired API

For years the standard answer to "how do I get Goodreads data" was the official REST API. That answer expired. Goodreads announced it would stop issuing new API keys as of December 8, 2020, and began retiring the public developer API for good. If you're starting a project today, you cannot get a key, existing integrations were told to expect deprecation, and the old endpoints have grown unreliable.

That leaves three practical options, and this guide covers all of them:

  1. Scrape the live Goodreads pages for the data that is genuinely unique to Goodreads: its large-sample aggregate rating, its review corpus, and its Listopia rankings.
  2. Pull the plain book facts from open sources (Open Library and the Google Books API) instead of scraping them, which is faster, free, and clearly licensed. See section 10.
  3. Combine both in a hybrid pipeline so you spend scraping budget only where you have to. See section 11.

One more thing worth knowing up front: Amazon has owned Goodreads since 2013. That matters for two reasons. The Terms of Service inherit Amazon's stance on automated access, and the book identifiers you'll see inside the page carry Amazon work and book IDs (more on that in section 6). If you already scrape the retail side, our guide to scraping Amazon product data shares a lot of the same defensive habits.


Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

The fastest path: the book-page JSON-LD

Open any Goodreads book page, view source, and search for application/ld+json. Goodreads embeds a schema.org Book object in the page for search engines, and it's server-rendered, so it sits in the initial HTML before any JavaScript runs. That one detail saves you money: you don't need a headless browser to read ratings.

Here's the shape of the block. The numbers are a snapshot from one fetch, since ratings move constantly:

{
  "@context": "https://schema.org",
  "@type": "Book",
  "name": "The Hunger Games",
  "image": "https://images.gr-assets.com/books/....jpg",
  "bookFormat": "Hardcover",
  "numberOfPages": 374,
  "inLanguage": "English",
  "isbn": "9780439023481",
  "author": [
    { "@type": "Person", "name": "Suzanne Collins",
      "url": "https://www.goodreads.com/author/show/153394.Suzanne_Collins" }
  ],
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": 4.34,
    "ratingCount": 9400000,
    "reviewCount": 235000
  }
}

Note the two count fields, because people confuse them constantly. ratingCount is how many people tapped a star score. reviewCount is the much smaller number who wrote text. On a popular title that gap is enormous, often 30 to 1, and the distinction matters if you're computing engagement or sampling reviews. Parsing this is a few lines:

import json
from bs4 import BeautifulSoup

def as_list(value):
    if value is None:
        return []
    return value if isinstance(value, list) else [value]

def parse_jsonld(html):
    soup = BeautifulSoup(html, "html.parser")
    block = None
    for tag in soup.find_all("script", {"type": "application/ld+json"}):
        data = json.loads(tag.string)
        if data.get("@type") == "Book":
            block = data
            break
    if not block:
        return {}
    rating = block.get("aggregateRating") or {}
    return {
        "title": block.get("name"),
        "format": block.get("bookFormat"),
        "pages": block.get("numberOfPages"),
        "language": block.get("inLanguage"),
        "isbn": block.get("isbn"),
        "authors": [a.get("name") for a in as_list(block.get("author"))],
        "rating": rating.get("ratingValue"),
        "rating_count": rating.get("ratingCount"),
        "review_count": rating.get("reviewCount"),
    }

The loop matters: a Goodreads page can carry more than one JSON-LD block (a BreadcrumbList sits alongside the Book), so filter on @type rather than grabbing the first one. This handles the headline fields for title, ratings, and edition metadata in one shot, with no selector maintenance. What it does not give you is the star histogram or the reviews, which live one level deeper.


Fetching Goodreads reliably with the SparkProxy Scraping API

Reading JSON-LD is easy once you have the HTML. Getting that HTML at volume, from an Amazon-owned property that rate-limits by IP, is the hard part. The SparkProxy Scraping API takes a target URL and handles the proxy, rotation, CAPTCHA, and optional rendering. Base URL https://scrape.sparkproxy.io/api/v1, auth through the X-API-Key header.

A basic fetch:

curl "https://scrape.sparkproxy.io/api/v1?url=https://www.goodreads.com/book/show/2767052&render_js=false&country_code=us" \
  -H "X-API-Key: YOUR_API_KEY"

The same call in Python, wrapped so the rest of the guide can reuse it:

import requests

def fetch(url, render_js="false"):
    resp = requests.get(
        "https://scrape.sparkproxy.io/api/v1",
        headers={"X-API-Key": "YOUR_API_KEY"},
        params={
            "url": url,
            "render_js": render_js,   # JSON-LD is in raw HTML, so keep this off
            "country_code": "us",     # Goodreads localizes buy links and some fields
        },
        timeout=90,
    )
    resp.raise_for_status()
    return resp.text

html = fetch("https://www.goodreads.com/book/show/2767052")
book = parse_jsonld(html)
print(book["title"], book["rating"], book["rating_count"])

Watch render_js="false". The parameter defaults to true, and on the SparkProxy credit model a non-rendered request is 1 credit versus 5 for a rendered one. Because the JSON-LD and the __NEXT_DATA__ blob are both baked into the initial HTML, you pay the cheap rate for almost everything and only turn rendering on for the rare page that needs it. Across tens of thousands of books, that is the difference between a sensible bill and a silly one.

If you'd rather not touch HTML at all, the API's extract_rules parameter runs CSS selectors server-side:

import json

params = {
    "url": "https://www.goodreads.com/book/show/2767052",
    "render_js": "false",
    "extract_rules": json.dumps({
        "title": "h1[data-testid='bookTitle']",
        "rating": "div.RatingStatistics__rating",
    }),
}

One caveat, and it's the reason JSON-LD wins on the book page: Goodreads' redesigned pages are a React app whose class names are hashed and rotate between builds, so selector-based extraction needs babysitting. The JSON-LD stays consistent because Goodreads keeps it stable for Google. Prefer the structured block, and reach for selectors only on the older server-rendered pages (lists, shelves, author) where the markup is steadier.


Going deeper: __NEXT_DATA__ and the Apollo GraphQL backend

The modern book page is a Next.js application, which means it ships a