Seleniumでスクレイピングするには、ブラウザでページを開き、必要な要素の表示を待ってからデータを取得し、CSVに保存します。まず1ページ分の取得結果を確認し、その後でページネーションを追加すると、問題の切り分けがしやすくなります。
この記事では、練習用サイト「Quotes to Scrape」のJavaScript版を使い、PythonとSeleniumによるスクレイピングのやり方を順に説明します。インストールからCSV出力、複数ページの取得、ヘッドレス実行、エラー対策まで、実行できるコードと確認手順をまとめました。
押さえておきたいポイント
- Seleniumは、ブラウザでの描画や操作が必要な処理に向いています。利用可能なAPIやHTTPレスポンスに必要なデータが含まれる場合は、そちらのほうが簡潔に実装できることもあります。
- 複数ページを取得する前に、1ページ分の項目の対応とCSV出力を確認します。
- 実行レポートの
last_pageはページ送りの終端、page_limitは指定ページ数への到達、errorは処理の失敗を表します。- CSVの件数が一致しても、サイト内の全データを取得できたとは限りません。
練習用ページでは、JavaScriptが引用文のカードを描画します。ただし、元となるデータはHTMLにも埋め込まれているため、Seleniumでしか取得できないページというわけではありません。掲載画像では、1ページから10件を出力した結果と、ヘッドレス実行で10ページ・100件を取得し、last_pageと終了コード0を確認した結果を示しています。
1. PythonとSeleniumの実行環境を準備する
必要なのはPython 3.10以降、デスクトップ版Chrome、Seleniumです。ここではSelenium 4.51.0にバージョンを固定し、仮想環境へインストールします。
PyPIのSelenium公式パッケージでは、Python 3.10以降をサポート対象としています。作業用フォルダーを作成し、そのフォルダーでターミナルを開いて、次のコマンドを実行してください。
Windows PowerShell:
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install selenium==4.51.0
macOS・Linux:
python3 -m venv .venv
.venv/bin/python -m pip install selenium==4.51.0
仮想環境内のPythonを直接指定するため、仮想環境の有効化は必須ではありません。Windowsでpyが見つからず、python --versionが使える場合は、環境作成のコマンドをpython -m venv .venvに置き換えます。掲載している実行画像はWindowsでの結果です。
ChromeDriverはSelenium Managerで管理する
現在のSeleniumには、必要なドライバーを解決するSelenium Managerが含まれています。ドライバーを明示的に指定していない場合、後述のwebdriver.Chrome(options=options)から必要に応じて呼び出します。
初回は、対応するドライバーの取得にインターネット接続が必要になることがあります。ネットワーク制限、非対応の実行環境、PATH上に残った別バージョンのドライバーなどがある場合は、個別の対応が必要です。
次のコードはselenium.pyではなく、scrape_quotes.pyとして保存します。selenium.pyという名前にすると、インストール済みパッケージより自作ファイルを優先して読み込み、正常にインポートできなくなることがあります。
2. Seleniumで1ページをスクレイピングしてCSVに出力する
最初は、表示中の1ページだけを取得します。引用文・著者・タグをカード単位で読み取り、1件につき1行のCSVとして保存する構成です。
SeleniumとPython標準ライブラリだけで動作するため、BeautifulSoupなどの追加ライブラリは不要です。次のコード全体をscrape_quotes.pyとして保存してください。
import csv
from pathlib import Path
from selenium import webdriver
from selenium.common.exceptions import (
NoSuchElementException, StaleElementReferenceException, WebDriverException
)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
def read_records(driver):
"""Wait until the currently rendered cards have their required fields."""
try:
cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
if not cards:
return False
records = []
for card in cards:
quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
tags = [tag.text for tag in card.find_elements(By.CSS_SELECTOR, ".tag")]
if not quote or not author:
return False
records.append({"quote": quote, "author": author,
"tags": " | ".join(tags)})
return records
except (NoSuchElementException, StaleElementReferenceException):
return False
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=options)
try:
driver.set_page_load_timeout(30)
driver.get("https://quotes.toscrape.com/js/")
records = WebDriverWait(driver, 15).until(read_records)
for record in records:
record["source_url"] = driver.current_url
output = Path("quotes.csv").resolve()
with output.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(
file, fieldnames=["quote", "author", "tags", "source_url"]
)
writer.writeheader()
writer.writerows(records)
print(f"Page: {driver.current_url}")
print(f"Saved {len(records)} records to {output}")
finally:
try:
driver.quit()
except WebDriverException:
pass
スクリプトの実行方法
ファイルの拡張子が.py.txtになっていないことを確認し、保存先のフォルダーで実行します。ターミナルに入力するのは次の実行コマンドです。実行後に表示するPage:やSavedは出力メッセージなので、入力する必要はありません。
Windows PowerShell:
.\.venv\Scripts\python.exe scrape_quotes.py
macOS・Linux:
.venv/bin/python scrape_quotes.py
Chromeのウィンドウが開き、処理が終わると自動で閉じます。次の3点を確認してください。
- ターミナルに、取得先URL・保存件数・
quotes.csvの絶対パスが表示される。 quotes.csvにヘッダーとデータ行がある。- 引用文と著者が、画面上の組み合わせと一致している。
CSVの保存先は現在の作業ディレクトリです。再実行すると同じファイルを上書きするため、以前の結果が必要な場合は先にコピーします。
このスクリプトが取得するのは1ページ分だけです。起動や取得に失敗した場合は、保存成功のメッセージではなくPythonの例外が表示されます。以前のquotes.csvが残っていることもあるため、ファイルの存在だけで判断せず、ターミナルと更新日時を確認してください。

3. 要素の指定・待機・CSV出力の仕組みを理解する
安定した取得には、レコード単位で要素を探し、必要な項目が読み取れる状態を待つことが大切です。固定秒数の待機や、別々に取得した配列の位置合わせに頼ると、欠落や項目のずれを見逃しやすくなります。
CSSセレクターは1件分の構造を確認して決める
ChromeでQuotes to ScrapeのJavaScript版を開き、引用文を右クリックして「検証」を選びます。1件分の構造は次のとおりです。
| 取得対象 | CSSセレクター | 用途 |
|---|---|---|
| カード全体 | .quote |
1件分の取得範囲 |
| 引用文 | .text |
本文 |
| 著者 | .author |
著者名 |
| タグ | .tag |
複数のタグ |
コードでは、まずfind_elements()で.quoteを列挙し、それぞれのカード内でfind_element()を使って引用文と著者を取得します。著者一覧と引用文一覧を別々に取得して順番で組み合わせる方法では、項目が1つ欠けただけで対応がずれるおそれがあります。
XPathも使用できますが、開発者ツールからコピーした長い絶対XPathは、ページの入れ子構造の変更に弱くなりがちです。対象を特定できる属性や、要素同士の関係を基準に指定します。

固定時間ではなく、必要な要素の状態を待つ
driver.get()が待つ範囲はページ読み込み戦略に依存します。呼び出しが戻っても、その後のJavaScriptによる更新まで完了したとは限りません。Selenium公式の待機処理ガイドでも、この違いと、暗黙的な待機と明示的な待機を混在させる際の注意点を説明しています。
今回のWebDriverWait(driver, 15).until(read_records)は、次の条件を満たすまで取得関数を繰り返し呼び出します。
.quoteに一致するカードがある。- 現時点で見つかった全カードで、引用文と著者が空ではない。
- 要素の欠落や参照切れが起きずに読み取れる。
待機時間の上限は15秒です。要素がまだない場合や更新途中で参照が無効になった場合は、制限時間内で再試行します。
ただし、これは練習用ページの描画方法に合わせた条件です。複数回に分けてデータを追加するサイトでは、既知の総件数や読み込みインジケーターなど、別の完了条件が必要になります。presence_of_all_elements_locatedも、一致する要素が現れた時点で条件を満たすことがあり、最終的な全件の到着を保証しません。
表示テキストとリンク先URLを使い分ける
引用文と著者の表示テキストは.textで取得します。一方、ページ送りで必要なリンク先はget_attribute("href")で取得します。画面上のNextという文字列と、次ページのURLは別の値です。
CSVでは、複数のタグを|で連結して1つのフィールドに保存します。後述の複数ページ版では、引用文と著者の組を重複判定に使い、source_urlには採用したレコードを最初に取得したURLを記録します。商品情報を扱うなら、商品名より安定した商品IDをキーにするほうが適切です。
CSVへの書き込みには標準ライブラリを使い、カンマ・引用符・改行を含む値のエスケープを任せています。newline=""はPython公式のCSVガイドに沿った指定です。utf-8-sigは、表計算ソフトでUnicodeの文字コードを認識しやすくするために使います。
件数を確認する際は、テキストの行数ではなくCSVとして読み込んだレコード数を数えてください。 引用符で囲んだフィールド内に改行があると、1件が複数行にまたがるためです。

4. ページネーションで複数ページを取得し、終了状態を確認する
複数ページを取得する際は、「次へ」を押すだけでなく、ページが切り替わったことを確認してから次のデータを読み取ります。取得結果とは別に終了理由を記録すると、最終ページへの到達と途中終了を区別できます。
1ページ分のCSVを確認できたら、次のコード全体を同じフォルダーのscrape_quotes_pages.pyに保存します。データ取得、ページ送り、重複除去、実行レポートの出力をまとめた例です。
import argparse
import csv
import json
from pathlib import Path
from urllib.parse import urljoin
from selenium import webdriver
from selenium.common.exceptions import (
NoSuchElementException, StaleElementReferenceException, WebDriverException
)
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
START_URL = "https://quotes.toscrape.com/js/"
FIELDS = ["quote", "author", "tags", "source_url"]
def read_records(driver):
"""Wait until the currently rendered cards have their required fields."""
try:
cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
if not cards:
return False
records = []
for card in cards:
quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
tags = [tag.text for tag in card.find_elements(By.CSS_SELECTOR, ".tag")]
if not quote or not author:
return False
records.append({"quote": quote, "author": author,
"tags": " | ".join(tags)})
return records
except (NoSuchElementException, StaleElementReferenceException):
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--pages", type=int, default=1)
parser.add_argument("--headless", action="store_true")
args = parser.parse_args()
if args.pages < 1:
parser.error("--pages must be at least 1")
options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
if args.headless:
options.add_argument("--headless=new")
driver = None
rows, seen, visited = [], set(), set()
raw_count = 0
status, detail = "error", "Browser did not start"
try:
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30)
wait = WebDriverWait(driver, 15)
driver.get(START_URL)
for page_number in range(1, args.pages + 1):
current_url = driver.current_url
if current_url in visited:
raise ValueError("Repeated page URL; pagination stopped")
batch = wait.until(read_records)
visited.add(current_url)
raw_count += len(batch)
for record in batch:
key = (record["quote"], record["author"])
if key not in seen:
seen.add(key)
rows.append({**record, "source_url": current_url})
print(f"Page {page_number}: {len(batch)} records | {current_url}")
links = driver.find_elements(By.CSS_SELECTOR, ".pager .next a")
if not links:
status, detail = "last_page", "No Next link on the processed page"
break
if page_number == args.pages:
status, detail = "page_limit", "More pages exist; requested limit reached"
break
expected_url = urljoin(current_url, links[0].get_attribute("href"))
old_card = driver.find_element(By.CSS_SELECTOR, ".quote")
links[0].click()
wait.until(EC.staleness_of(old_card))
wait.until(EC.url_to_be(expected_url))
except (WebDriverException, ValueError) as exc:
status = "error"
detail = f"{type(exc).__name__}: {str(exc).splitlines()[0]}"
if driver is not None:
try:
driver.save_screenshot("failure.png")
except WebDriverException:
pass
finally:
if driver is not None:
try:
driver.quit()
except WebDriverException:
pass
output = Path("quotes-pages.csv").resolve()
with output.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=FIELDS)
writer.writeheader()
writer.writerows(rows)
report = {
"status": status, "detail": detail,
"pages_processed": len(visited), "raw_records": raw_count,
"unique_records": len(rows), "duplicates_removed": raw_count - len(rows),
"csv": str(output)
}
Path("run-status.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(json.dumps(report, indent=2))
if status == "error":
raise SystemExit(1)
if __name__ == "__main__":
main()
最大20ページを取得するコマンド
Windows PowerShell:
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20
macOS・Linux:
.venv/bin/python scrape_quotes_pages.py --pages 20
仮想環境の有効化は不要です。出力先はquotes-pages.csvとrun-status.jsonで、1ページ版のquotes.csvはそのまま残ります。複数ページ版を再実行すると、その出力ファイルを上書きします。
--pages 20の20は、取得するページ数の上限です。サイトに20ページあるという意味ではありません。省略時の上限は1ページで、1未満の値は受け付けません。
ページが切り替わったことをどう判定するか
このコードは、各ページの取得後に次の順序で処理します。
.pager .next aで次ページへのリンクを探す。- リンク先URLと、現在のページにあるカード要素を保持する。
- リンクをクリックする。
staleness_of()で古いカードへの参照が無効になるのを待つ。url_to_be()で想定したURLへの遷移を確認する。- 次のループで、新しいページのレコードが読み取れる状態を待つ。
クリック直後に単に.quoteの存在を待つだけでは、切り替え前の要素でも条件を満たしてしまいます。古い要素の無効化とURLの確認を組み合わせることで、この取り違えを防ぎます。
これは、ページ全体が切り替わる形式のページネーション向けです。画面内のデータだけを更新するSPAでは、ページ番号の変化とレコードIDの更新など、その画面に合った条件へ変更してください。
last_page・page_limit・errorの違い
ページ送りの終了理由と、CSVの出力結果は分けて確認します。まずrun-status.jsonを読み、CSV件数をunique_recordsと照合したうえで、実際の値も確認してください。
status |
終了理由 | 確認するポイント |
|---|---|---|
last_page |
処理済みページに次ページへのリンクがない | セレクターとレイアウトが正しければ、通常のページ送りの終端です。サーバー側の全データが公開・取得できた証明ではありません。 |
page_limit |
次ページはあるが、指定した上限に達した | 指定範囲の処理は終わっていますが、ページ送りの終端には達していません。必要に応じて上限を見直します。 |
error |
起動・取得・画面遷移などに失敗した | 終了コードは非ゼロになります。それまでのデータの出力を試みるため、CSVがあっても成功とは判断できません。 |
レポートには、処理ページ数pages_processed、重複除去前の件数raw_records、重複除去後の件数unique_records、除去した件数duplicates_removedも含まれます。必須項目が読み取れないまま待機時間を超えたページは、処理ページ数に含めません。
重複が0件でも、取りこぼしがないとは限りません。目的に対する取得範囲と、サンプルの値を併せて確認します。

Windowsでは、スクリプト終了直後に終了コードを確認し、CSVとして読み込んだ件数を数えます。
Write-Output "Exit code: $LASTEXITCODE"
Write-Output "CSV records: $(@(Import-Csv .\quotes-pages.csv).Count)"
終了コード0は、エラーとして報告せずに終了したことを表します。page_limitでも0になるため、必ずrun-status.jsonも確認してください。CSV件数とunique_recordsの一致は出力件数の確認であり、各フィールドの正しさやサイト全体の網羅性まで保証するものではありません。
5. 別のサイトに合わせてスクレイピングを変更する
別のページを取得する場合は、URLとセレクターをセットで変更します。URLだけを変えて.quoteを残しても、新しいサイトに対応したスクレイパーにはならず、空の結果やタイムアウトにつながります。
| 変更箇所 | 調整する内容 |
|---|---|
| 開始URL | START_URLを取得が認められた一覧ページへ変更し、リダイレクト後も目的のページに到達するか確認します。 |
| レコードの範囲 | .quoteを繰り返し表示する項目や表の行に置き換え、一致する件数を画面と照合します。 |
| 項目・CSV列・重複判定 | 取得処理、FIELDS、重複判定キーをまとめて変更します。任意項目は必須の待機条件から外します。 |
| ページ送り | .pager .next aを変更し、次のデータが準備できたと判断する条件も見直します。 |
「もっと見る」ボタンと無限スクロールへの対応
「もっと見る」ボタンでデータを追加する画面では、既存の取得結果を保持し、新しいレコードIDなどの追加を待ちます。ページ遷移が起きないなら、古いドキュメント全体が無効になるのを待つ方法は適しません。
無限スクロールでは、表示中のデータを取得し、スクロールして新しいIDの出現を待つ処理を繰り返します。実行時間または反復回数に上限を設けてください。明確な終端表示がなく待機時間を超えた場合は、全件取得済みとせず、終端を確認できなかったことを記録します。
仮想スクロールを使う一覧では、画面外の過去の要素をDOMから削除することがあります。最後に一度だけ取得すると、途中に表示していたデータを取りこぼす可能性があります。
iframe・Shadow DOM・canvasの確認
対象がiframe内にある場合は、該当フレームへ切り替えてから要素を探します。Shadow DOMやcanvas内のコンテンツにも、それぞれに合った取得方法が必要です。待機時間を延ばすだけでは、通常のDOM要素と同じように扱えるようにはなりません。
6. Seleniumで要素が見つからない場合の対策とエラーの切り分け
取得件数が0件になったり、同じページを繰り返したりする場合は、現在のURL・表示内容・要素の指定・遷移状態を確認します。原因を確かめずに待機時間や再試行回数を増やしても、誤ったセレクターや遷移失敗は直りません。
| 症状 | 確認・対処 |
|---|---|
| 要素が見つからない、タイムアウトする | 現在のURLと表示中のDOMを確認します。セレクター、フレームの切り替え、想定したコンテンツの表示有無を点検します。 |
StaleElementReferenceExceptionが出る |
参照していたノードの置換やページ遷移が原因です。更新後のDOMから要素を取得し直します。 |
| 同じページを繰り返す | 次ページの実URLやページ番号を確認します。操作できそうに見えるボタンでも、遷移が完了したとは限りません。 |
| Chromeが起動しない | ドライバーのダウンロード失敗とブラウザの起動失敗を切り分け、例外、Chromeのインストール状況、競合するドライバー、OSの制限を確認します。 |
| CSVが不足している | 1ページ版は例外とquotes.csvの更新日時、複数ページ版はrun-status.jsonとquotes-pages.csvを確認します。途中までの結果だけが残っている場合があります。 |
保存時にPermissionErrorが出る |
Excelなどで開いているCSVを閉じ、フォルダーへの書き込み権限を確認して再実行します。 |
要素の参照は、ページの更新後に自動で新しい要素へ置き換わるわけではありません。詳しくはSelenium公式のエラー解説を参照してください。
同じページを繰り返す場合は、URLの再訪を検出する処理を残し、遷移しない原因を調べます。重複除去だけで同じページの取得を延々と続けないようにします。
また、ブラウザにプロキシを設定しても、Selenium Managerのダウンロード経路まで自動で改善するわけではありません。環境に応じて許可されたネットワークや明示的なドライバー指定を使い、ブラウザのセキュリティ機能を一律に無効化する対処は避けてください。
ファイルへの書き込みに失敗すると、新しい実行レポートも作成できない場合があります。過去のCSVやレポートを今回の結果と取り違えないよう、更新日時まで確認します。
アクセス拒否やCAPTCHAが表示された場合は自動再試行を止め、取得許可、利用規約、利用可能なAPIを確認します。ブロック画面を、通常の「データ0件」のページとして扱わないことが重要です。
7. 動作確認後にヘッドレスモードで実行する
ヘッドレスモードは、ブラウザのウィンドウを表示せずにChromeを動かす方法です。まず通常表示で複数ページの取得を確認し、その後で--headlessを追加します。
この引数に対応しているのはscrape_quotes_pages.pyです。先に紹介した1ページ版には引数を処理するコードがありません。
Windows PowerShell:
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20 --headless
macOS・Linux:
.venv/bin/python scrape_quotes_pages.py --pages 20 --headless
画面幅によるレイアウトの差を抑えるため、通常表示とヘッドレスの両方でウィンドウサイズを1280×900に設定しています。ヘッドレスでもブラウザ自体は動作します。対応する実行方式はChrome公式のヘッドレスモード解説で確認できます。
複数ページ版では、ブラウザのセッションがある状態で取得や遷移に失敗すると、failure.pngへのスクリーンショット保存を試みます。起動前に失敗した場合は画像が残らないことがあります。以前の画像が残っている場合もあるため、確認時は更新日時を見てください。認証情報や個人情報が写り込んでいる画像は、公開せずに扱います。
ヘッドレス化だけで、高速化、コスト削減、アクセス成功を保証できるわけではありません。定期実行へ移す前に、通常表示での取得データと終了状態を比較します。
8. Seleniumのスクレイピングで使うプロキシを選ぶ
プロキシは、ブラウザのアクセス元となる出口回線を選ぶために使います。価格調査や地域別コンテンツの確認では、必要な地域と、同じIPアドレスを維持する時間を先に決めると選びやすくなります。
| 利用目的 | 確認する条件 |
|---|---|
| 地域ごとの価格や表示内容を比較する | 対応地域と、ブラウザが実際に使う出口IPの所在地 |
| 独立したページをバッチ単位で取得する | バッチ間でのIP切り替えの要否と、アクセス間隔 |
| ページネーションやセッション依存の処理を続ける | 一連の処理中に同じ出口IPを維持できるか |
| 同じ出口IPで定期的に確認する | 固定IPの利用期間と保守・管理の条件 |
出口の切り替えやバッチ単位の取得が必要な場合は、Socks5.IOのローテーション型住宅用プロキシが検討対象になります。
長期間同じIPを使う定期処理では、固定住宅用プロキシの条件を比較してください。接続前に、選択する製品の対応地域、セッション維持時間、認証方式を確認します。

接続方法は、Socks5.IOのSeleniumのプロキシ設定で、契約する製品に対応した内容を確認します。導入時は次の順序で検証してください。
- 接続先を設定し、実際の出口IPと所在地を確認する。
- 1ページ版でCSV出力を確認する。
- 同じセッション設定でページネーションを確認する。
- 結果と終了状態に問題がないことを確認してから、取得ページ数を増やす。
Chromeのプロキシ認証には、対応した設定方法が必要です。--proxy-serverにusername:passwordを含めるだけでは認証を設定できません。対象サイトの許可範囲とリクエスト上限を守り、地域とセッションの要件に合う構成を選びます。






