最讓人挫折的事之一,就是你辛辛苦苦寫了 30 行 Python,跑 Goodreads 爬蟲,結果卻只回傳 []。空清單。什麼都沒有。只剩你和閃爍的游標大眼瞪小眼。
這種狀況我看過太多次了——不管是在 Thunderbit 內部實驗、各大開發者論壇,還是在那些早就荒廢的爬蟲 GitHub 專案 issue 裡,抱怨幾乎都一模一樣:「熱門評論區是空的,只顯示 []」、「不管我跑哪一頁,抓到的永遠都是第一頁」、「我去年還能用,現在整個壞掉了」。更麻煩的是,Goodreads API 已經在 2020 年 12 月停用,所以老文章裡那句「直接用 API 就好」根本是死路一條。
如果你現在想從 Goodreads 抓結構化的書籍資料——像是書名、作者、評分、評論、分類、ISBN——那最可行的方法就是爬蟲。這篇指南會帶你完整走一遍如何用 Python 擷取 Goodreads 資料,包含 JS 動態渲染內容、分頁、反封鎖與匯出。當然,如果你不想寫 Python,我也會示範一個不用寫程式、兩下就能完成的替代方案。
什麼是 Goodreads 擷取?為什麼要用 Python?
Goodreads 擷取,指的是透過程式自動從 Goodreads 網頁中提取書籍資料——例如書名、作者、評分、評論數、分類、ISBN、頁數、出版日期等——而不是手動複製貼上。
Goodreads 是全球最大的書籍資料庫之一,擁有超過 ,以及大約 。每個月都有超過 1,800 萬本書被加入「Want to Read」清單。這種持續更新、結構清楚的資料,正是出版商、資料科學家、書商與研究人員一直回頭使用的原因。
Python 幾乎是這類工作的首選語言——大約支撐了 的爬蟲專案。相關函式庫成熟(requests、BeautifulSoup、Selenium、Playwright、pandas),語法對初學者友善,而且社群龐大。
如果你以前從來沒有爬過網站,Python 會是最適合入門的起點。
為什麼要用 Python 擷取 Goodreads?真實應用場景
在進入程式碼之前,先問一個問題:到底是誰需要這些資料?拿到之後又能做什麼?
| 應用場景 | 受益者 | 擷取內容 |
|---|---|---|
| 出版市場研究 | 出版社、文學經紀人 | 熱門分類、高評分書籍、新興作者、競爭對手評分 |
| 書籍推薦系統 | 資料科學家、業餘開發者、App 開發者 | 評分、分類、使用者書架、評論情緒 |
| 價格與庫存監控 | 電商書商 | 熱門書名、評論量、"Want to Read" 數量 |
| 學術研究 | 研究人員、學生 | 評論文字、評分分布、分類標註 |
| 閱讀分析 | 書評部落客、個人專案 | 個人書架資料、閱讀紀錄、年度閱讀統計 |
幾個具體例子:UCSD Book Graph——推薦系統研究中最常被引用的學術資料集之一——包含 ,資料來源就是公開可存取的 Goodreads 書架。多個 Kaggle 資料集(像是 goodbooks-10k、Best Books Ever 等)也都是從 Goodreads 擷取而來。而 2025 年發表在 Big Data and Society 的研究,則透過計算方式整理了 ,用來分析贊助評論如何影響平台內容。
在商業層面,Bright Data 甚至直接販售預先擷取好的 Goodreads 資料集,每 1,000 筆資料只要 $0.50 起——這本身就證明這類資料具有實際市場價值。
Goodreads API 已經沒了——現在有哪些替代方案?
如果你最近搜尋過「Goodreads API」,很可能會點進一篇過時的教學。2020 年 12 月 8 日,Goodreads 悄悄停止發放新的開發者 API 金鑰。沒有部落格公告,也沒有群發信件——只有文件頁面上的一小段提示,外加一票困惑的開發者。

後果很快就出現了。一位開發者 Kyle K 做了一個分享書籍推薦的 Discord 機器人——「突然之間,啪的一聲就不能用了。」 另一位 Matthew Jones 在 Reddit r/Fantasy Stabby Awards 投票前一週失去了 API 權限,只好緊急改回 Google Forms。還有一位研究生 Elena Neacsu 的碩士論文專案也在開發中途被迫中斷。
那現在還剩下什麼?目前的選項大致如下:
| 方法 | 可取得的資料 | 使用難度 | 速率限制 | 狀態 |
|---|---|---|---|---|
| Goodreads API | 完整中繼資料、評論 | 以前很簡單 | 1 req/sec | 已停用(2020 年 12 月)——不再發放新金鑰 |
| Open Library API | 書名、作者、ISBN、封面(約 3,000 萬本) | 簡單 | 1–3 req/sec | 仍可使用、免費、免授權 |
| Google Books API | 中繼資料、預覽 | 簡單 | 每日 1,000 次免費額度 | 仍可使用(非英文 ISBN 有缺口) |
| Python 擷取(requests + BS4) | 初始 HTML 裡看得到的任何內容 | 中等 | 自行管理 | 適用於靜態內容 |
| Python 擷取(Selenium/Playwright) | 也能抓 JS 動態渲染內容 | 較難 | 自行管理 | 需要抓評論、部分清單時必備 |
| Thunderbit(免寫碼 Chrome 擴充功能) | 頁面上可見的任何資料 | 非常簡單(兩下完成) | 依點數計費 | 正常運作——不需要 Python |
Open Library 是很好的補充,特別適合查 ISBN 和基本中繼資料。但如果你需要評分、評論、分類標籤,或是「Want to Read」數量,那你就得直接抓 Goodreads——不論是用 Python,還是像 Thunderbit 這樣的工具。Thunderbit 可以抓取 Goodreads 頁面(包含書籍詳細頁子頁面),並透過 AI 建議欄位,直接匯出到 Google Sheets、Notion 或 Airtable。
為什麼你的 Goodreads Python 爬蟲總是回傳空結果?怎麼修?
這一段是我當初開始接觸 Goodreads 資料時最希望有人先寫給我的。拿到空結果是開發者論壇裡最常見的抱怨,而它其實有好幾種不同原因——每一種都有對應的解法。
| 症狀 | 根本原因 | 解法 |
|---|---|---|
評論/評分回傳 [] | JS 動態渲染內容(React/延遲載入) | 改用 Selenium 或 Playwright,不要只用 requests |
| 永遠只抓到第 1 頁 | 分頁參數沒生效,或由 JS 控制 | 正確帶入 ?page=N;無限捲動則用瀏覽器自動化 |
| 程式去年可用,今年失效 | Goodreads 改了 HTML class 名稱 | 改用更穩定的選擇器(JSON-LD、data-testid 屬性) |
| 幾次請求後出現 403/被封鎖 | 缺少 headers/請求太快 | 加上 User-Agent、time.sleep()、輪換代理 IP |
| 書架/清單頁出現登入牆 | 需要 cookie/session | 用 requests.Session() 加 cookies,或改用瀏覽器爬取 |
JS 動態渲染內容:評論與評分都變成空白
Goodreads 使用的是 React 前端。當你對書籍頁面執行 requests.get() 時,拿到的只是初始 HTML——但評論、評分分布,以及許多「更多資訊」區塊,是透過 JavaScript 非同步載入的。你的爬蟲根本看不到它們。
解法是:只要你需要 JS 動態渲染內容,就改用 Selenium 或 Playwright。若是新專案,我會推薦 Playwright——因為它採用 WebSocket 通訊協定,速度比 Selenium 快 ,而且內建更好的隱匿能力與非同步支援。

分頁永遠只回傳第 1 頁
這個問題很陰險。你明明有寫迴圈、也有遞增 ?page=N,結果每次抓到的內容都一樣。Goodreads 的書架頁面在未登入時,會默默回傳第 1 頁內容,不管你傳入什麼 ?page= 參數。沒有錯誤,也不會轉址——只是不斷重複同一頁。
解法:加上已驗證的 session 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 函式庫,其 TLS 指紋與 Chrome 不同。即使你塞進 Chrome 的 User-Agent 字串,像 AWS WAF 這種反機器人系統(Goodreads 因為是 Amazon 旗下服務,也會用到)還是有機會辨識出差異。解法是:加入更像真實瀏覽器的 headers、每次請求間加上 3–8 秒的 time.sleep() 延遲,如果你要做大規模擷取,還可以考慮用 curl_cffi 來模擬 TLS 指紋。
書架頁與清單頁出現登入牆
有些 Goodreads 書架頁與清單頁需要登入才能看到完整內容,尤其是第 5 頁之後。你可以用 requests.Session() 搭配從瀏覽器匯出的 cookies,或直接使用已登入帳號的 Selenium/Playwright。Thunderbit 則更自然,因為它直接在你自己的已登入 Chrome 瀏覽器中運作。
開始前的準備
- 難度: 中階(假設你已具備基本 Python 知識)
- 所需時間: 約 20–30 分鐘可完成完整教學
- 你需要:
- Python 3.8 以上
- Chrome 瀏覽器(用於 DevTools 檢查與 Selenium/Playwright)
- 函式庫:
requests、beautifulsoup4、selenium或playwright、pandas - (選用)
gspread,用來匯出到 Google Sheets - (選用) 作為免寫碼替代方案

步驟 1:設定 Python 環境
先安裝需要的函式庫。打開終端機執行:
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:用正確的 Headers 發出第一個請求
先在瀏覽器打開 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,先檢查你的 headers——Goodreads 的 AWS WAF 會檢查是否像真實瀏覽器的 User-Agent,基本版請求很容易被擋。上面的 headers 其實就是在模擬真實 Chrome 瀏覽器。
步驟 3:檢查頁面並找出正確的選擇器
在 Goodreads 清單頁上打開 Chrome DevTools(F12),右鍵點一個書名,選擇「檢查」。你會看到每本書條目的 DOM 結構。
對清單頁來說,每本書通常包在一個 <tr> 元素中,且帶有 itemtype="http://schema.org/Book"。裡面通常可以找到:
- 書名:
a.bookTitle(連結文字就是書名,href則是書籍網址) - 作者:
a.authorName - 評分:
span.minirating(包含平均評分與評分數) - 封面圖: 書籍列中的
img
對單本書詳細頁來說,與其硬找 CSS 選擇器,不如直接讀 JSON-LD。Goodreads 會在 <script type="application/ld+json"> 標籤裡嵌入結構化資料,格式符合 schema.org 的 Book 標準。這比隨時可能被 Goodreads 改掉的 class 名稱穩定太多了。
步驟 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) # 尊重速率限制
每次請求之間加上 time.sleep(),建議 3–8 秒。Goodreads 的速率限制大約在單一 IP 每分鐘 20–30 次請求,太快就容易遇到 403 或 CAPTCHA。
這種兩段式做法——先從清單頁收集所有書籍網址,再逐一前往詳細頁補資料——通常更穩定,也更容易在中途中斷後繼續跑。大多數成功的 Goodreads 爬蟲都是這樣設計的。
補充: 也能自動完成這一步,透過子頁抓取功能逐一前往每本書的詳細頁,補齊 ISBN、簡介、分類等資料——不用寫程式、不用迴圈、也不用 sleep timer。
步驟 6:用 Selenium 處理 JavaScript 動態內容
如果你要抓的內容是透過 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"Rating: {stars.get_text(strip=True) if stars else 'N/A'}")
28 print(f"Review: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29 print()
30driver.quit()
什麼時候用 Selenium,什麼時候用 requests:
- 用
requests+ BeautifulSoup 來抓書籍中繼資料(JSON-LD)、清單頁、書架頁(第 1 頁)、以及 Choice Awards 資料 - 用 Selenium 或 Playwright 來抓評論、評分分布,以及任何原始 HTML 中看不到的內容
對新專案來說,Playwright 通常更值得選——速度更快、記憶體更省、預設的隱匿能力也更好。不過 Selenium 的社群更大,而且針對 Goodreads 已有較多現成範例。
真正可用的分頁方式:抓完整 Goodreads 清單
分頁是 Goodreads 爬蟲最常失敗的地方,而我幾乎沒看過哪篇教學真正把它講清楚。下面是正確做法。
Goodreads 的分頁網址怎麼運作?
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 頁以上時,會悄悄回傳第 1 頁內容。沒有錯誤,也不會轉址——就是一直重複同樣的資料。
要修正這個問題,請從瀏覽器匯出 _session_id2 cookie(可以用 cookie 匯出外掛,或在 Chrome DevTools > Application > Cookies 取得),然後加進你的 requests:
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。如果你的分頁邏輯一直出錯,真的很值得考慮它。
可直接複製使用的完整 Python 腳本
下面是完整整合版腳本。它會處理 headers、分頁、透過 JSON-LD 擷取子頁資料、速率限制,以及 CSV 匯出。我已經在 2025 年中期的 Goodreads 實際頁面上測試過。
1"""
2goodreads_scraper.py — 透過分頁與書籍詳細頁補充資料,擷取 Goodreads 清單。
3Usage: python goodreads_scraper.py
4Output: 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 """回傳單一清單頁的 title、author、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 fallback 擷取中繼資料。"""
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 # --- 第一段:從清單頁收集書籍網址 ---
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 # --- 第二段:逐本補齊詳細頁資料 ---
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() 搭配 list of lists),因為 Google 的速率限制是每個專案每 60 秒 300 次請求。
如果你覺得這些設定太麻煩,Thunderbit 可以一鍵匯出到 Google Sheets、Airtable、Notion、Excel、CSV 與 JSON——完全不用裝函式庫、也不用 credentials 檔案、更不用擔心 API 額度。
免寫碼替代方案:用 Thunderbit 擷取 Goodreads
不是每個人都想維護 Python 腳本。也許你是出版商,只需要做一次性的市場分析;又或者你是書評部落客,只想要一份今年暢銷書的表格。這正是我們打造 Thunderbit 的原因。
如何用 Thunderbit 擷取 Goodreads
- 從 安裝 Thunderbit Chrome 擴充功能,然後打開 Goodreads 的清單頁、書架頁或搜尋結果頁。
- 在 Thunderbit 側邊欄點擊「AI 建議欄位」。AI 會讀取頁面並推薦欄位——通常包含書名、作者、評分、封面圖片網址與書籍連結。
- 點擊「擷取」——資料會在幾秒內轉成結構化表格。
- 匯出到 Google Sheets、Excel、Airtable、Notion、CSV 或 JSON。
如果你要更完整的書籍資料(ISBN、簡介、分類、頁數),Thunderbit 的子頁抓取功能會自動逐一拜訪每本書的詳細頁並補齊欄位——不用迴圈、不用 sleep timer、也不用 debug。
Thunderbit 也原生支援分頁清單。你只要告訴它按「下一頁」或持續捲動,它就會把所有頁面的資料都收集起來,完全不用寫程式。
取捨其實很簡單:Python 腳本讓你保有完全控制權,且幾乎免費(但要花時間);Thunderbit 則用少許彈性換來大量省時與零維護。以 300 本書的清單來說,Python 腳本執行時間大約 25 分鐘,還不算你前面寫程式與除錯的時間。Thunderbit 只要兩下,大約 3 分鐘就能拿到同樣的資料。
負責任地擷取 Goodreads:robots.txt、服務條款與倫理
這一段值得直接說清楚,而不是只丟一句制式免責聲明。
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 服務條款
服務條款(最近一次修訂是 2021 年 4 月 28 日)明文禁止「任何形式的資料探勘、機器人或類似的資料蒐集與擷取工具」。這種語言範圍很廣,確實應該慎重看待——但法院一向認為,單純違反服務條款,不等於刑事上的「未經授權存取」。 的判決就指出:「把服務條款違規刑事化,等於讓每個網站都成為自己的刑事管轄區。」
最佳實務
- 請求間加延遲: 每次請求間隔 3–8 秒(Goodreads 自己的 robots.txt 對 bot 也建議 5 秒)
- 單一 IP 每日請求量盡量低於 5,000 次
- 只擷取公開可存取頁面——避免大量抓取僅限登入使用者的資料
- 不要商業性地散布原始評論文字——評論屬於受著作權保護的創作內容
- 只儲存真正需要的資料,並建立資料保留期限
- 個人研究 vs. 商業用途: 為個人分析或學術研究而擷取公開資料,通常較能被接受;若是商業再散布,法律風險會明顯提高。
使用像 Thunderbit 這樣透過你自己瀏覽器工作階段擷取的工具,視覺上與正常瀏覽幾乎一致,但不論你用什麼工具,仍然應遵守相同的倫理原則。如果你想更深入了解 ,我們也有另外整理文章。
提示與常見陷阱
提示:先看 JSON-LD。 在寫複雜的 CSS 選擇器之前,先檢查你要的資料是否已經在 <script type="application/ld+json"> 標籤裡。它更穩定、更容易解析,也比較不會因為 Goodreads 更新前端而壞掉。
提示:使用兩段式策略。 先從清單頁收集所有書籍網址,再逐一前往詳細頁。這樣你的爬蟲中途掛掉也比較容易續跑,而且可以先把 URL 清單存到磁碟當作檢查點。
陷阱:忘記處理缺失欄位。 不是每本書都有 ISBN、分類標籤或簡介。務必使用帶預設值的 .get(),或在選擇器外層加上 if 判斷。一次 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 動態渲染、過時的選擇器、缺少 headers,或分頁驗證問題造成。
- JSON-LD 是擷取書籍中繼資料最好的朋友。它穩定、結構清楚,也很少變動。
- 分頁 對很多第 5 頁之後的書架與清單頁都需要驗證。記得帶上
_session_id2cookie。 - 速率限制 真實存在。請保留 3–8 秒延遲,且每日請求量控制在 5,000 次以下。
- 兩段式策略(先收集 URL,再抓詳細頁)更穩定,也更容易續跑。
- 對於不寫程式的人(或任何想保住下午時間的人), 可以把 JS 渲染、分頁、子頁補資料與匯出這些流程,濃縮成大約兩下操作。
請負責任地擷取、尊重 robots.txt,也祝你的書籍資料永遠不會只剩 []。
常見問答
現在還能使用 Goodreads API 嗎?
不能。Goodreads 已在 2020 年 12 月停用公開 API,並且不再發放新的開發者金鑰。原本閒置超過 30 天的既有金鑰也會自動失效。現在要用程式存取書籍資料,只能選擇網頁爬蟲或替代 API(例如 Open Library 或 Google Books)。
為什麼我的 Goodreads 爬蟲會回傳空結果?
最常見的原因是 JavaScript 動態渲染。Goodreads 會透過 React/JavaScript 載入評論、評分分布與許多詳細區塊,而單純 requests.get() 看不到這些內容。這類頁面請改用 Selenium 或 Playwright。其他原因還包括:CSS 選擇器過時(Goodreads 改了 HTML)、缺少 User-Agent headers(導致 403 封鎖),或在需要登入的分頁書架頁上直接發送未驗證請求。
擷取 Goodreads 合法嗎?
就目前法律判例而言,擷取公開資料用於個人或研究用途,通常是可接受的(hiQ v. LinkedIn、Meta v. Bright Data)。但 Goodreads 的服務條款禁止自動化資料蒐集,而且你也應該隨時檢查它的 robots.txt。請避免商業性地重新散布受著作權保護的評論文字,並控制請求量,以免對網站造成負擔。
我要怎麼抓 Goodreads 的多頁資料?
在書架或清單 URL 後面加上 ?page=N,然後用迴圈依序抓每一頁。你可以透過空結果或缺少「下一頁」連結來判定最後一頁。要注意的是:某些書架頁在第 5 頁之後需要驗證(_session_id2 cookie)才能正常回傳資料——如果沒有帶上,你會默默拿到重複的第 1 頁內容。
不寫程式也能擷取 Goodreads 嗎?
可以。 是一個 Chrome 擴充功能,只要兩下就能抓 Goodreads——AI 會先幫你建議資料欄位,你再點「擷取」,就能直接匯出到 Google Sheets、Excel、Airtable 或 Notion。它會自動處理 JS 動態內容、分頁與子頁資料補齊,不需要 Python 或任何程式碼。
延伸閱讀