Pythonで30行ほど書いてGoodreadsスクレイパーを動かしたのに、返ってきたのが[]だけ——こんなに気持ちがしぼむ瞬間はなかなかありません。空の配列。何もなし。そこにあるのは、ただ点滅するカーソルだけです。
この光景は、Thunderbit内部の検証でも、開発者フォーラムでも、放置されたスクレイパーのGitHubリポジトリに積み上がるIssueでも、私は何度も見てきました。よくある不満はだいたい同じです。「上位レビュー欄が空で、[]しか出てこない」、「何ページ目を指定しても、必ず1ページ目しか取れない」、「去年は動いていたコードが、今は壊れている」。さらに厄介なのは、Goodreads APIは2020年12月に終了していることです。昔のチュートリアルでよく見る「APIを使えばいい」という助言は、今では完全に行き止まりです。
今のGoodreadsから、タイトル、著者、評価、レビュー、ジャンル、ISBNなどを構造化データとして取りたいなら、基本的にはスクレイピング一択です。このガイドでは、JavaScriptで生成されるコンテンツ、ページネーション、ブロック回避、エクスポートまで含めて、PythonでGoodreadsを実際に取得するための手順を最初から最後まで解説します。もしPythonが苦手でも大丈夫。コード不要で、たった2クリックほどで済む代替手段も紹介します。
Goodreadsスクレイピングとは?Pythonで行う理由は?
Goodreadsスクレイピングとは、コードを使ってGoodreadsのWebページから書籍データを自動で抜き出すことです。対象は、タイトル、著者、評価、レビュー数、ジャンル、ISBN、ページ数、出版日など。手作業でコピペするのではなく、自動で集めます。
Goodreadsは世界最大級の書籍データベースのひとつで、と、約を抱えています。毎月1,800万冊以上が「Want to Read」に追加されています。こうした常に更新される構造化データがあるからこそ、出版社、データサイエンティスト、書店、研究者が何度もここに戻ってくるのです。
この種の作業ではPythonが定番です。スクレイピング案件全体の約を支えているとも言われています。requests、BeautifulSoup、Selenium、Playwright、pandasなどのライブラリが成熟しており、文法も初学者にやさしく、コミュニティも非常に大きいです。
まだWebスクレイピングをしたことがないなら、まずPythonから始めるのがいちばんです。
なぜPythonでGoodreadsをスクレイピングするのか?実務での活用例
コードの話に入る前に、まずは「誰がこのデータを必要としているのか」「何に使うのか」を考えてみましょう。
| 用途 | 恩恵を受ける人 | 取得するデータ |
|---|---|---|
| 出版市場の調査 | 出版社、文芸エージェント | トレンドジャンル、高評価タイトル、新進著者、競合の評価 |
| 書籍レコメンドシステム | データサイエンティスト、個人開発者、趣味の制作者 | 評価、ジャンル、ユーザーの本棚、レビュー感情 |
| 価格・在庫監視 | EC書店 | 話題のタイトル、レビュー件数、「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投票の1週間前に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日1,000件(無料枠) | 稼働中(非英語ISBNに抜けあり) |
| Pythonスクレイピング(requests + BS4) | 初期HTMLにあるものなら何でも | 中程度 | 自前で管理 | 静的コンテンツ向け |
| Pythonスクレイピング(Selenium/Playwright) | JS描画コンテンツも対象 | やや難しい | 自前で管理 | レビューや一部リストで必要 |
| Thunderbit(コード不要のChrome拡張) | 画面に見えているデータすべて | とても簡単(2クリック) | クレジット制 | 稼働中 — Python不要 |
Open Libraryは、ISBN検索や基本メタデータの補完にかなり有効です。ただし、評価、レビュー、ジャンルタグ、「Want to Read」数まで必要なら、Goodreadsを直接取得するしかありません。方法はPythonか、Thunderbitのようなツールです。Thunderbitなら、AIが提案した項目を使いながら、Goodreadsのページ(本の詳細サブページも含む)をスクレイピングし、Google Sheets、Notion、Airtableへそのままエクスポートできます。
なぜPythonのGoodreadsスクレイパーは空の結果を返すのか?その直し方
これは、私がGoodreadsデータに触り始めた頃に一番知りたかった部分です。「空の結果」問題は開発者フォーラムで最も多い不満で、原因はひとつではありません。しかも、それぞれ対処法が違います。
| 症状 | 原因 | 対処法 |
|---|---|---|
レビュー/評価が [] になる | JS描画コンテンツ(React/遅延ロード) | requests ではなく Selenium または Playwright を使う |
| 常に1ページ目しか取れない | ページネーションが無視される、またはJS依存 | ?page=N を正しく渡す。無限スクロールならブラウザ自動化を使う |
| 去年は動いたのに今は失敗する | GoodreadsがHTMLのクラス名を変更した | 壊れにくいセレクタ(JSON-LD、data-testid)を使う |
| 数回で403/ブロックされる | ヘッダー不足 / リクエストが速すぎる | User-Agent を追加し、time.sleep() を入れ、プロキシをローテーションする |
| 本棚/リストページでログイン壁に当たる | Cookie/セッションが必要 | requests.Session() にCookieを入れるか、ブラウザスクレイピングを使う |
JS描画コンテンツ:レビューと評価が空欄になる
GoodreadsのフロントエンドはReactベースです。requests.get()で本のページを取得すると、最初のHTMLは取れますが、レビュー、評価分布、そして多くの「詳細」セクションはJavaScriptで後から読み込まれます。つまり、スクレイパーからは物理的に見えていません。
対処法はシンプルです。JS描画が必要なページでは、SeleniumかPlaywrightに切り替えましょう。新規プロジェクトならPlaywrightがおすすめです。WebSocketベースの仕組みによりで、ステルス性と非同期処理のサポートも優れています。

ページ2以降を回しても1ページ目しか返らない
これはかなり厄介です。ループを組んで?page=Nを増やしているのに、毎回同じ結果が返ってくる。Goodreadsでは、ログインしていない状態だと、本棚ページが?page=の値に関係なく、黙って1ページ目の内容を返すことがあります。エラーもリダイレクトもなく、ただ同じ1ページ目が繰り返されるだけです。
対処法は、認証済みのセッションCookie(特に_session_id2)を含めることです。詳しくは後ほどのページネーション節で説明します。
昨年は動いていたコードが今は失敗する
Goodreadsは定期的にHTMLのクラス名やページ構造を変えます。GitHubで人気のmaria-antoniak/goodreads-scraperリポジトリには、今では恒久的な注意書きとして*「このプロジェクトは保守されておらず、現在は動作しません」*とあります。対処法は、壊れやすいクラス名ではなく、JSON-LDの構造化データ(schema.org準拠で、めったに変わらない)やdata-testid属性のような、より堅牢なセレクタを使うことです。
403エラー、またはブロックされる
Pythonのrequestsは、ChromeとはTLSフィンガープリントが異なります。Chrome風のUser-Agentを入れていても、Goodreadsが使っているAWS WAFのようなボット検知は、その不一致を見抜けます。対処法は、現実的なブラウザヘッダーを追加し、リクエスト間に3〜8秒のtime.sleep()を入れ、大量取得する場合はTLSフィンガープリントを合わせるcurl_cffiの利用も検討することです。
本棚ページやリストページでログイン壁に当たる
一部のGoodreads本棚ページやリストページは、全文を取得するのに認証が必要です。特に5ページ目以降で起こりがちです。ブラウザからCookieをエクスポートしてrequests.Session()に入れるか、ログイン済みプロファイルでSelenium/Playwrightを使いましょう。Thunderbitなら、自分のログイン済みChromeブラウザ内で動くので、そのまま自然に処理できます。
始める前に
- 難易度: 中級(基本的なPython知識を想定)
- 所要時間: 全体で約20〜30分
- 必要なもの:
- Python 3.8以上
- Chromeブラウザ(DevToolsでの確認やSelenium/Playwright用)
- ライブラリ:
requests,beautifulsoup4,seleniumまたはplaywright,pandas - (任意)Google Sheets出力用の
gspread - (任意)コード不要の代替として

ステップ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でチェックできます。
インストール後は、すべてのライブラリをエラーなしでimportできるはずです。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と表示されればOKです。403なら、ヘッダーを見直してください。GoodreadsのAWS WAFは、現実的なUser-Agentを確認しており、簡素すぎるリクエストは弾きます。上のヘッダーは、本物のChromeブラウザセッションを模したものです。
ステップ3: ページを確認して正しいセレクタを見つける
Chrome DevTools(F12)でGoodreadsのリストページを開きます。本のタイトルを右クリックして「Inspect」を選びましょう。各書籍エントリのDOM構造が表示されます。
リストページでは、各本は通常itemtype="http://schema.org/Book"を持つ<tr>要素で囲まれています。中には次の要素があります。
- タイトル:
a.bookTitle(リンクテキストがタイトル、hrefが書籍URL) - 著者:
a.authorName - 評価:
span.minirating(平均評価と評価件数が含まれる) - 表紙画像: 書籍行の中にある
img
個別の書籍詳細ページでは、CSSセレクタを細かく追うより、JSON-LDを直接読むほうが早いです。Goodreadsは<script type="application/ld+json">タグの中にschema.org Book形式の構造化データを埋め込んでいます。クラス名は気まぐれに変わるので、JSON-LDのほうがはるかに安定しています。
ステップ4: 1つのリストページから書籍データを抽出する
では、リストページを解析して、各本の基本情報を抜き出してみましょう。
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"1ページ目で{len(books)}冊見つかりました")
28for b in books[:3]:
29 print(b)
リストページ1枚あたり、およそ100冊が取得できるはずです。各行には、タイトル、著者、「4.28 avg rating — 9,031,257 ratings」のような評価文字列、そして詳細ページへのURLが入ります。
ステップ5: 各本の詳細ページをたどって情報を増やす
一覧ページで取れるのは基本情報までです。本当に価値があるISBN、詳細説明、ジャンルタグ、ページ数、出版日などは、各本の個別ページにあります。ここでJSON-LDが威力を発揮します。
1import json
2import time
3def scrape_book_detail(book_url, headers):
4 """1冊分の詳細ページを訪問し、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"取得完了: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
32 time.sleep(4) # レート制限への配慮
リクエスト間には3〜8秒のtime.sleep()を入れましょう。Goodreadsは、単一IPからのアクセスが1分あたり20〜30件を超えるとレート制限が効き始め、速すぎると403やCAPTCHAが出ることがあります。
この「2段階方式」——まずリストページで本のURLを集め、次に詳細ページを巡回する——は、信頼性が高く、途中で止まっても再開しやすい方法です。実際に成功しているGoodreadsスクレイパーの多くがこの戦略を採用しています。
補足: なら、この処理をサブページスクレイピングで自動化できます。AIが各本の詳細ページを訪れて、ISBN、説明、ジャンルなどを自動で補完してくれます。コードもループもスリープタイマーも不要です。
ステップ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"評価: {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()
requestsとSeleniumの使い分け:
- 書籍メタデータ(JSON-LD)、リストページ、本棚ページの1ページ目、Choice Awardsのデータには
requests+ BeautifulSoup - レビュー、評価分布、そして生HTMLに出てこない内容には Selenium または Playwright
新規プロジェクトなら、一般的にはPlaywrightのほうが優秀です。高速で、メモリ消費も少なく、デフォルトのステルス性も高いです。ただし、Goodreads向けの既存コード例やコミュニティはSeleniumのほうがまだ多いです。
実際に使えるページネーション:Goodreadsのリストを最後まで取る
ページネーションは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}: status {resp.status_code} を受け取ったので停止します。")
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}: 書籍が見つからないため終了と判断します。")
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}: {len(rows)}冊取得(合計: {len(all_books)}冊)")
23 time.sleep(5) # ページ間は5秒待機
24print(f"\n完了。合計取得冊数: {len(all_books)}")
最後のページかどうかは、結果リストが空かどうか(tr[itemtype="http://schema.org/Book"]が見つからないか)で判定するか、「次へ」リンク(a.next_page)が存在しないかで確認します。
注意点:5ページ目以降はログインが必要な場合がある
ここが多くの人をつまずかせる罠です。Goodreadsの一部の本棚ページやリストページでは、認証なしで6ページ目以降を取得すると、黙って1ページ目の内容を返すことがあります。エラーもリダイレクトもなく、ただ同じデータが繰り返されるだけです。
これを直すには、ブラウザから_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# 以後はrequests.get()ではなくsession.get()を使う
5resp = session.get(f"{base_url}?page=6", timeout=15)
Thunderbitは、クリック型ページネーションにも無限スクロールにも最初から対応しています。コードもCookie管理も不要です。ページネーションの実装が何度も壊れるなら、検討する価値は十分あります。
コピペで使える完全版Pythonスクリプト
以下が、ヘッダー、ページネーション、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 """1つの一覧ページから、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の補助情報を抽出する。"""
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: リストページから本の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}: 空だったためページネーションを停止します。")
70 break
71 all_books.extend(page_books)
72 print(f"Page {page}: {len(page_books)}冊(合計: {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"\n{len(all_books)}冊を{OUTPUT_FILE}に保存しました")
88 else:
89 print("書籍データは取得できませんでした。")
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だけでなくGoogle Sheetsにも出力する:gspreadを使う
CSVだけでなくGoogle Sheetsにも入れたい場合は、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("データをGoogle Sheetsに送信しました。")
Google Cloudで、Sheets APIとDrive APIを有効にしたサービスアカウントが必要です。セットアップはに5分ほどでまとまっています。数百行を超えるなら、append_rows()のようなバッチ処理を使いましょう。Google側のレート制限は、1プロジェクトあたり60秒間に300リクエストです。
もちろん、こうした設定が面倒なら、ThunderbitならでGoogle Sheets、Airtable、Notion、Excel、CSV、JSONへ書き出せます。ライブラリ設定も、認証ファイルも、APIクォータも不要です。
コード不要の代替案:ThunderbitでGoodreadsをスクレイピングする
誰もがPythonスクリプトを保守したいわけではありません。たとえば、単発の市場調査をしたい出版社や、今年のベストセラー一覧を表にしたいブックブロガーなら、まさにThunderbit向きです。
ThunderbitでGoodreadsをスクレイピングする方法
- からThunderbit Chrome拡張をインストールし、Goodreadsのリスト・本棚・検索結果ページを開きます。
- Thunderbitのサイドバーで「AIで項目を提案」をクリックします。AIがページを読み取り、通常はタイトル、著者、評価、表紙画像URL、本のリンクなどの列を提案します。
- 「スクレイプ」をクリックすると、数秒で構造化テーブルに抽出されます。
- Google Sheets、Excel、Airtable、Notion、CSV、JSONへエクスポートします。
ISBN、説明、ジャンル、ページ数などの詳細データが必要なら、Thunderbitのサブページスクレイピング機能が各本の詳細ページを自動で巡回し、テーブルを拡張してくれます。ループもスリープタイマーもデバッグも不要です。
Thunderbitはページネーションにも標準対応しています。「次へ」をクリックさせる、あるいはスクロールさせるよう指示するだけで、コードなしで全ページからデータを集められます。
トレードオフはシンプルです。Pythonスクリプトは完全な制御ができ、コストは時間だけです。一方Thunderbitは、柔軟性を少し譲る代わりに、圧倒的な時短と保守ゼロを実現します。300冊規模のリストなら、Pythonでは実行時間だけで約25分、さらに作成とデバッグの時間がかかります。Thunderbitなら、同じデータを2クリックほどで約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日改訂)は、「データマイニング、ロボット、または類似のデータ収集・抽出ツールの使用」を禁じています。かなり広い表現なので真剣に受け止めるべきですが、裁判所は一貫して、利用規約違反だけでは刑事上の「無許可アクセス」には当たらないとしています。判決では、「利用規約違反を犯罪化すると、各Webサイトが独自の刑事管轄権を持つことになりかねない」と述べられました。
ベストプラクティス
- リクエストは遅らせる: 1〜8秒の間隔を空ける(Goodreadsのrobots.txt自体もボットに5秒を示唆)
- 1日5,000リクエスト未満に抑える: 単一IPからの大量アクセスを避ける
- 公開ページだけを取得する: ログイン必須データの大量取得は避ける
- レビュー本文の再配布は商用目的で行わない: レビューは著作物です
- 必要なものだけ保存し、保持期間を決める: データ保管を最小限にする
- 個人研究と商用利用を分けて考える: 公開データを個人分析や学術研究に使うのは一般的に受け入れられています。商用再配布になると法的リスクが上がります。
Thunderbitのように自分のブラウザセッション経由でスクレイピングするツールでも、見た目は通常のブラウジングと同じです。ただし、ツールが何であれ倫理原則は同じです。についてさらに深く知りたい場合は、別記事で解説しています。
ヒントとよくある落とし穴
ヒント: まずはJSON-LDを見る。 複雑なCSSセレクタを書く前に、欲しいデータが<script type="application/ld+json">に入っていないか確認しましょう。より安定していて、解析しやすく、Goodreadsのフロントエンド更新で壊れにくいです。
ヒント: 2段階方式を使う。 まず一覧ページから本のURLを全部集め、次に各詳細ページを訪問します。これなら途中で落ちても再開しやすく、URLリストをチェックポイントとして保存することもできます。
落とし穴: 欠損フィールドの処理忘れ。 すべての本にISBN、ジャンルタグ、説明があるわけではありません。常に.get()のデフォルト値を使うか、セレクタをifで包みましょう。NoneTypeエラー1つで、3時間のスクレイピングが吹き飛ぶことがあります。
落とし穴: 速く回しすぎること。 time.sleep(0.5)にして一気に進めたくなる気持ちはわかります。でも、Goodreadsは20〜30回の高速リクエストで403を返し始めます。いったんフラグが立つと、数時間待つかIPを変える必要が出ることもあります。4〜5秒の遅延がちょうどいいバランスです。
落とし穴: 古いチュートリアルを信じること。 Goodreads APIに触れている、あるいは.field.valueや#bookTitleのようなクラス名を使っているガイドは、たいてい古いです。スクレイパーを作る前に、必ず実際のページでセレクタを確認しましょう。
使うべきスクレイピングツールやフレームワークを選ぶコツは、やのガイドも参考になります。
まとめと重要ポイント
PythonでGoodreadsをスクレイピングするのは十分可能です。ただし、どこに落とし穴があるかを知っておく必要があります。要点は次の通りです。
- Goodreads APIは終了済み(2020年12月以降)。構造化された書籍データを取るには、スクレイピングが主な方法です。
- 空の結果は、ほとんどの場合、JS描画コンテンツ、古いセレクタ、ヘッダー不足、ページネーションの認証問題が原因です。コードが間違っているとは限りません。
- JSON-LDは書籍メタデータ取得の最強の味方です。安定していて構造化されており、滅多に変わりません。
- ページネーションは、5ページ目以降の多くの本棚・リストページで認証が必要です。
_session_id2Cookieを入れましょう。 - レート制限は実在します。3〜8秒の待機を入れ、1日5,000リクエスト未満を意識してください。
- 2段階方式(先にURL収集、後で詳細ページ取得)は、より信頼性が高く、再開もしやすいです。
- 非エンジニアや、午後を節約したい人には、が、JS描画、ページネーション、サブページ補完、エクスポートまでまとめて2クリックほどで処理してくれます。
責任を持ってスクレイピングし、robots.txtを尊重し、あなたの書籍データが[]ではなくちゃんと返ってくることを願っています。
よくある質問
Goodreads APIは今でも使えますか?
いいえ。Goodreadsは2020年12月に公開APIを終了し、新しい開発者キーの発行も停止しました。30日間非アクティブだった既存キーは自動で無効化されました。現在、書籍データをプログラムで取得するには、Webスクレイピングか、Open LibraryやGoogle Booksのような代替APIを使うしかありません。
なぜGoodreadsスクレイパーは空の結果を返すのですか?
最も多い原因はJavaScript描画コンテンツです。Goodreadsはレビュー、評価分布、多くの詳細セクションをReact/JavaScriptで読み込むため、単純なrequests.get()では見えません。該当ページではSeleniumかPlaywrightに切り替えましょう。ほかには、CSSセレクタが古い、User-Agentヘッダーがないため403になる、ページネーション済み本棚ページで未認証アクセスしている、などの原因があります。
Goodreadsをスクレイピングするのは合法ですか?
公開データを個人利用や研究目的でスクレイピングすることは、現在の法的判断(hiQ v. LinkedIn、Meta v. Bright Dataなど)では一般的に認められています。ただし、Goodreadsの利用規約は自動収集を禁じており、robots.txtも必ず確認すべきです。著作権のあるレビュー本文の商用再配布は避け、サイトに負荷をかけないようリクエスト量も抑えてください。
Goodreadsを複数ページまとめてスクレイピングするには?
本棚やリストURLに?page=Nを付けて、ページ番号をループさせます。最後のページは、空結果または「次へ」リンクがないことで判定します。重要な注意点として、一部の本棚ページでは5ページ目以降を取るのに認証(_session_id2 Cookie)が必要で、これがないと1ページ目のデータが静かに繰り返されます。
コードを書かずにGoodreadsをスクレイピングできますか?
はい。はChrome拡張で、2クリックでGoodreadsをスクレイピングできます。AIが項目を提案し、「スクレイプ」を押せば、Google Sheets、Excel、Airtable、Notionへ直接エクスポートできます。JavaScript描画コンテンツ、ページネーション、サブページ補完にも自動対応で、Pythonやコーディングは不要です。
さらに詳しく