PythonでGoodreadsをスクレイピングする方法(空っぽの結果にさようなら)

最終更新日 July 28, 2026
PythonでGoodreadsをスクレイピングする方法(空っぽの結果にさようなら)

Goodreadsから書籍情報を取得するPythonスクリプトでは、コード自体に問題がなくても、結果が[]になることがあります。主な原因は、JavaScriptで後から描画されるコンテンツ、ページネーション時の認証、HTML構造の変更、アクセス制限です。

Thunderbit内部の検証や開発者フォーラム、GoodreadsスクレイパーのGitHub Issueでも、「上位レビュー欄が空で、[]しか出てこない」「何ページ目を指定しても、必ず1ページ目しか取れない」、*「去年は動いていたコードが、今は壊れている」*といった報告が見られます。また、Goodreads APIは2020年12月に終了しているため、古いチュートリアルにある「APIを使う」という方法は、現在の新規実装には利用できません。

本記事は、Goodreadsからタイトル、著者、評価、レビュー、ジャンル、ISBNなどを取得したいPython利用者を対象に、取得方式の選び方から実装、エラーの切り分け、ページネーション、アクセス頻度への配慮、エクスポートまでを順に解説します。Pythonを使わずに試したい場合に向けて、コード不要で、たった2クリックほどで済む代替手段も紹介します。

Goodreadsスクレイピングとは?Pythonで行う理由は?

Goodreadsスクレイピングとは、コードを使ってGoodreadsのWebページから書籍データを自動で抜き出すことです。対象は、タイトル、著者、評価、レビュー数、ジャンル、ISBN、ページ数、出版日など。手作業でコピペするのではなく、自動で集めます。

Goodreadsは世界最大級の書籍データベースのひとつで、1億5,000万人超の登録メンバーと、約9,000万件のユーザーレビューを抱えています。毎月1,800万冊以上が「Want to Read」に追加されています。こうした常に更新される構造化データがあるからこそ、出版社、データサイエンティスト、書店、研究者が何度もここに戻ってくるのです。

この種の作業ではPythonが定番です。スクレイピング案件全体の約69.6%を支えているとも言われています。requests、BeautifulSoup、Selenium、Playwright、pandasなどのライブラリが成熟しており、文法も初学者にやさしく、コミュニティも非常に大きいです。

まだWebスクレイピングをしたことがないなら、まずPythonから始めるのがいちばんです。

なぜPythonでGoodreadsをスクレイピングするのか?実務での活用例

コードの話に入る前に、まずは「誰がこのデータを必要としているのか」「何に使うのか」を考えてみましょう。

用途恩恵を受ける人取得するデータ
出版市場の調査出版社、文芸エージェントトレンドジャンル、高評価タイトル、新進著者、競合の評価
書籍レコメンドシステムデータサイエンティスト、個人開発者、趣味の制作者評価、ジャンル、ユーザーの本棚、レビュー感情
価格・在庫監視EC書店話題のタイトル、レビュー件数、「Want to Read」数
学術研究研究者、学生レビュー本文、評価分布、ジャンル分類
読書分析ブックブログ運営者、個人プロジェクト自分の本棚、読書履歴、年間まとめ統計

具体例を挙げると、UCSD Book Graphは推薦研究で最も引用される学術データセットのひとつで、公開アクセス可能なGoodreadsの本棚から収集した236万冊のユニーク本と2億2,800万件超のユーザー×書籍インタラクションを含んでいます。Kaggle上の複数のデータセット(goodbooks-10k、Best Books Everなど)も、元をたどればGoodreadsのスクレイピング由来です。さらに、2025年の Big Data and Society の研究では、スポンサー付きレビューがどのようにプラットフォーム上の評価形成に影響するかを分析するために、331,211件のインセンティブ付き書評が機械的に収集されました。

商業面でも需要ははっきりしています。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投票の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不要

ISBN検索や基本メタデータの補完が目的なら、Open Libraryが候補になります。一方、Goodreads上の評価、レビュー、ジャンルタグ、「Want to Read」数が必要な場合は、対象ページを直接取得する方法を検討します。実装や保守を細かく制御したい場合は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で後から読み込まれます。そのため、取得した生HTMLだけを解析しても、対象要素が見つからない場合があります。

まず、ブラウザのDevToolsとresponse.textを比較し、必要な要素が初期HTMLに含まれているか確認します。含まれていなければ、SeleniumまたはPlaywrightへの切り替えを検討します。新規プロジェクトではPlaywrightも候補になります。WebSocketベースの仕組みによりSeleniumより35〜45%高速とする比較がありますが、実際の速度はページ構造、待機条件、実行環境によって変わるため、対象ページで計測して選んでください。

troubleshooting-empty-array-causes.webp

ページ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
    • (任意)コード不要の代替として Thunderbit Chrome Extension

goodreads-scraping-flow.webp

ステップ1: Python環境を整える

必要なライブラリをインストールします。ターミナルを開いて、次を実行してください。

pip install requests beautifulsoup4 selenium pandas lxml

Playwrightを使う場合(新規プロジェクトにはこちら推奨):

pip install playwright
playwright install chromium

Google Sheetsへ出力したい場合(任意):

pip 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でこのページを取得してみましょう。

import requests
from bs4 import BeautifulSoup

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}

url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
response = requests.get(url, headers=headers, timeout=15)
print(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つのリストページから書籍データを抽出する

では、リストページを解析して、各本の基本情報を抜き出してみましょう。

import requests
from bs4 import BeautifulSoup

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
}

url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
response = requests.get(url, headers=headers, timeout=15)
soup = BeautifulSoup(response.text, "lxml")

books = []
rows = soup.select('tr[itemtype="http://schema.org/Book"]')

for row in rows:
    title_tag = row.select_one("a.bookTitle")
    author_tag = row.select_one("a.authorName")
    rating_tag = row.select_one("span.minirating")

    title = title_tag.get_text(strip=True) if title_tag else ""
    book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
    author = author_tag.get_text(strip=True) if author_tag else ""
    rating_text = rating_tag.get_text(strip=True) if rating_tag else ""

    books.append({
        "title": title,
        "author": author,
        "rating_info": rating_text,
        "book_url": book_url,
    })

print(f"1ページ目で{len(books)}冊見つかりました")
for b in books[:3]:
    print(b)

リストページ1枚あたり、およそ100冊が取得できるはずです。各行には、タイトル、著者、「4.28 avg rating — 9,031,257 ratings」のような評価文字列、そして詳細ページへのURLが入ります。

ステップ5: 各本の詳細ページをたどって情報を増やす

一覧ページで取れるのは基本情報までです。本当に価値があるISBN、詳細説明、ジャンルタグ、ページ数、出版日などは、各本の個別ページにあります。ここでJSON-LDが威力を発揮します。

import json
import time

def scrape_book_detail(book_url, headers):
    """1冊分の詳細ページを訪問し、JSON-LDからメタデータを抽出する。"""
    resp = requests.get(book_url, headers=headers, timeout=15)
    if resp.status_code != 200:
        return {}

    soup = BeautifulSoup(resp.text, "lxml")
    script = soup.find("script", {"type": "application/ld+json"})
    if not script:
        return {}

    data = json.loads(script.string)
    agg = data.get("aggregateRating", {})

    # ジャンルタグはJSON-LDにないためHTMLから取得
    genres = [g.get_text(strip=True) for g in soup.select('span.BookPageMetadataSection__genreButton a span')]

    return {
        "isbn": data.get("isbn", ""),
        "pages": data.get("numberOfPages", ""),
        "language": data.get("inLanguage", ""),
        "format": data.get("bookFormat", ""),
        "avg_rating": agg.get("ratingValue", ""),
        "rating_count": agg.get("ratingCount", ""),
        "review_count": agg.get("reviewCount", ""),
        "description": data.get("description", "")[:200],  # プレビュー用に短縮
        "genres": ", ".join(genres[:5]),
    }

# 例: 最初の3冊を詳細化
for book in books[:3]:
    details = scrape_book_detail(book["book_url"], headers)
    book.update(details)
    print(f"取得完了: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
    time.sleep(4)  # レート制限への配慮

リクエスト間には3〜8秒のtime.sleep()を入れましょう。Goodreadsは、単一IPからのアクセスが1分あたり20〜30件を超えるとレート制限が効き始め、速すぎると403やCAPTCHAが出ることがあります。

この「2段階方式」——まずリストページで本のURLを集め、次に詳細ページを巡回する——は、途中結果をURLリストとして保存できるため、処理が停止した場合も再開しやすい方法です。実行時は、各ページのステータスコードと取得件数を記録し、同じページや空の結果が続いていないか確認してください。

補足: Thunderbitでは、サブページスクレイピングを使って各本の詳細ページをたどり、ISBN、説明、ジャンルなどを一覧へ追加できます。Pythonのループを保守せずに少量のデータを確認したい場合は候補になります。対象ページによって取得できる項目が異なるため、まず数冊で結果を確認してください。

ステップ6: SeleniumでJavaScript描画コンテンツに対応する

レビュー、評価の内訳、「さらに詳細」セクションのようにJavaScriptで読み込まれるページでは、ブラウザ自動化ツールが必要です。Seleniumの例を示します。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument("--headless=new")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                     "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")

driver = webdriver.Chrome(options=options)
driver.get("https://www.goodreads.com/book/show/5907.The_Hobbit")

# レビューの読み込みを待つ
try:
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
    )
except:
    print("レビューが読み込まれませんでした。ログインが必要か、JSの読み込みがタイムアウトした可能性があります")

# レンダリング済みのページを解析
page_source = driver.page_source
soup = BeautifulSoup(page_source, "lxml")

reviews = soup.select("article.ReviewCard")
for rev in reviews[:3]:
    text = rev.select_one("span.Formatted")
    stars = rev.select_one("span.RatingStars")
    print(f"評価: {stars.get_text(strip=True) if stars else 'N/A'}")
    print(f"レビュー: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
    print()

driver.quit()

requestsとSeleniumの使い分け:

  • 書籍メタデータ(JSON-LD)、リストページ、本棚ページの1ページ目、Choice Awardsのデータには requests + BeautifulSoup
  • レビュー、評価分布、そして生HTMLに出てこない内容には Selenium または Playwright

新規プロジェクトでは、PlaywrightとSeleniumのどちらも候補になります。非同期処理や待機条件を組み込みやすい構成を重視する場合はPlaywright、Goodreads向けの既存コード例を流用したい場合はSeleniumが選びやすいでしょう。速度、メモリ使用量、検知されやすさは実行環境と対象ページによって変わるため、少数ページで動作と取得結果を比較してから決めてください。

実際に使えるページネーション:Goodreadsのリストを最後まで取る

ページネーションは、Goodreadsスクレイパーで結果の重複や取りこぼしが起きやすい工程です。ここでは、URLパラメータの指定、終了条件、認証が必要な場合の確認方法を順に説明します。実行時はページ番号だけでなく、各ページの先頭URLや取得件数も記録し、同じ内容が繰り返されていないか確認してください。

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冊ほど表示されます。

終了条件を見極めるページネーションループを書く

import time

all_books = []
base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"

for page_num in range(1, 50):  # 安全のため最大50ページまで
    url = f"{base_url}?page={page_num}"
    resp = requests.get(url, headers=headers, timeout=15)

    if resp.status_code != 200:
        print(f"Page {page_num}: status {resp.status_code} を受け取ったので停止します。")
        break

    soup = BeautifulSoup(resp.text, "lxml")
    rows = soup.select('tr[itemtype="http://schema.org/Book"]')

    if not rows:
        print(f"Page {page_num}: 書籍が見つからないため終了と判断します。")
        break

    for row in rows:
        title_tag = row.select_one("a.bookTitle")
        author_tag = row.select_one("a.authorName")
        title = title_tag.get_text(strip=True) if title_tag else ""
        book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
        author = author_tag.get_text(strip=True) if author_tag else ""
        all_books.append({"title": title, "author": author, "book_url": book_url})

    print(f"Page {page_num}: {len(rows)}冊取得(合計: {len(all_books)}冊)")
    time.sleep(5)  # ページ間は5秒待機

print(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を使用)、リクエストに含めます。

session = requests.Session()
session.headers.update(headers)
session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")

# 以後はrequests.get()ではなくsession.get()を使う
resp = session.get(f"{base_url}?page=6", timeout=15)

同じページが繰り返されていないか確認するため、ページごとの先頭書籍URLや取得件数をログに残してください。Cookieは認証情報に当たるため、コードや共有リポジトリへ保存せず、対象サイトの利用規約と社内の認証情報管理ルールも確認します。

Thunderbitは、クリック型ページネーションと無限スクロールに対応しています。Python側でページ遷移やCookie処理を保守したくない場合は候補になりますが、ログイン後ページを含む取得では、対象ページで遷移と抽出結果を少量確認してから範囲を広げてください。

コピペで使える完全版Pythonスクリプト

以下が、ヘッダー、ページネーション、JSON-LDによる詳細ページ取得、レート制限、CSV出力まで含めた完全版です。2025年半ば時点の実際のGoodreadsページで動作確認しています。

"""
goodreads_scraper.py — ページネーション付きGoodreadsリストをスクレイピングし、詳細情報を補完する。
使い方: python goodreads_scraper.py
出力: goodreads_books.csv
"""

import csv, json, time, requests
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
BASE_URL = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
MAX_PAGES = 3          # 必要に応じて調整
DELAY_LISTING = 5      # リストページ間の秒数
DELAY_DETAIL = 4       # 詳細ページ間の秒数
OUTPUT_FILE = "goodreads_books.csv"


def scrape_listing_page(url):
    """1つの一覧ページから、title・author・book_urlの辞書リストを返す。"""
    resp = requests.get(url, headers=HEADERS, timeout=15)
    if resp.status_code != 200:
        return []
    soup = BeautifulSoup(resp.text, "lxml")
    rows = soup.select('tr[itemtype="http://schema.org/Book"]')
    books = []
    for row in rows:
        t = row.select_one("a.bookTitle")
        a = row.select_one("a.authorName")
        if t:
            books.append({
                "title": t.get_text(strip=True),
                "author": a.get_text(strip=True) if a else "",
                "book_url": "https://www.goodreads.com" + t["href"],
            })
    return books


def scrape_book_detail(book_url):
    """本の詳細ページにアクセスし、JSON-LD + HTMLの補助情報を抽出する。"""
    resp = requests.get(book_url, headers=HEADERS, timeout=15)
    if resp.status_code != 200:
        return {}
    soup = BeautifulSoup(resp.text, "lxml")
    script = soup.find("script", {"type": "application/ld+json"})
    if not script:
        return {}
    data = json.loads(script.string)
    agg = data.get("aggregateRating", {})
    genres = [g.get_text(strip=True)
              for g in soup.select("span.BookPageMetadataSection__genreButton a span")]
    return {
        "isbn": data.get("isbn", ""),
        "pages": data.get("numberOfPages", ""),
        "avg_rating": agg.get("ratingValue", ""),
        "rating_count": agg.get("ratingCount", ""),
        "review_count": agg.get("reviewCount", ""),
        "description": (data.get("description", "") or "")[:300],
        "genres": ", ".join(genres[:5]),
        "language": data.get("inLanguage", ""),
        "format": data.get("bookFormat", ""),
        "published": data.get("datePublished", ""),
    }


def main():
    all_books = []

    # --- パス1: リストページから本のURLを集める ---
    for page in range(1, MAX_PAGES + 1):
        url = f"{BASE_URL}?page={page}"
        page_books = scrape_listing_page(url)
        if not page_books:
            print(f"Page {page}: 空だったためページネーションを停止します。")
            break
        all_books.extend(page_books)
        print(f"Page {page}: {len(page_books)}冊(合計: {len(all_books)}冊)")
        time.sleep(DELAY_LISTING)

    # --- パス2: 各本を詳細ページで補完 ---
    for i, book in enumerate(all_books):
        details = scrape_book_detail(book["book_url"])
        book.update(details)
        print(f"[{i+1}/{len(all_books)}] {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
        time.sleep(DELAY_DETAIL)

    # --- CSVに出力 ---
    if all_books:
        fieldnames = list(all_books[0].keys())
        with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(all_books)
        print(f"\n{len(all_books)}冊を{OUTPUT_FILE}に保存しました")
    else:
        print("書籍データは取得できませんでした。")


if __name__ == "__main__":
    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出力の後ろに次を追加します。

import gspread
from oauth2client.service_account import ServiceAccountCredentials

scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", scope)
client = gspread.authorize(creds)

sheet = client.open("Goodreads Scrape").sheet1
header = list(all_books[0].keys())
sheet.append_row(header)
for book in all_books:
    sheet.append_row([str(book.get(k, "")) for k in header])
print("データをGoogle Sheetsに送信しました。")

Google Cloudで、Sheets APIとDrive APIを有効にしたサービスアカウントが必要です。セットアップ手順はgspreadのドキュメントで確認できます。数百行を超える場合は、1行ずつ送信するのではなく、append_rows()のようなバッチ処理を検討してください。Google側のレート制限は、1プロジェクトあたり60秒間に300リクエストです。

認証ファイルやAPI設定を管理せずに出力したい場合は、Thunderbitの1クリックエクスポートも候補になります。Google Sheets、Airtable、Notion、Excel、CSV、JSONへの出力に対応しています。まず少量のデータで列の並び、文字コード、欠損値の扱いを確認してから、全件を出力してください。

コード不要の代替案:ThunderbitでGoodreadsをスクレイピングする

誰もがPythonスクリプトを保守したいわけではありません。たとえば、単発の市場調査をしたい出版社や、今年のベストセラー一覧を表にしたいブックブロガーなら、まさにThunderbit向きです。

ThunderbitでGoodreadsをスクレイピングする方法

  1. ChromeウェブストアからThunderbit Chrome拡張をインストールし、Goodreadsのリスト・本棚・検索結果ページを開きます。
  2. Thunderbitのサイドバーで「AIで項目を提案」をクリックします。AIがページを読み取り、通常はタイトル、著者、評価、表紙画像URL、本のリンクなどの列を提案します。提案された列が実際の用途に合っているか確認し、不要な列や欠損があれば調整します。
  3. スクレイプ」をクリックすると、構造化テーブルに抽出されます。まず1ページ分を取得し、件数と各列の内容を元ページと照合してください。
  4. Google Sheets、Excel、Airtable、Notion、CSV、JSONへエクスポートします。

ISBN、説明、ジャンル、ページ数などの詳細データが必要なら、Thunderbitのサブページスクレイピング機能で各本の詳細ページを巡回し、テーブルへ項目を追加できます。対象ページの構造によっては欠損や表示待ちが生じるため、数冊で結果を確認してから対象範囲を広げます。

Thunderbitはページネーションにも標準対応しています。「次へ」をクリックさせる、あるいはスクロールさせるよう指示して、複数ページのデータを収集できます。ログイン状態やページ構造によって動作が異なる場合があるため、ページごとの件数と重複も確認してください。

Pythonは、抽出条件、再試行、ログ、保存方法まで細かく制御したい場合に向いています。一方Thunderbitは、単発調査や小〜中規模の一覧取得を、コードを保守せずに試したい場合の候補です。300冊規模のリストについて、元の検証ではPythonが実行だけで約25分、Thunderbitが2クリックほどで約3分という結果でしたが、所要時間はページ構造、通信環境、詳細ページ数によって変わります。まず同じ少量のページで取得件数、精度、所要時間を比較してください。

GoodreadsスクレイピングにThunderbitを試す

Goodreadsを責任ある形でスクレイピングする:robots.txt、利用規約、倫理

実装前に、対象パスのrobots.txt、Goodreadsの利用規約、著作権、認証情報、アクセス頻度、取得データの利用目的を確認してください。特に商用利用やレビュー本文の保存・再配布を伴う場合は、技術的に取得できるかだけで判断せず、必要に応じて法務担当者へ確認します。

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日改訂)には、「データマイニング、ロボット、または類似のデータ収集・抽出ツールの使用」を禁じる条項があります。対象データが公開されていても、利用規約上の制限がなくなるわけではないため、利用目的、取得範囲、再配布の有無を確認してください。

一方、利用規約違反と刑事上の「無許可アクセス」が常に同じ扱いになるとは限りません。Sandvig v. Barr判決では、「利用規約違反を犯罪化すると、各Webサイトが独自の刑事管轄権を持つことになりかねない」と述べられました。ただし、個別の取得行為が適法かどうかは、管轄、取得方法、データの種類、利用目的などによって異なります。本記事は法的助言ではないため、商用利用や大量取得では専門家へ確認してください。

ベストプラクティス

  • リクエストは遅らせる: 1〜8秒の間隔を空ける(Goodreadsのrobots.txt自体もボットに5秒を示唆)
  • 1日5,000リクエスト未満に抑える: 単一IPからの大量アクセスを避ける
  • 公開ページだけを取得する: ログイン必須データの大量取得は避ける
  • レビュー本文の再配布は商用目的で行わない: レビューは著作物です
  • 必要なものだけ保存し、保持期間を決める: データ保管を最小限にする
  • 個人研究と商用利用を分けて考える: 公開データを個人分析や学術研究に使うのは一般的に受け入れられています。商用再配布になると法的リスクが上がります。

Thunderbitのように自分のブラウザセッション経由でスクレイピングするツールでも、見た目は通常のブラウジングと同じです。ただし、ツールが何であれ倫理原則は同じです。Webスクレイピングの法的論点についてさらに深く知りたい場合は、別記事で解説しています。

ヒントとよくある落とし穴

ヒント: まずはJSON-LDを見る。 複雑なCSSセレクタを書く前に、欲しいデータが<script type="application/ld+json">に入っていないか確認しましょう。より安定していて、解析しやすく、Goodreadsのフロントエンド更新で壊れにくいです。

ヒント: 2段階方式を使う。 まず一覧ページから本のURLを全部集め、次に各詳細ページを訪問します。これなら途中で落ちても再開しやすく、URLリストをチェックポイントとして保存することもできます。

落とし穴: 欠損フィールドの処理忘れ。 すべての本にISBN、ジャンルタグ、説明があるわけではありません。常に.get()のデフォルト値を使うか、セレクタをifで包みましょう。欠損値を処理しないと、1件のNoneTypeエラーで長時間の処理全体が停止する可能性があります。URL一覧や処理済み件数を定期的に保存しておくと、再開しやすくなります。

落とし穴: 速く回しすぎること。 待機時間を短くすると、403やCAPTCHAが発生し、処理を継続できなくなる場合があります。Goodreadsは20〜30回の高速リクエストで403を返し始めるとされていますが、固定のしきい値として扱わず、少量から開始して応答状況を記録してください。元の検証では4〜5秒の遅延を使用しています。

落とし穴: 古いチュートリアルを信じること。 Goodreads APIに触れている、あるいは.field.value#bookTitleのようなクラス名を使っているガイドは、現在のページと一致しない可能性があります。スクレイパーを作る前に、実際のページでセレクタを確認しましょう。

使うべきスクレイピングツールやフレームワークを選ぶコツは、おすすめのPython WebスクレイピングツールPythonでWebスクレイピングする方法のガイドも参考になります。

まとめと重要ポイント

PythonでGoodreadsをスクレイピングすることは可能ですが、対象データが初期HTMLにあるか、認証が必要か、ページが重複していないかを工程ごとに確認する必要があります。要点は次の通りです。

  • Goodreads APIは終了済み(2020年12月以降)。構造化された書籍データを取得する場合は、必要な項目に応じてスクレイピングまたは代替APIを選びます。
  • 空の結果は、JS描画コンテンツ、古いセレクタ、ヘッダー不足、ページネーション時の認証などが原因になることがあります。まず初期HTML、ステータスコード、取得件数を確認します。
  • JSON-LDは、書籍メタデータを構造化された形で取得できる候補です。CSSクラスより変更の影響を受けにくい場合がありますが、必要なフィールドがあるかはページごとに確認します。
  • ページネーションでは、一部の本棚・リストページで認証が必要になる場合があります。_session_id2 Cookieを扱う場合は、認証情報として安全に管理し、同じページが繰り返されていないか確認します。
  • アクセス頻度は、3〜8秒の待機を目安に少量から検証し、レスポンス状況を記録します。1日5,000リクエスト未満という数値も固定ルールではなく、利用規約や対象サイトの状況とあわせて判断してください。
  • 2段階方式(先にURL収集、後で詳細ページ取得)は、途中結果を保存しやすく、停止後の再開にも向いています。
  • コードを保守せずに試したい場合は、Thunderbitを使って、JS描画、ページネーション、サブページ補完、エクスポートを2クリックほどで設定する方法も候補になります。まず少量の書籍で取得件数、項目の欠損、出力形式を確認してください。

実行前にrobots.txtと利用規約を確認し、著作権、認証情報、アクセス頻度、取得データの利用目的に配慮してください。最初は1ページと数冊の詳細ページでテストし、結果と負荷を確認してから対象範囲を広げるのが現実的です。

よくある質問

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をスクレイピングできますか?

はい。ThunderbitはChrome拡張で、2クリックでGoodreadsをスクレイピングできます。AIが項目を提案し、「スクレイプ」を押すと、Google Sheets、Excel、Airtable、Notionへ直接エクスポートできます。JavaScript描画コンテンツ、ページネーション、サブページ補完にも対応しています。

コードを保守せずにリストや書籍詳細を表にしたい場合に向いています。最初に1ページと数冊のサブページで試し、タイトル、著者、ISBNなど必要な列の取得精度、重複、欠損、出力形式を確認してください。複雑な再試行条件や独自の保存処理が必要な場合は、Pythonによる実装も比較対象になります。

さらに詳しく

Shuai Guan
Shuai Guan
Thunderbit の CEO | AIデータ自動化のエキスパート Shuai Guan は Thunderbit の CEO であり、ミシガン大学工学部の卒業生です。テクノロジーと SaaS アーキテクチャの分野で約10年にわたる経験をもとに、複雑な AI モデルを、実務で使えるノーコードのデータ抽出ツールへと落とし込むことを得意としています。このブログでは、ウェブスクレイピングや自動化戦略について、実践で磨かれた率直な知見を共有し、より賢くデータ主導のワークフローを構築できるよう支援しています。データワークフローの最適化から離れているときは、同じこだわりと観察眼を写真への情熱にも注いでいます。
目次

ただ伝えるだけで、Webページをスクレイピング

必要なことをそのまま英語で伝えるだけ。いや、何も言わなくてもOKです。

Thunderbitを試す 無料
AIでデータを抽出
Google Sheets、Airtable、Notionへ簡単にデータを移行できます
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week