如何用 Python 擷取 Goodreads 資料(不再只拿到空結果)

最後更新於 April 16, 2026

最讓人挫折的事之一,就是你辛辛苦苦寫了 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 金鑰。沒有部落格公告,也沒有群發信件——只有文件頁面上的一小段提示,外加一票困惑的開發者。

goodreads-data-access-tools.webp

後果很快就出現了。一位開發者 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-Agenttime.sleep()、輪換代理 IP
書架/清單頁出現登入牆需要 cookie/sessionrequests.Session() 加 cookies,或改用瀏覽器爬取

JS 動態渲染內容:評論與評分都變成空白

Goodreads 使用的是 React 前端。當你對書籍頁面執行 requests.get() 時,拿到的只是初始 HTML——但評論、評分分布,以及許多「更多資訊」區塊,是透過 JavaScript 非同步載入的。你的爬蟲根本看不到它們。

解法是:只要你需要 JS 動態渲染內容,就改用 Selenium 或 Playwright。若是新專案,我會推薦 Playwright——因為它採用 WebSocket 通訊協定,速度比 Selenium 快 ,而且內建更好的隱匿能力與非同步支援。

troubleshooting-empty-array-causes.webp

分頁永遠只回傳第 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)
    • 函式庫:requestsbeautifulsoup4seleniumplaywrightpandas
    • (選用)gspread,用來匯出到 Google Sheets
    • (選用) 作為免寫碼替代方案

goodreads-scraping-flow.webp

步驟 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

  1. 安裝 Thunderbit Chrome 擴充功能,然後打開 Goodreads 的清單頁、書架頁或搜尋結果頁。
  2. 在 Thunderbit 側邊欄點擊「AI 建議欄位」。AI 會讀取頁面並推薦欄位——通常包含書名、作者、評分、封面圖片網址與書籍連結。
  3. 點擊「擷取」——資料會在幾秒內轉成結構化表格。
  4. 匯出到 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: / 擋下。bingbotCrawl-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_id2 cookie。
  • 速率限制 真實存在。請保留 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 或任何程式碼。

延伸閱讀

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