PythonとBeautifulSoupでWebスクレイピング:コーディング前に適切なツールを選ぶ

Python tutorial - IT technology blog
Python tutorial - IT technology blog

私は毎日商品価格を収集する200行ほどの小さなスクリプトからautomationを始めました。今ではそのプロジェクトが2000行まで膨れ上がり、その中でいちばん痛い教訓は最初にツールを間違えたことです——完全にサーバーサイドレンダリングのページなのにSeleniumで1週間費やしてしまい、requests + BeautifulSoupなら2時間で終わる作業でした。

コードを1行書く前に、Pythonにはwebスクレイピングの主なアプローチが3つあることを知っておく必要があります:

  • requests + BeautifulSoup — 静的HTMLをダウンロードし、DOM構造を解析
  • Selenium / Playwright — 実際のブラウザを制御し、JavaScriptのレンダリング完了を待機
  • Scrapy — 大規模クローリング向けのフル機能フレームワーク

3つのアプローチを比較する

この3つのツールは競合しているわけではなく、それぞれ異なる種類の問題を解決します。私が経験したように、最初に間違ったものを選ぶと、書き直しに丸一週間かかることになります。

requests + BeautifulSoup

  • ✅ 5分でインストール、読みやすいコード、GUIなしで動作
  • ✅ 高速・軽量——512MB RAMのVPSでも問題なく動作
  • ✅ 実際のユースケースの大部分に十分対応
  • ❌ JavaScriptを処理できない——React/Vue/Angularを使ったページでは空のHTMLやスケルトンが返される
  • ❌ クリック、スクロール、フォーム送信のシミュレーションができない

Selenium / Playwright

  • ✅ フルJavaScript、SPAのページに対応
  • ✅ あらゆるユーザー操作をシミュレーション可能
  • ❌ ブラウザインスタンスごとに約200MBのRAMを消費し、10〜20倍遅い
  • ❌ 検出されやすい——多くのサイトがヘッドレスブラウザのフィンガープリントを取り、即座にCAPTCHAや403を返す

Scrapy

  • ✅ 非同期で数千ページを並列クロール
  • ✅ パイプライン、ミドルウェア、リトライロジック、CSV/JSONエクスポートが標準搭載
  • ❌ 学習曲線が高い——最初の1行を書く前にSpider、Item、Pipelineを理解する必要がある
  • ❌ 数十ページのスクレイピングプロジェクトには完全にオーバースペック

分析:いつどのツールを使うべきか?

スクレイピングプロジェクトを始める前に私がよく使う判断基準:

  1. DevToolsを開く → View Page Source。取得したいデータがHTMLソースに表示されている → requests + BeautifulSoupを使用。
  2. DevToolsを開く → Networkタブ → XHR/Fetch/api/posts.jsonのようなエンドポイントがクリーンなJSONを返している → そのAPIを直接呼び出す。HTMLを解析する必要はない。
  3. データがスクロール、クリック、ログイン後にのみ表示される → Playwrightを使用(APIがより現代的なためSeleniumより推奨)。
  4. リトライ/パイプラインを使って数百・数千のURLを同時にクロールする必要がある → Scrapyを使用。

スクレイピングを初めて行うなら、requests + BeautifulSoupから始めましょう。5分でインストールでき、安価なVPSでも動作し、JSが不要なほとんどの問題を解決できます。以下でステップごとに解説します。

BeautifulSoupを使ったステップごとの実装

ステップ1:インストール

pip install requests beautifulsoup4 lxml

lxmlはPython標準のhtml.parserより高速なパーサーです。最初からインストールしておくことをお勧めします。

ステップ2:HTMLのダウンロードと基本的な解析

import requests
from bs4 import BeautifulSoup

url = "https://example.com/articles"

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}

response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()  # ステータスが200以外の場合は例外を発生させる

soup = BeautifulSoup(response.text, "lxml")
print(soup.title.text)  # ページタイトルを出力

User-Agentは必ず設定しましょう。このヘッダーなしでリクエストすると403を返したり、全く異なるHTMLを返すサーバーが多くあります。timeout=10はサーバーが応答しない場合にスクリプトが無限に止まるのを防ぎます。

ステップ3:CSSセレクタとfind()で要素を検索

BeautifulSoupには要素を検索する主な方法が2つあります:

# 方法1: find()とfind_all() — タグ、クラス、IDで検索
title = soup.find("h1", class_="article-title")
all_links = soup.find_all("a", href=True)

# 方法2: select() — CSSセレクタ(CSSを知っていれば馴染みやすい)
title = soup.select_one("h1.article-title")
all_links = soup.select("nav a[href]")

# テキストと属性を取得
print(title.get_text(strip=True))
print(all_links[0]["href"])

私はselect()をよく使います。CSSセレクタの方が柔軟で、特に属性による選択や複雑な親子関係が必要な場合に便利です。

ステップ4:実践例——記事一覧のスクレイピング

import requests
from bs4 import BeautifulSoup
import time

def scrape_blog_posts(url):
    headers = {"User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)"}

    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "lxml")
    posts = []

    # 対象ページのHTML構造に合わせてセレクタを調整する
    for article in soup.select("article.post"):
        title_el = article.select_one("h2.entry-title a")
        date_el = article.select_one("time.entry-date")

        if title_el:
            posts.append({
                "title": title_el.get_text(strip=True),
                "url": title_el["href"],
                "date": date_el["datetime"] if date_el else None
            })

    return posts

# 複数ページのクロール——リクエスト間のdelayを忘れずに
all_posts = []
for page in range(1, 6):
    url = f"https://example.com/blog/page/{page}/"
    posts = scrape_blog_posts(url)
    all_posts.extend(posts)
    time.sleep(1.5)  # 1.5秒待機——サーバーへの配慮

print(f"Scraped {len(all_posts)} posts")

time.sleep()はオプションではありません——スクレイピング時の最低限のマナーです。相手のサーバーには、無制限にリクエストに応答する義務はありません。

ステップ5:エッジケースと壊れたHTMLの処理

from urllib.parse import urljoin

# 存在しない要素を安全に処理——スクリプトをクラッシュさせない
price_el = soup.select_one(".product-price")
price = price_el.get_text(strip=True) if price_el else "N/A"

# 再帰的にテキストを取得し、子HTMLタグを除去
description = soup.select_one(".description")
clean_text = description.get_text(separator=" ", strip=True)

# 相対URLを絶対URLに変換
base_url = "https://example.com"
for link in soup.select("a[href]"):
    full_url = urljoin(base_url, link["href"])

# 文字化けが発生した場合のエンコーディング修正
response = requests.get(url, headers=headers)
response.encoding = "utf-8"  # 自動検出ではなくUTF-8を強制的に指定
soup = BeautifulSoup(response.text, "lxml")

始める前に知っておきたかったこと

プロジェクトが2000行まで膨れ上がり、一度作り直しが必要になった経験から、早めに知っておくべきだったことをまとめます:

  1. ElementsタブではなくNetworkタブを調べる。多くのページが隠れたJSON APIでデータを読み込んでいます。DevTools → Network → XHRフィルタを開くと、/api/v1/products.jsonのようなエンドポイントが見つかることがあり、HTMLを解析するよりそのエンドポイントを直接呼び出す方がずっとクリーンです。
  2. デバッグ時はHTMLをローカルに保存する。HTMLファイルをダウンロードしてファイルから読み込みながら開発する——テストのたびにサーバーにアクセスする必要がなく、時間を節約しレート制限も回避できます。
  3. soup.prettify()で構造を把握する。セレクタを書く前にフォーマットされたHTMLを出力する——1行10,000文字のminified HTMLをじっと眺める必要がなくなります。
  4. 事前にrobots.txtを確認するexample.com/robots.txtにアクセスして、クロールが許可されているパスを確認しましょう。法的義務ではありませんが、倫理的なマナーです——商業目的のスクレイピングを禁止している利用規約を持つサイトもあります。
  5. 壊れやすいセレクタは本物の技術的負債div > div:nth-child(3) > spanのようなセレクタは、ページのレイアウトが変わった瞬間に壊れます。.product-titleのような意味のあるクラスやdata-testid属性に基づくセレクタを優先しましょう。

次のステップ

BeautifulSoupは実際のスクレイピングの大部分に対応できます——価格収集、ニュース監視、コンテンツのバックアップ、市場調査など。難しいのはBeautifulSoupのAPIではありません。難しいのは対象ページのHTML構造を正確に読み解き、3ヶ月後にレイアウトが変わっても壊れないセレクタを書くことです。

ツールを変えるべき時は?リトライロジックとエクスポートパイプラインを使って数千のURLを並列クロールする必要がある → Scrapyを試す。隠れたJSON APIのないSPAに遭遇した → Playwrightに切り替える。それ以外は、オーバーエンジニアリングしないこと——BeautifulSoupは一般的なスクレイピングの約80%を解決できます。

Share: