如何用 Python 抓取 Goodreads(告别空结果)

最后更新于 April 16, 2026

最让人心累的,不是写几十个小时代码,而是你辛苦写了 30 行 Python,结果一跑只有 []

空空的列表。啥都没有。Terminal里面只剩一个闪烁的光标。

这种场景我见得太多了。Thunderbit 内部测试里有,开发者论坛里有,GitHub 那些年久失修的爬虫仓库里也全是。大家的吐槽几乎一模一样:“热门评论区是空的,只返回 []“不管我跑第几页,抓到的永远是第一页”“去年还能用,现在彻底坏了”。更糟的是,Goodreads API 已经在 2020 年 12 月停用,所以老教程里那句“直接用 API 就行”基本已经走进死胡同。

如果你现在要从 Goodreads 拿结构化图书数据(书名、作者、评分、评论、分类、ISBN 等),网页抓取就是主路径。这篇会给你一套能跑通的 Python 方案,覆盖 JS 渲染、分页、反封锁和导出。如果你不想写代码,后面我也给你一个无代码方案,基本两步就能完成。

先说结论:Goodreads 抓取,本质是“把图书数据自动化搬出来”

Goodreads 抓取,指的是不用手动复制粘贴,而是通过代码自动从 Goodreads 网页中提取图书数据,比如书名、作者、评分、评论数、分类、ISBN、页数、出版日期等等。

Goodreads 是全球最大的图书数据库之一,拥有超过 ,以及大约 。每个月都有超过 1800 万本书被加入 “Want to Read(想读)” 书架。正是这种持续更新、结构化的数据,让出版商、数据科学家、书商和研究人员一直离不开它。

Python 之所以是首选,很现实:大约 的抓取项目都在用它。它的库成熟(requests、BeautifulSoup、Selenium、Playwright、pandas),语法对新手友好,社区规模也非常庞大。

如果你从来没抓取过网页,Python 就是最适合入门的选择。

这事为什么值得做?看 3 个真实场景就懂了

在开始写代码之前,先想一个问题:到底是谁需要这些数据,他们会拿来做什么?

一些具体例子:UCSD Book Graph 是推荐系统研究中最常被引用的学术数据集之一,包含 ,全部来自公开可访问的 Goodreads 书架。多个 Kaggle 数据集(如 goodbooks-10k、Best Books Ever 等)也都来源于 Goodreads 抓取。而《Big Data and Society》在 2025 年的一项研究,系统整理了 ,用于分析赞助评论如何影响平台。

从商业角度看,Bright Data 甚至把预抓取的 Goodreads 数据集按每 1000 条记录 $0.50 起售——这足以说明这类数据确实有市场价值。

Goodreads API 早就下线了,现在主流只剩这几条路

如果你最近搜过 “Goodreads API”,大概率会碰到过时教程。2020 年 12 月 8 日,Goodreads 悄悄停止发放新的开发者 API key。没有博客公告,没有群发邮件——只有文档页上的一个小横幅,和一大批一头雾水的开发者。

goodreads-data-access-tools.webp

影响很快就来了。开发者 Kyle K 做了一个用于分享读书推荐的 Discord bot——“突然之间它就啪地一下不能用了。” 另一位开发者 Matthew Jones 在 Reddit r/Fantasy Stabby Awards 投票前一周失去了 API 访问权限,不得不临时退回 Google Forms。研究生 Elena Neacsu 的硕士论文项目也在开发中途被迫中断。

那现在还能怎么做?主流方案大致就这几类:

Open Library 是很好的补充,尤其适合 ISBN 查询和基础元数据。但如果你需要评分、评论、分类标签或 “Want to Read” 数量,就得直接抓 Goodreads——要么用 Python,要么用像 Thunderbit 这样的工具。Thunderbit 可以抓取 Goodreads 页面(包括图书详情子页面),自动推荐字段,并直接导出到 Google Sheets、Notion 或 Airtable。

你总抓到空结果,不是你菜:常见原因和修法都在这

这部分是我刚开始接触 Goodreads 数据时最希望有人写出来的。“空结果” 是开发者论坛里最常见的抱怨,而且原因并不止一种——每种情况都有对应的修复方式。

JS 渲染内容:评论和评分显示为空

Goodreads 使用的是 React 前端。当你对图书页发起 requests.get() 时,拿到的只是初始 HTML——但评论、评分分布和很多 “更多信息” 区块其实是通过 JavaScript 异步加载的。你的爬虫根本看不到它们。

解决办法:只要页面内容依赖 JS 渲染,就改用 Selenium 或 Playwright。对于新项目,我更推荐 Playwright——它基于 WebSocket 协议,速度比 Selenium 快 ,而且自带更好的隐身能力和异步支持。

troubleshooting-empty-array-causes.webp

分页总是只返回第一页

这个坑非常隐蔽。你写了循环,也加了 ?page=N,但每次结果都一样。在 Goodreads 上,如果你没有登录,某些书架页会无声地返回第一页内容,不管你传什么 ?page= 参数。没有报错,也不会跳转——就是同一页反复出现。

修法很明确:带上认证会话 Cookie,重点是 _session_id2。后面的分页部分会给完整写法。

代码去年能跑,现在却不行了

Goodreads 会周期性调整 HTML class 名称和页面结构。GitHub 上很流行的 maria-antoniak/goodreads-scraper 仓库现在直接挂着永久提示:“This project is unmaintained and no longer functioning.” 解决办法是使用更稳定的选择器——比如 JSON-LD 结构化数据(遵循 schema.org 标准,变化很少),或者 data-testid 属性,而不是脆弱的 class 名称。

403 错误或被封禁

Python 的 requests 库和 Chrome 的 TLS 指纹并不一样。即使你伪装成 Chrome User-Agent,像 AWS WAF 这样的反爬系统(Goodreads 作为亚马逊子公司也在用)仍然可能识别出差异。解决办法:添加更真实的浏览器请求头,每次请求之间加入 3 - 8 秒的 time.sleep() 延迟;如果你要大规模抓取,还可以考虑用 curl_cffi 来匹配 TLS 指纹。

书架页和列表页出现登录墙

某些 Goodreads 书架页和列表页需要登录才能访问完整内容,尤其是第 5 页之后。你可以从浏览器导出 _session_id2 Cookie(用 Cookie 导出扩展,或者在 Chrome DevTools > Application > Cookies 中查看),然后写进请求中:

1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")
4# 之后用 session.get() 替代 requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)

Thunderbit 天然支持这类场景,因为它是在你自己的已登录 Chrome 浏览器里运行。

开跑前,先把这几件事准备好

  • 难度: 中级(默认你已掌握基础 Python)
  • 所需时间: 完整流程约 20 - 30 分钟
  • 你需要准备:
  • Python 3.8+
  • Chrome 浏览器(用于 DevTools 检查以及 Selenium/Playwright)
  • 依赖库:requestsbeautifulsoup4seleniumplaywrightpandas
  • (可选)gspread,用于导出到 Google Sheets
  • (可选) 作为无代码替代方案

goodreads-scraping-flow.webp

第 1 步:把环境一次装对

先安装所需库。打开终端并运行:

1pip install requests beautifulsoup4 selenium pandas lxml

如果你更喜欢 Playwright(新项目推荐):

1pip install playwright
2playwright install chromium

如果需要导出到 Google Sheets(可选):

1pip install gspread oauth2client

确保你的 Python 版本是 3.8 或更高。可以用 python --version 检查。

安装完成后,你应该可以无报错地导入这些库。可以试试 python -c "import requests, bs4, pandas; print('Ready')" 来确认。

第 2 步:先发一个“像真人”的请求

先在浏览器中打开 Goodreads 的某个分类书架或列表页——例如 https://www.goodreads.com/list/show/1.Best_Books_Ever。现在我们用 Python 抓取这个页面。

1import requests
2from bs4 import BeautifulSoup
3headers = {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6 "Accept-Language": "en-US,en;q=0.9",
7 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8}
9url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
10response = requests.get(url, headers=headers, timeout=15)
11print(f"Status: {response.status_code}")

你应该会看到 Status: 200。如果返回 403,先检查请求头——Goodreads 使用的 AWS WAF 会校验是否像真实浏览器请求,太简陋的请求会被拦下。上面的请求头就是在模拟真实 Chrome 浏览器会话。

第 3 步:先看 DOM,再写选择器

在 Goodreads 列表页里打开 Chrome DevTools(F12),右键点击一本书的标题,选择 “Inspect(检查)”。你会看到每个书目条目的 DOM 结构。

对于列表页,每本书通常包在一个 元素里,并带有 itemtype="http://schema.org/Book"。在里面你能找到:

  • 书名: a.bookTitle(链接文本就是书名,href 是书籍 URL)
  • 作者: a.authorName
  • 评分: span.minirating(包含平均评分和评分数量)
  • 封面图: 书目行内部的 img

而对于单本图书详情页,与其依赖 CSS 选择器,不如直接读取 JSON-LD。Goodreads 会把结构化数据嵌在 标签里,格式遵循 schema.org Book 标准。它比 class 名称稳定得多,因为 Goodreads 改前端时经常会动这些类名。

第 4 步:先把列表页数据拿下来

接下来我们解析列表页,并提取每本书的基本信息:

1import requests
2from bs4 import BeautifulSoup
3headers = {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6 "Accept-Language": "en-US,en;q=0.9",
7}
8url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
9response = requests.get(url, headers=headers, timeout=15)
10soup = BeautifulSoup(response.text, "lxml")
11books = []
12rows = soup.select('tr[itemtype="http://schema.org/Book"]')
13for row in rows:
14 title_tag = row.select_one("a.bookTitle")
15 author_tag = row.select_one("a.authorName")
16 rating_tag = row.select_one("span.minirating")
17 title = title_tag.get_text(strip=True) if title_tag else ""
18 book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
19 author = author_tag.get_text(strip=True) if author_tag else ""
20 rating_text = rating_tag.get_text(strip=True) if rating_tag else ""
21 books.append({
22 "title": title,
23 "author": author,
24 "rating_info": rating_text,
25 "book_url": book_url,
26 })
27print(f"Found {len(books)} books on page 1")
28for b in books[:3]:
29 print(b)

你应该会看到第一页大约有 100 本书。每条记录会包含书名、作者、类似 “4.28 avg rating — 9,031,257 ratings” 的评分字符串,以及该书详情页链接。

第 5 步:进详情页,把关键字段补齐

列表页只能拿到基础信息,而真正有价值的内容——ISBN、完整简介、分类标签、页数、出版日期——都在每本书自己的详情页里。这时候 JSON-LD 就派上用场了。

1import json
2import time
3def scrape_book_detail(book_url, headers):
4 """访问单本图书页面,并通过 JSON-LD 提取详细元数据。"""
5 resp = requests.get(book_url, headers=headers, timeout=15)
6 if resp.status_code != 200:
7 return {}
8 soup = BeautifulSoup(resp.text, "lxml")
9 script = soup.find("script", {"type": "application/ld+json"})
10 if not script:
11 return {}
12 data = json.loads(script.string)
13 agg = data.get("aggregateRating", {})
14 # 分类标签不在 JSON-LD 中,使用 HTML 作为补充
15 genres = [g.get_text(strip=True) for g in soup.select('span.BookPageMetadataSection__genreButton a span')]
16 return {
17 "isbn": data.get("isbn", ""),
18 "pages": data.get("numberOfPages", ""),
19 "language": data.get("inLanguage", ""),
20 "format": data.get("bookFormat", ""),
21 "avg_rating": agg.get("ratingValue", ""),
22 "rating_count": agg.get("ratingCount", ""),
23 "review_count": agg.get("reviewCount", ""),
24 "description": data.get("description", "")[:200], # 截断作预览
25 "genres": ", ".join(genres[:5]),
26 }
27# 示例:给前 3 本书补充详情
28for book in books[:3]:
29 details = scrape_book_detail(book["book_url"], headers)
30 book.update(details)
31 print(f"Scraped: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
32 time.sleep(4) # 尊重限速

每次请求之间加入 3 - 8 秒的 time.sleep()。Goodreads 的限速大约在单个 IP 每分钟 20 - 30 次请求左右,更快的话就会开始出现 403 或验证码。

这种“两段式”流程——先收集列表页中的所有书籍 URL,再逐个访问详情页——更稳,也更适合中途断点恢复。大多数成功的 Goodreads 爬虫都是这个思路。

补充说明: 可以通过子页面抓取自动完成这件事。AI 会自动访问每本书的详情页,并把 ISBN、简介、分类等信息补充到表格中——无需代码、无需循环、无需 sleep。

第 6 步:遇到 JS 渲染,直接上 Selenium / Playwright

如果你需要的内容是通过 JavaScript 加载的——比如评论、评分分布、“更多详情”区块——就必须用浏览器自动化工具。下面是一个 Selenium 示例:

1from selenium import webdriver
2from selenium.webdriver.chrome.options import Options
3from selenium.webdriver.common.by import By
4from selenium.webdriver.support.ui import WebDriverWait
5from selenium.webdriver.support import expected_conditions as EC
6options = Options()
7options.add_argument("--headless=new")
8options.add_argument("--disable-blink-features=AutomationControlled")
9options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
10 "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
11driver = webdriver.Chrome(options=options)
12driver.get("https://www.goodreads.com/book/show/5907.The_Hobbit")
13# 等待评论加载
14try:
15 WebDriverWait(driver, 10).until(
16 EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17 )
18except:
19 print("评论没有加载出来——页面可能需要登录,或者 JS 超时了")
20# 现在解析完整渲染后的页面
21page_source = driver.page_source
22soup = BeautifulSoup(page_source, "lxml")
23reviews = soup.select("article.ReviewCard")
24for rev in reviews[:3]:
25 text = rev.select_one("span.Formatted")
26 stars = rev.select_one("span.RatingStars")
27 print(f"评分: {stars.get_text(strip=True) if stars else 'N/A'}")
28 print(f"评论: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29 print()
30driver.quit()

什么时候用 Selenium,什么时候用 requests:

  • requests + BeautifulSoup 处理图书元数据(JSON-LD)、列表页、书架页(第一页)、Choice Awards 数据
  • 用 Selenium 或 Playwright 处理评论、评分分布,以及任何在原始 HTML 中看不到的内容

对于新项目,Playwright 通常是更好的选择——更快、内存占用更低、默认隐身能力更强。不过在 Goodreads 相关示例上,Selenium 的社区更大,现成代码也更多。

分页这关最容易翻车,这套写法更稳

分页是 Goodreads 爬虫最常见的失败点,而且我几乎没见过哪篇竞品教程真正把它讲对。下面这套方法更靠谱。

Goodreads 分页 URL 的工作方式

Goodreads 大多数分页页面都使用简单的 ?page=N 参数:

  • 列表页: https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2
  • 书架页: https://www.goodreads.com/shelf/show/thriller?page=2
  • 搜索页: https://www.goodreads.com/search?q=fantasy&page=2

每个列表页通常显示 100 本书。书架页每页显示 50 本。

编写一个知道何时停止的分页循环

1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50): # 安全上限 50 页
5 url = f"{base_url}?page={page_num}"
6 resp = requests.get(url, headers=headers, timeout=15)
7 if resp.status_code != 200:
8 print(f"Page {page_num}: got status {resp.status_code}, stopping.")
9 break
10 soup = BeautifulSoup(resp.text, "lxml")
11 rows = soup.select('tr[itemtype="http://schema.org/Book"]')
12 if not rows:
13 print(f"Page {page_num}: no books found, reached the end.")
14 break
15 for row in rows:
16 title_tag = row.select_one("a.bookTitle")
17 author_tag = row.select_one("a.authorName")
18 title = title_tag.get_text(strip=True) if title_tag else ""
19 book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
20 author = author_tag.get_text(strip=True) if author_tag else ""
21 all_books.append({"title": title, "author": author, "book_url": book_url})
22 print(f"Page {page_num}: scraped {len(rows)} books (total: {len(all_books)})")
23 time.sleep(5) # 每页之间延迟 5 秒
24print(f"\nDone. Total books collected: {len(all_books)}")

你可以通过检查结果列表是否为空(是否没有 tr[itemtype="http://schema.org/Book"] 元素),或者查看是否没有 “next” 链接(a.next_page)来判断是否到达最后一页。

边界情况:第 5 页之后需要登录

这是最容易坑到人的地方:如果你没登录,有些 Goodreads 书架页和列表页在请求第 6 页及以后时,会悄悄返回第一页内容。没有报错,没有跳转,就是重复同一页数据。

解决办法是:从浏览器导出 _session_id2 Cookie(可用 Cookie 导出扩展,或在 Chrome DevTools > Application > Cookies 中查看),然后把它加到请求里:

1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")
4# 现在用 session.get() 替代 requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)

Thunderbit 原生支持点击分页和无限滚动分页,不需要写代码,也不用管理 Cookie。如果你的分页逻辑总是出问题,值得考虑一下它。

一份可直接跑的完整脚本(复制即用)

下面是完整整合版脚本。它处理了请求头、分页、通过 JSON-LD 抓取子页面、限速和 CSV 导出。我已经用 2025 年中期的 Goodreads 真实页面测试过。

1"""
2goodreads_scraper.py — 抓取 Goodreads 列表,支持分页和图书详情补全。
3用法:python goodreads_scraper.py
4输出:goodreads_books.csv
5"""
6import csv, json, time, requests
7from bs4 import BeautifulSoup
8HEADERS = {
9 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
10 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
11 "Accept-Language": "en-US,en;q=0.9",
12 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
13}
14BASE_URL = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
15MAX_PAGES = 3 # 可按需调整
16DELAY_LISTING = 5 # 列表页之间的秒数
17DELAY_DETAIL = 4 # 详情页之间的秒数
18OUTPUT_FILE = "goodreads_books.csv"
19def scrape_listing_page(url):
20 """返回一个列表页中的书籍字典列表,包含书名、作者和 book_url。"""
21 resp = requests.get(url, headers=HEADERS, timeout=15)
22 if resp.status_code != 200:
23 return []
24 soup = BeautifulSoup(resp.text, "lxml")
25 rows = soup.select('tr[itemtype="http://schema.org/Book"]')
26 books = []
27 for row in rows:
28 t = row.select_one("a.bookTitle")
29 a = row.select_one("a.authorName")
30 if t:
31 books.append({
32 "title": t.get_text(strip=True),
33 "author": a.get_text(strip=True) if a else "",
34 "book_url": "https://www.goodreads.com" + t["href"],
35 })
36 return books
37def scrape_book_detail(book_url):
38 """访问图书页,通过 JSON-LD + HTML 兜底提取元数据。"""
39 resp = requests.get(book_url, headers=HEADERS, timeout=15)
40 if resp.status_code != 200:
41 return {}
42 soup = BeautifulSoup(resp.text, "lxml")
43 script = soup.find("script", {"type": "application/ld+json"})
44 if not script:
45 return {}
46 data = json.loads(script.string)
47 agg = data.get("aggregateRating", {})
48 genres = [g.get_text(strip=True)
49 for g in soup.select("span.BookPageMetadataSection__genreButton a span")]
50 return {
51 "isbn": data.get("isbn", ""),
52 "pages": data.get("numberOfPages", ""),
53 "avg_rating": agg.get("ratingValue", ""),
54 "rating_count": agg.get("ratingCount", ""),
55 "review_count": agg.get("reviewCount", ""),
56 "description": (data.get("description", "") or "")[:300],
57 "genres": ", ".join(genres[:5]),
58 "language": data.get("inLanguage", ""),
59 "format": data.get("bookFormat", ""),
60 "published": data.get("datePublished", ""),
61 }
62def main():
63 all_books = []
64 # --- 第 1 轮:从列表页收集 book URL ---
65 for page in range(1, MAX_PAGES + 1):
66 url = f"{BASE_URL}?page={page}"
67 page_books = scrape_listing_page(url)
68 if not page_books:
69 print(f"Page {page}: empty — stopping pagination.")
70 break
71 all_books.extend(page_books)
72 print(f"Page {page}: {len(page_books)} books (total: {len(all_books)})")
73 time.sleep(DELAY_LISTING)
74 # --- 第 2 轮:逐本补全详情页数据 ---
75 for i, book in enumerate(all_books):
76 details = scrape_book_detail(book["book_url"])
77 book.update(details)
78 print(f"[{i+1}/{len(all_books)}] {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
79 time.sleep(DELAY_DETAIL)
80 # --- 导出 CSV ---
81 if all_books:
82 fieldnames = list(all_books[0].keys())
83 with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
84 writer = csv.DictWriter(f, fieldnames=fieldnames)
85 writer.writeheader()
86 writer.writerows(all_books)
87 print(f"\nSaved {len(all_books)} books to {OUTPUT_FILE}")
88 else:
89 print("No books scraped.")
90if __name__ == "__main__":
91 main()

MAX_PAGES = 3 设好之后,这个脚本会从 “Best Books Ever” 列表中抓取大约 300 本书,访问每本书的详情页,并把所有结果写入 CSV。在我的机器上,整个过程大约需要 25 分钟(主要耗在详情页之间的 4 秒延迟上)。输出的 CSV 会包含 title、author、book_url、isbn、pages、avg_rating、rating_count、review_count、description、genres、language、format 和 published 等字段。

不止 CSV:用 gspread 导出到 Google Sheets

如果你希望把数据放进 Google Sheets,而不是只保存成 CSV,可以在 CSV 导出后加上这段:

1import gspread
2from oauth2client.service_account import ServiceAccountCredentials
3scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
4creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", scope)
5client = gspread.authorize(creds)
6sheet = client.open("Goodreads Scrape").sheet1
7header = list(all_books[0].keys())
8sheet.append_row(header)
9for book in all_books:
10 sheet.append_row([str(book.get(k, "")) for k in header])
11print("Data pushed to Google Sheets.")

你需要一个启用了 Sheets 和 Drive API 的 Google Cloud service account。里大约 5 分钟就能走完配置流程。如果你要推送几百行以上的数据,建议使用批量操作(append_rows(),传入列表的列表),因为 Google 的限额是每个项目 60 秒内最多 300 次请求。

如果你不想折腾这些配置,Thunderbit 直接 导出到 Google Sheets、Airtable、Notion、Excel、CSV、JSON。省掉装库、配凭证、盯配额这三件烦心事。

不想写代码?两步也能把 Goodreads 抓下来

不是每个人都想维护 Python 脚本。也许你是出版商,只想做一次市场分析;或者你是书评博主,只想要一份当年畅销书的表格。这正是 Thunderbit 诞生的场景。

如何用 Thunderbit 抓 Goodreads

  1. 安装 Thunderbit Chrome 扩展,然后打开 Goodreads 的列表页、书架页或搜索结果页。
  2. 在 Thunderbit 侧边栏点击 “AI Suggest Fields(AI 建议字段)”。AI 会读取页面并推荐列名——通常包括书名、作者、评分、封面图片 URL 和图书链接。
  3. 点击 “Scrape”,几秒内就能把数据提取成结构化表格。
  4. 导出到 Google Sheets、Excel、Airtable、Notion、CSV 或 JSON。

如果你需要更详细的图书数据(ISBN、简介、分类、页数),Thunderbit 的子页面抓取功能会自动访问每本书的详情页,把信息补充到表格中——无需循环、无需 sleep、无需调试。

Thunderbit 还能原生处理分页列表。你只需要告诉它点击 “Next” 或滚动,它就会自动收集所有页面的数据,不需要写任何代码。

取舍很简单:Python 脚本给你完全控制权;Thunderbit 用少量灵活度,换来更快交付和几乎零维护。对于一个 300 本书的列表,Python 脚本的运行时间大约 25 分钟,还得加上你写代码和调试的时间;Thunderbit 只需两次点击,差不多 3 分钟就能拿到同样的数据。

最后别忽略:合规边界和抓取伦理

这个问题值得认真回答,而不是随口丢一句免责声明。

Goodreads 的 robots.txt 到底写了什么

Goodreads 当前的 robots.txt 其实写得很明确。图书详情页(/book/show/)、公开列表页(/list/show/)、公开书架页(/shelf/show/)和作者页(/author/show/)都没有被禁止。被禁止的路径包括:/api/book/reviews//review/list/review/show/search 等多个路径。GPTBot 和 CCBot(Common Crawl)则是直接被 Disallow: / 全面禁止。对 bingbot 还设置了 Crawl-delay: 5,但并没有全站统一延迟。

用大白话理解 Goodreads 的服务条款

他们的 ToS(最近一次修订是 2021 年 4 月 28 日)明确禁止“任何数据挖掘、机器人或类似的数据采集和提取工具”。这句话范围很广,确实需要认真对待——但法院一贯认为,仅仅违反 ToS 并不等同于刑事层面的“未授权访问”。 案明确指出:“把违反服务条款定为刑事犯罪,可能会让每个网站都变成自己的刑事管辖区。”

最佳实践

  • 加延迟: 每次请求之间间隔 3 - 8 秒(Goodreads 自己的 robots.txt 对 bot 建议 5 秒)
  • 单 IP 每天控制在 5,000 次请求以内
  • 只抓公开可访问页面——避免大规模抓取仅登录后可见的数据
  • 不要商业化再分发原始评论文本——评论属于受版权保护的创作内容
  • 只保存你真正需要的数据,并制定数据保留周期
  • 个人研究 vs. 商业用途: 为个人分析或学术研究抓取公开数据,一般更容易被接受;商业再分发则风险更高

像 Thunderbit 这种通过你自己的浏览器会话抓取的工具,交互过程在视觉上和正常浏览完全一致,但无论用什么工具,上述伦理原则都适用。如果你想进一步了解 ,我们也单独写过相关文章。

实战避坑清单(真的能省你很多时间)

技巧:先查 JSON-LD。 在写复杂 CSS 选择器之前,先看看你需要的数据是不是已经在 里了。它更稳定、解析更简单,也不容易在 Goodreads 更新前端时坏掉。

技巧:使用两段式策略。 先从列表页收集所有书籍 URL,再访问每个详情页。这样爬虫中途崩溃时更容易恢复,而且你还能把 URL 列表保存到磁盘作为检查点。

坑:忘记处理缺失字段。 并不是每本书页面都有 ISBN、分类标签或简介。记得总是用带默认值的 .get(),或者先判断选择器是否存在。一次 NoneType 错误就可能让 3 小时的抓取任务直接中断。

坑:跑太快。 我知道把 time.sleep(0.5) 设得很小很诱人,好像一下就能跑完。但 Goodreads 大约在 20 - 30 次连续请求后就会开始返回 403,一旦被标记,可能要等好几个小时,甚至换 IP 才行。4-5 秒的延迟才是比较稳的区间。

坑:轻信老教程。 如果某篇教程还在提 Goodreads API,或者用的是 .field.value#bookTitle 这类 class 名称,那它大概率已经过时了。搭建爬虫前,一定先去真实页面核对选择器。

如果你想进一步了解如何选择合适的抓取工具和框架,可以看看我们关于 的指南。

总结:你真正要记住的就这几条

用 Python 抓 Goodreads 完全可行——关键是知道哪些地方最容易踩坑。简单总结如下:

  • Goodreads API 已经没了(自 2020 年 12 月起)。现在获取平台结构化图书数据,抓取网页是主要方式。
  • 空结果 几乎总是由 JS 渲染内容、过时选择器、缺少请求头或分页认证问题造成的,而不是你的代码本身写错了。
  • JSON-LD 是图书元数据的最佳帮手。它稳定、结构清晰,而且很少变动。
  • 很多书架页和列表页在第 5 页之后需要认证。记得带上 _session_id2 Cookie。
  • 限速是真实存在的。每次请求之间保留 3 - 8 秒延迟,并将每天请求控制在 5,000 次以内。
  • 两段式策略(先收集 URL,再抓详情页)更稳,也更容易断点续跑。
  • 对于非程序员(或者任何想省下午时间的人), 可以把 JS 渲染、分页、子页面补全和导出这些事都搞定,基本两次点击就行。

负责任地抓取,尊重 robots.txt。希望你之后看到的结果,不再是 []

FAQ:把最常问的问题一次讲清楚

现在还能用 Goodreads API 吗?

不能。Goodreads 在 2020 年 12 月停用了公开 API,也不再发放新的开发者 key。原本不活跃 30 天的旧 key 也会自动失效。现在程序化访问图书数据,只能用网页抓取,或者改用 Open Library、Google Books 之类的替代 API。

为什么我的 Goodreads 爬虫总是返回空结果?

最常见的原因是 JavaScript 渲染内容。Goodreads 会用 React/JavaScript 加载评论、评分分布和很多详情区块,而简单的 requests.get() 看不到这些内容。对于这类页面,请改用 Selenium 或 Playwright。其他常见原因还包括:CSS 选择器过时(Goodreads 改了 HTML)、缺少 User-Agent 请求头(触发 403)、或者在未登录状态下抓取分页书架页。

抓 Goodreads 合法吗?

根据当前法律判例,抓取公开可访问数据用于个人或研究用途通常是可接受的(如 hiQ v. LinkedIn、Meta v. Bright Data)。不过,Goodreads 的服务条款禁止自动化数据采集,你也应该始终查看其 robots.txt。请避免商业化再分发受版权保护的评论文本,并控制请求量,不要给网站带来过大负担。

如何抓取 Goodreads 的多页内容?

在书架页或列表页 URL 后面加上 ?page=N,然后循环页码即可。通过检测结果是否为空,或是否没有 “next” 链接,来判断是否到了最后一页。注意:某些书架页需要认证(_session_id2 Cookie)才能返回第 5 页之后的数据——没有它,你会悄悄拿到重复的第一页数据。

不写代码能抓 Goodreads 吗?

可以。 是一个 Chrome 扩展,只要两步就能抓 Goodreads——AI 会建议数据字段,你点击 “Scrape” 后,就能直接导出到 Google Sheets、Excel、Airtable 或 Notion。它可以自动处理 JS 渲染内容、分页和子页面补全,不需要 Python,也不需要编程。

了解更多

Shuai Guan
Shuai Guan
Co-founder/CEO @ Thunderbit. Passionate about cross section of AI and Automation. He's a big advocate of automation and loves making it more accessible to everyone. Beyond tech, he channels his creativity through a passion for photography, capturing stories one picture at a time.
目录

试试 Thunderbit

只需 2 次点击即可抓取线索和其他数据。AI 驱动。

获取 Thunderbit 免费使用
使用 AI 提取数据
轻松将数据传输到 Google Sheets、Airtable 或 Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week