Goodreads 是网络上最大的图书数据公开目录之一,每个公开书籍页面都承载着驱动趋势分析、推荐引擎和市场研究的读者信号:书名、作者、平均评分、评分人数,以及公开评论的正文。问题在于 Goodreads 在客户端渲染大部分内容并异步加载评论,因此普通 HTTP 请求返回的只是一个薄薄的 shell,而非你所需要的评分和评论文本。

本指南演示如何以可靠的方式用 Python 抓取 Goodreads 评分和评论。你将构建一个小型可运行的爬虫,通过 Crawling API 获取渲染后的书籍页面,用 BeautifulSoup 解析所需字段,并输出整洁的结构化记录。整个流程仅限于公开书籍和评论数据,结尾处的合法性章节并非走过场,请在大量抓取前认真阅读。

你将构建什么

一个 Python 脚本:接受一个公开的 Goodreads 书籍 URL,通过 Crawling API 获取渲染后的 HTML,并提取书籍及其可见评论的结构化记录。我们以一本知名公开书目作为示例,提取以下字段:

  • 书名 书籍的名称,例如"The Great Gatsby"。
  • 作者 书籍页面上标注的作者。
  • 平均评分 Goodreads 根据用户评分计算的综合分数。
  • 评分数量 已为该书评分的人数。
  • 评论 页面上显示的公开评论文本,以及评论者的显示名称。

为什么普通请求在 Goodreads 上会失败

如果用裸 HTTP 客户端请求 Goodreads 书籍 URL,你会得到状态码 200 的响应,但正文中几乎没有任何评论内容。有两个原因。第一,Goodreads 通过 JavaScript 在浏览器中渲染大部分评分和评论区域,因此初始 HTML 是一个 shell,只有页面脚本运行后才会填充内容。第二,评论列表异步加载并随交互增长,因此即使是部分渲染也可能遗漏你想要的数据,除非你给页面足够的时间稳定下来。

因此,一个可用的 Goodreads 爬虫在单次请求中需要两件事:一个实际渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自己搭建无头浏览器加上轮换住宅代理池,但将这两者整合并保持正常运行占了大部分工作量。Crawling API 将两者合并为一次调用:你把 URL 和 JavaScript token 一起发给它,它在受信任的 IP 后面渲染页面,并返回供你解析的完整 HTML。

为什么需要 JS token

Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 会先在真实浏览器中渲染页面。Goodreads 在客户端加载其评分和评论,因此这里需要 JS token。使用普通 token 返回的 shell 与普通请求相同,几乎没有什么可解析的价值。

前提条件

在编写任何代码之前,你需要准备好以下几项。都不会花太长时间。

基础 Python 知识。 你应该能够编写和运行 Python 脚本,并使用 pip 安装包。如果你不熟悉 HTML 解析,我们的如何在 Python 中使用 BeautifulSoup 入门指南涵盖了本教程所依赖的选择器基础知识。

Python 3.8 或更高版本。python --version 确认版本。如果没有,从 python.org 安装,或通过 Anaconda 等发行版安装。

Crawlbase 账号和 JS token。 注册后打开控制台,从账号文档页面复制你的 JavaScript(JS)token。请像对待密码一样保管 token:它用于验证你的请求,不要放入版本控制。

搭建项目

创建虚拟环境以隔离项目依赖,然后安装爬虫所需的两个库。

bash
python --version

python -m venv goodreads_env
source goodreads_env/bin/activate

pip install crawlbase beautifulsoup4

在 Windows 上,用 goodreads_env\Scripts\activate 替换 source 那行来激活环境。两个依赖各司其职:crawlbase 是 Crawling API 的官方客户端,beautifulsoup4 解析返回的 HTML,让你通过 CSS 选择器提取各个字段。

第一步:获取渲染后的书籍页面

先获取完整页面。导入 CrawlingAPI 类,用你的 JS token 初始化它,然后请求书籍 URL。在解析之前检查状态,可以让失败信息清晰可见。

python
from crawlbase import CrawlingAPI

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

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = "https://www.goodreads.com/book/show/4671.The_Great_Gatsby"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

两个等待选项对这类客户端渲染目标非常重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后再等待固定毫秒数,让延迟渲染的元素在页面被捕获前出现。5 秒是一个合理的起点;如果评论字段仍为空,可以增大该值。用 python scraper.py 运行脚本,你应该能看到真实的书籍标记,而不是普通请求返回的薄薄 shell。这能在你编写第一个选择器之前确认渲染正常工作。

Crawlbase Crawling API

Goodreads 需要在受信任 IP 后面渲染页面,通过一次调用完成。Crawling API 接受 JS token,在真实浏览器中运行页面,在服务器端轮换住宅 IP,并将完整 HTML 交给你,省去了自己运行无头浏览器队列和代理池的麻烦。先用免费套餐指向一个公开书籍页面试试。

第二步:用 BeautifulSoup 解析书籍字段

拿到渲染后的 HTML,将其加载到 BeautifulSoup 并通过选择器提取每个字段。Goodreads 书籍页面的核心详情布局具有可预测的结构,你可以将书名、作者、平均评分和评分数量映射到各自的选择器,然后遍历评论卡片以收集公开评论文本。在元素缺失时返回 None 的辅助函数确保单个字段缺失不会导致整个运行崩溃。

python
from bs4 import BeautifulSoup

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_book(html):
    soup = BeautifulSoup(html, "html.parser")

    rating_el = soup.select_one("div.RatingStatistics span.RatingStars")
    average_rating = rating_el["aria-label"] if rating_el else None

    reviews = []
    for card in soup.select("div.ReviewsList article.ReviewCard"):
        reviews.append({
            "user": text_of(card, 'div[data-testid="name"]'),
            "review": text_of(card, "section.ReviewText span.Formatted"),
        })

    return {
        "title": text_of(soup, 'h1.H1Title a[data-testid="title"]'),
        "author": text_of(soup, "span.ContributorLink__name"),
        "average_rating": average_rating,
        "ratings_count": text_of(soup, 'span[data-testid="ratingsCount"]'),
        "reviews": reviews,
    }

text_of 辅助函数同时完成两件事:在给定节点内查询单个元素,并在元素缺失时返回 None,而不是对空对象调用 .get_text() 抛出异常。显式传递节点在这里很重要,因为每张评论卡片都有自己的范围,你希望从该卡片中读取评论者名称和评论文本,而不是从整个页面的第一个匹配结果中读取。平均评分从 aria-label 属性读取,而非可见文本,因此单独处理。

选择器会发生变化

Goodreads 的类名(RatingStarsReviewCard 标记、data-testid 属性和区域包装器)会不定期变化。请将上述选择器视为起始模板,而非固定合约。当某个字段返回 None 时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作,并非哪里出错了的信号。

第三步:加载更多评论

首次渲染只显示评论的顶部片段。Goodreads 通过"显示更多评论"按钮而非编号分页器来展示其余评论,因此要获取更深层的评论文本,需要在捕获页面之前点击该按钮。Crawling API 提供了 css_click_selector 选项,在渲染过程中点击匹配的元素,让你在同一次请求中获取更大量的评论集合。

python
def crawl_with_more_reviews(page_url):
    options = {
        "ajax_wait": "true",
        "page_wait": 5000,
        "css_click_selector": 'button:has(span[data-testid="loadMore"])',
    }
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

选择器针对包裹加载更多控件的按钮。点击一次会在 HTML 被捕获之前展开可见评论列表,同一个 scrape_book 解析器随后无需任何修改就能看到更多卡片。如果你需要一次点击获得的评论还不够多,可以增大 page_wait 以给展开后的列表更多渲染时间。关于交互驱动内容为何如此运作的背景,请参阅我们的如何爬取 JavaScript 网站指南。

第四步:整合代码

现在将获取和解析整合为一个可运行的脚本。获取带有点击加载更多的渲染 HTML,交给解析器,然后将结构化记录写入 JSON 文件。

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

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

def crawl(page_url):
    options = {
        "ajax_wait": "true",
        "page_wait": 5000,
        "css_click_selector": 'button:has(span[data-testid="loadMore"])',
    }
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_book(html):
    soup = BeautifulSoup(html, "html.parser")
    rating_el = soup.select_one("div.RatingStatistics span.RatingStars")
    average_rating = rating_el["aria-label"] if rating_el else None

    reviews = []
    for card in soup.select("div.ReviewsList article.ReviewCard"):
        reviews.append({
            "user": text_of(card, 'div[data-testid="name"]'),
            "review": text_of(card, "section.ReviewText span.Formatted"),
        })

    return {
        "title": text_of(soup, 'h1.H1Title a[data-testid="title"]'),
        "author": text_of(soup, "span.ContributorLink__name"),
        "average_rating": average_rating,
        "ratings_count": text_of(soup, 'span[data-testid="ratingsCount"]'),
        "reviews": reviews,
    }

def main():
    page_url = "https://www.goodreads.com/book/show/4671.The_Great_Gatsby"
    html = crawl(page_url)
    if not html:
        return
    data = scrape_book(html)
    with open("goodreads_book.json", "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(json.dumps(data, indent=2, ensure_ascii=False)[:600])

if __name__ == "__main__":
    main()

输出示例

python scraper.py 运行完整脚本,你会得到该书籍的整洁结构化记录,可直接写入 JSON、CSV 或数据库。

json
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "average_rating": "Rating 3.93 out of 5",
  "ratings_count": "5,432,109 ratings",
  "reviews": [
    {
      "user": "Alex",
      "review": "Charms you with some of the most elegant English prose ever published."
    },
    {
      "user": "Inge",
      "review": "There was one thing I really liked about The Great Gatsby. It was short."
    }
  ]
}

扩展到多本书

单本书只是演示;实际任务需要处理书目列表。结构保持不变:维护一个书籍 URL 列表,通过 Crawling API 逐一获取,用同一个函数解析,然后收集结果。由于每个书籍页面共享相同的结构,你已经写好的解析器无需修改即可适用所有页面。

python
import time

books = [
    "https://www.goodreads.com/book/show/4671.The_Great_Gatsby",
    "https://www.goodreads.com/book/show/5470.1984",
]

results = []
for url in books:
    html = crawl(url)
    if html:
        results.append(scrape_book(html))
    time.sleep(2)

with open("goodreads_books.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

请求之间的 time.sleep(2) 控制循环节奏,避免背靠背地抓取书籍页面。要大规模收集书籍 URL,你可以用同样的获取+解析模式抓取 Goodreads 公开列表和书架页面,收集书籍链接后再逐一访问。只需保持合理的请求量,并遵守下面介绍的速率限制。

保持不被封锁

即使渲染已处理妥当,Goodreads 仍会监测爬虫特征的流量。养成几个好习惯能让运行保持健康,这些习惯同样适用于任何大型公开目标。

  • 控制请求频率。 在紧密循环中频繁请求书籍页面是最快被限速的方式。分散请求并变换目标,而不是以全速爬取一条路径。
  • 依赖 IP 轮换。 住宅 IP 池将请求分散到众多真实用户地址,使单个 IP 不会触发速率限制。Crawling API 为你处理这一切;如果你自己搭建,这部分是关键。
  • 关注状态码。 运行期间开始返回挑战或错误,说明当前频率或 IP 层级已不够用。将其视为需要减速的信号,而不是可以忽略的噪音。

更广泛的策略请参阅如何在不被封锁的情况下抓取网站以及网络抓取中如何绕过 CAPTCHA的深度剖析。如果你更倾向于通过轮换池路由自己的流量而非使用托管 API,Smart AI Proxy(也称 AI Proxy)以直连代理端点的形式提供同等的住宅 IP 轮换能力。

抓取 Goodreads 合法吗?

抓取 Goodreads 是否被允许,取决于 Goodreads 和 Amazon 的服务条款、你所在的司法管辖区以及数据的用途。Goodreads 是 Amazon 旗下的平台,其条款限制自动化访问,因此无论你的工具有多谨慎,抓取行为都可能违反这些条款。这里的代码不改变这一点,只是让技术部分得以运行。请阅读 Goodreads 服务条款及其 robots.txt,并将两者视为你采集内容的边界。

几条值得遵守的底线。只采集公开书籍和评论数据:任何人无需账号即可看到的书名、作者、平均评分、评分数量和评论文本。尊重 Goodreads 声明的速率预期,保持请求量足够低,不给其服务器造成压力。避免收集超出页面公开显示的评论者个人数据,不要从中构建可识别个人的档案。如果你计划将数据用于商业目的,请获得许可或授权来源,而不是将沉默视为默许。

关于 Goodreads 的一个实际说明:官方 Goodreads API 实际上已停止维护,不再接受新的申请密钥,因此不像某些平台那样有可回退的实时第一方数据源。这使得公开数据只剩下两个现实选择:像本文所示的那样抓取公开书籍页面,或通过授权提供商获取数据。本指南有意将范围限定在公开书籍页面内容,因为这是让工作具有可辩护性的边界。本指南不涵盖任何需要登录的内容、用户的私人书架或账号数据、超出公开显示的评论者个人数据,或任何绕过身份验证的尝试。如果你的项目需要超出公开书籍和评论数据的内容,授权数据来源才是正确的途径,而不是更聪明的爬虫。

回顾

核心要点

  • Goodreads 是客户端渲染的。 普通请求返回薄薄的 shell,因此必须先渲染再解析评分和评论。
  • 你需要同时具备渲染能力和受信任的 IP。 带 JS token 的 Crawling API 通过一次调用同时完成;ajax_waitpage_wait 控制等待内容的时长。
  • 评论隐藏在按钮后面。 使用 css_click_selector 在渲染过程中展开评论列表,让同一个解析器看到更多卡片。
  • BeautifulSoup 负责提取。 将书名、作者、平均评分、评分数量和评论文本映射到当前选择器,并预期这些选择器会发生变化。
  • 坚守公开数据。 尊重 Goodreads 和 Amazon 的服务条款及 robots.txt,对于批量或商业用途优先选择授权来源,绝不触碰账号、私人书架或评论者个人数据。

常见问题

为什么普通请求从 Goodreads 返回不到评论?

因为 Goodreads 通过 JavaScript 在客户端渲染其评分和评论内容,并异步加载评论列表。初始 HTML 是一个 shell,只有页面脚本在浏览器中运行后才会填充内容,因此原始 HTTP 请求返回状态码 200 但评论字段为空。要获取真实数据,必须先渲染页面,这正是 Crawling API 的 JS token 为你处理的事情。

Goodreads 需要普通 token 还是 JS token?

需要 JS token。普通 token 获取静态 HTML,在 Goodreads 上这与普通请求返回的薄薄 shell 相同。JS token 在交还 HTML 之前先在真实浏览器中渲染页面,因此当 BeautifulSoup 解析时,评分和评论文本都已存在。

如何加载超过前几条的评论?

Goodreads 通过"显示更多评论"按钮而非编号分页器来展示额外评论。向 Crawling API 传递针对该按钮的 css_click_selector 选项,它会在渲染过程中被点击,使捕获的 HTML 包含展开后的列表。如果你需要新显示的评论在捕获前完成渲染,可以增大 page_wait

我的选择器返回 None。是什么变了?

几乎可以肯定是 Goodreads 的标记发生了变化。其 RatingStarsReviewCard 类名、data-testid 属性和区域包装器会不定期变化,因此上个月有效的选择器可能会失效。在浏览器开发者工具中重新检查实时书籍页面并更新选择器。定期维护选择器是任何生产爬虫的正常工作。

我可以使用官方 Goodreads API 吗?

实际上不行。官方 Goodreads API 已停止维护,不再接受新的申请密钥,因此没有可依赖的实时第一方数据源。对于公开数据,现实的选择是像本指南所示的那样抓取公开书籍页面,或通过授权提供商获取数据。无论哪种方式,都要遵守服务条款、robots.txt 和速率限制。

如何避免在抓取 Goodreads 时被封锁?

保持每个 IP 的请求频率较低,变换目标而不是循环同一路径,并通过轮换住宅 IP 路由,使单个地址不会触发速率限制。Crawling API 为你管理轮换和受信任的 IP 池;如果你自己搭建,这部分是关键所在。关注状态码,当开始出现挑战时及时退让。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量