requests-html 完全ガイド:PythonでのWebスクレイピングを快適に 🐍✨

Python

Requestsライクな手軽さとJavaScriptレンダリング機能を両立!

はじめに:requests-htmlとは? 🤔

Webスクレイピングは、Webサイトからデータを抽出するための強力な技術です。Pythonには多くのスクレイピング用ライブラリが存在しますが、その中でも「requests-html」は、特にその使いやすさと機能性で注目を集めています。

requests-htmlは、有名なHTTPライブラリ「Requests」の作者であるKenneth Reitz氏によって開発されました。その名前が示す通り、Requestsライブラリのシンプルなインターフェースを踏襲しつつ、HTMLのパース(解析)や要素の検索機能を統合しています。これにより、HTTPリクエストの送信からHTMLコンテンツの解析までを、非常に直感的に行うことができます。

requests-htmlの主な特徴

  • RequestsライクなAPI: Requestsを使ったことがある人なら、すぐに馴染めるシンプルなインターフェース。
  • 統合されたパーサー: Beautiful Soupやlxmlのような外部パーサーライブラリを別途導入する必要が少ない。内部ではlxmlやpyqueryなどが利用されています。
  • CSSセレクタとXPath: 使い慣れた方法でHTML要素を簡単に選択可能。
  • JavaScriptレンダリング: ✨ これがrequests-htmlの最大の魅力!内部でPyppeteer(Headless Chrome/Chromiumを操作するライブラリ)を使い、JavaScriptによって動的に生成されるコンテンツも取得できます。
  • 非同期サポート: 🚀 `async`/`await` を使った非同期処理に対応しており、大量のリクエストを効率的に処理できます。

特に、JavaScriptを多用する現代的なWebサイト(シングルページアプリケーションなど)からデータを取得したいけれど、SeleniumやPlaywrightのような本格的なブラウザ自動化ツールを使うのは少し大げさだと感じる場合に、requests-htmlは非常に強力な選択肢となります。

このガイドでは、requests-htmlの基本的な使い方から、JavaScriptレンダリング、非同期処理、他のライブラリとの比較、そして利用上の注意点まで、幅広く解説していきます。さあ、requests-htmlを使って快適なWebスクレイピングの世界へ飛び込みましょう!

インストール 💻

requests-htmlのインストールは、Pythonのパッケージ管理ツールであるpipを使って簡単に行えます。

pip install requests-html

これで基本的な機能は利用可能になります。しかし、requests-htmlの目玉機能であるJavaScriptレンダリングを使用するには、追加のステップが必要です。

requests-htmlはJavaScriptの実行にPyppeteerというライブラリを使用し、Pyppeteerは内部でHeadless Chrome/Chromiumブラウザを操作します。初めてJavaScriptレンダリング機能 (`render()` メソッド) を呼び出す際に、必要なChromiumが自動的にダウンロードされることがあります。

もし自動ダウンロードがうまくいかない場合や、事前に準備しておきたい場合は、以下のコマンドを手動で実行してChromiumをダウンロードできます。

python -m pyppeteer download

注意: PyppeteerおよびChromiumのダウンロード/セットアップは、環境によってはプロキシ設定や権限の問題で失敗することがあります。また、Chromium自体が数十MB〜数百MBのサイズになるため、ディスク容量とネットワーク帯域が必要です。

requests-htmlやPyppeteerの開発状況によっては、最新のPythonバージョンやOSとの互換性に問題が生じる可能性もあります。もしインストールや実行で問題が発生した場合は、requests-htmlやPyppeteerのGitHubリポジトリのIssueを確認することをお勧めします。

基本的な使い方 🛠️

requests-htmlの基本的な使い方は非常に直感的です。Requestsライブラリの経験があれば、すんなりと理解できるでしょう。

1. HTMLSessionの開始

まず、`HTMLSession` オブジェクトを作成します。これは `requests.Session` を継承しており、Cookieの永続化や接続のプーリングなど、Requestsのセッション機能を引き継いでいます。

from requests_html import HTMLSession

# セッションを開始
session = HTMLSession()

2. Webページへのリクエスト送信

`session` オブジェクトのメソッド(`get()`, `post()` など)を使って、WebページにHTTPリクエストを送信します。これはRequestsライブラリと全く同じです。

# GETリクエストを送信
url = 'https://python.org/'
r = session.get(url)

# レスポンスのステータスコードを確認
print(f"ステータスコード: {r.status_code}")

# レスポンスURLを確認 (リダイレクトされた場合など)
print(f"URL: {r.url}")

3. レスポンスオブジェクトの操作

`session.get()` (や `post()` など) の戻り値は `requests.Response` オブジェクトを拡張したものです。基本的な属性 (`text`, `content`, `encoding`, `status_code`, `headers` など) はRequestsと同様に使えます。

requests-html特有の機能として、パースされたHTMLにアクセスするための `html` 属性が追加されています。

# レスポンスのHTMLコンテンツ (文字列)
# print(r.text)

# パースされたHTMLオブジェクトにアクセス
html = r.html
print(type(html)) # <class 'requests_html.HTML'>

この `r.html` オブジェクト (`requests_html.HTML` クラスのインスタンス) を使って、HTML要素の検索やデータの抽出を行います。

4. HTML要素の検索 (CSSセレクタ)

`html.find()` メソッドを使うと、CSSセレクタを使って要素を検索できます。これは非常に強力で、Web開発者にはお馴染みの方法です。

# すべてのリンク (<a> タグ) を検索
all_links = html.find('a')
print(f"ページ内のリンク数: {len(all_links)}")

# 特定のクラスを持つ要素を検索 (例: class="introduction")
intro_elements = html.find('.introduction')

# IDで要素を検索 (例: id="about")
about_section = html.find('#about', first=True) # first=True で最初に見つかった要素のみ取得

if about_section:
    print("ID 'about' の要素が見つかりました。")

# 複数のセレクタを組み合わせる (例: divタグ内のクラス widget の要素)
widgets_in_div = html.find('div .widget')

`find()` メソッドは要素のリストを返します。もし最初に見つかった要素だけが必要な場合は、`first=True` オプションを指定すると便利です。要素が見つからない場合は空のリスト `[]` が返ります (`first=True` の場合は `None`)。

5. HTML要素の検索 (XPath)

CSSセレクタに加えて、XPathを使った要素検索も可能です。`html.xpath()` メソッドを使います。

# XPathで特定のh2要素を検索
h2_elements = html.xpath('//h2')
print(f"ページ内のH2タグ数: {len(h2_elements)}")

# 特定の属性を持つ要素をXPathで検索 (例: href属性を持つすべての a タグ)
links_with_href = html.xpath('//a[@href]')

# 最初のh2要素を取得
first_h2 = html.xpath('//h2', first=True)

if first_h2:
    print("最初のH2要素が見つかりました。")

XPathはCSSセレクタよりも複雑な条件を指定できる場合があります。どちらを使うかは、好みや状況に応じて選択してください。`xpath()` も `first=True` オプションをサポートしています。

6. 要素からのデータ抽出

要素を見つけたら、その要素からテキストや属性値などのデータを抽出します。

# 例として、最初のh2要素を取得
first_h2 = html.find('h2', first=True)

if first_h2:
    # 要素のテキストコンテンツを取得
    print(f"H2テキスト: {first_h2.text}")

    # 要素のHTML全体を取得 (タグを含む)
    print(f"H2 HTML: {first_h2.html}")

    # 要素の属性を辞書として取得
    print(f"H2 属性: {first_h2.attrs}") # 例: {'class': ['some-class'], 'id': 'main-title'}

    # 特定の属性値を取得 (例: 'class' 属性)
    h2_class = first_h2.attrs.get('class')
    if h2_class:
        print(f"H2 クラス: {h2_class}") # リストで返ることが多い ['some-class']

# すべてのリンクのURLとテキストを取得
all_links = html.find('a')
for link in all_links:
    href = link.attrs.get('href')
    link_text = link.text
    if href:
        print(f"リンクテキスト: {link_text}, URL: {href}")

7. リンクの抽出と絶対URL

ページ内のリンクを簡単に抽出するための便利なプロパティも用意されています。

# すべてのリンクURLをセットとして取得 (相対URLも含む)
all_hrefs = html.links
# print(all_hrefs) # 例: {'/about', 'contact.html', 'https://example.com'}

# すべてのリンクURLを絶対URLに変換してセットとして取得
absolute_hrefs = html.absolute_links
# print(absolute_hrefs) # 例: {'https://python.org/about', 'https://python.org/contact.html', 'https://example.com'}

# 特定のリンクの絶対URLを取得
first_link = html.find('a', first=True)
if first_link:
    print(f"最初のリンクの絶対URL: {first_link.absolute_links}") # セットで返ることに注意

`absolute_links` は、相対パス (`/about`, `contact.html` など) をページのベースURLに基づいて自動的に絶対URL (`https://python.org/about` など) に変換してくれるため、非常に便利です。

簡単なスクレイピング例: Python公式サイトのニュースを取得

これまでの知識を組み合わせて、Python公式サイト (https://www.python.org/) のトップページにあるニュースの見出しとリンクを取得してみましょう。

(※ サイトの構造は変更される可能性があるため、以下のセレクタは執筆時点のものです)

from requests_html import HTMLSession

session = HTMLSession()
url = 'https://www.python.org/'
r = session.get(url)

# レスポンスが成功したか確認
if r.status_code == 200:
    # ニュースセクションの要素を取得 (例: class="medium-widget blog-widget")
    news_widget = r.html.find('.blog-widget', first=True)

    if news_widget:
        print("Pythonニュース:")
        # ニュースリストの各項目 (li タグ) を取得
        news_items = news_widget.find('li')
        for item in news_items:
            # 各項目内のリンク (a タグ) を取得
            link = item.find('a', first=True)
            if link:
                news_title = link.text
                news_url = list(link.absolute_links)[0] # absolute_links はセットなのでリストに変換して取得
                print(f"- {news_title} ({news_url})")
    else:
        print("ニュースセクションが見つかりませんでした。")

else:
    print(f"ページの取得に失敗しました。ステータスコード: {r.status_code}")

# セッションを閉じる (任意ですが、リソース解放のために推奨)
session.close()

このように、requests-htmlを使えば、数行のコードでWebページから情報を取得し、必要なデータを抽出できます。

JavaScriptレンダリング ✨

requests-htmlの最も強力な機能の一つが、JavaScriptレンダリング機能です。現代のWebサイトの多くは、ページの初期ロード後にJavaScriptを実行してコンテンツを動的に生成・表示します。通常のHTTPリクエスト(`requests` ライブラリなど)では、このJavaScript実行後のHTMLを取得できません。

しかし、requests-htmlの `render()` メソッドを使えば、内部でHeadless Chrome/Chromiumを起動し、ページ内のJavaScriptを実行させたのHTMLを取得できます!これにより、React, Vue, Angularなどで構築されたSPA(シングルページアプリケーション)や、Ajaxで遅延ロードされるコンテンツもスクレイピング対象に含めることができます。

`render()` メソッドの使い方

`render()` メソッドは、`session.get()` などで取得したレスポンスオブジェクト `r` に対して呼び出します。

from requests_html import HTMLSession

session = HTMLSession()
url = 'https://example.com/dynamic-page' # JavaScriptでコンテンツが生成されるページのURL
r = session.get(url)

# JavaScriptを実行し、レンダリング後のHTMLを取得
# この時点で内部的にChromiumが起動し、ページが表示され、JSが実行される
r.html.render()

# レンダリング後のHTMLに対して要素を検索
dynamic_content = r.html.find('#dynamically-loaded-content', first=True)

if dynamic_content:
    print("動的に読み込まれたコンテンツ:")
    print(dynamic_content.text)
else:
    print("動的コンテンツが見つかりませんでした。")

session.close()

重要: 初めて `render()` を実行する際には、前述の通りChromiumのダウンロードが行われることがあります。また、`render()` の実行は実際のブラウザをバックグラウンドで起動するため、通常のHTTPリクエストよりも時間がかかり、CPUやメモリリソースを多く消費します。

`render()` の引数

`render()` メソッドには、レンダリングの挙動を制御するための引数を指定できます。

引数説明デフォルト値
sleepレンダリング後に指定秒数だけ待機します。JavaScriptの実行完了を待つために使います。0
wait非推奨 (deprecated): 以前は `sleep` と同様の目的で使われていました。代わりに `sleep` を使用してください。0.2
timeoutレンダリング処理全体のタイムアウト秒数。この時間を超えるとエラーが発生します。8
scrolldown指定回数だけページを下にスクロールします。無限スクロールでコンテンツが読み込まれるページなどに有効です。0
scriptページ読み込み後に実行するJavaScriptコードを文字列で指定します。例えば、特定のボタンをクリックするなどの操作が可能です。None
keep_pageTrue にすると、レンダリングに使用したブラウザのページを開いたままにします。デバッグや後続の操作に役立ちます。r.html.page でPyppeteerのPageオブジェクトにアクセスできます。False
reloadTrue の場合、ページを再読み込みしてからレンダリングします。デフォルトは `True` です。キャッシュを使わずに最新の状態でレンダリングしたい場合に有効です。True
retriesレンダリングに失敗した場合のリトライ回数。0
# 例: 5秒待機し、3回下にスクロールしてからレンダリング
r.html.render(sleep=5, scrolldown=3)

# 例: 特定の要素が表示されるまで待機するJavaScriptを実行
# (Pyppeteerの waitForSelector を利用)
script = """
    () => {
        // ここにブラウザのコンテキストで実行したいJSを書く
        // 例えば、特定のボタンをクリックする
        // document.querySelector('#load-more-button').click();
        // waitForSelector は requests-html 内部で使われるため、ここでは単純な操作が良い
    }
"""
# 実際には、特定の要素が現れるまで待つといった高度な待機は、
# Pyppeteerを直接使うか、sleepで十分な時間を取るのが一般的
# r.html.render(script=script, sleep=3) # スクリプト実行後、3秒待機

# ページの表示を維持して、後で Pyppeteer の機能を使う
# r.html.render(keep_page=True)
# page = r.html.page # PyppeteerのPageオブジェクトを取得
# await page.screenshot({'path': 'screenshot.png'}) # スクリーンショットを撮る (非同期処理が必要)
# await page.close() # 手動で閉じる必要がある

どのくらいの `sleep` 時間が必要かは、対象サイトのJavaScriptが完了するまでの時間によります。試行錯誤が必要になることもあります。

JavaScriptレンダリングの注意点 ⚠️

  • リソース消費: 前述の通り、CPUとメモリを多く消費します。大量のURLに対して `render()` を行う場合は、マシンスペックに注意が必要です。
  • 時間: 通常のHTTPリクエストより格段に時間がかかります。
  • Chromium/Pyppeteer依存: 環境によってはセットアップが難しい場合があります。また、これらのコンポーネントのアップデートによっては、requests-htmlとの互換性問題が発生する可能性があります。
  • ライブラリのメンテナンス状況: requests-html自体の開発が近年あまり活発ではないため、将来的な互換性やバグ修正に不安が残る可能性があります。(詳細は後述)

それでも、手軽にJavaScriptレンダリングを行いたい場合には、requests-htmlは依然として魅力的な選択肢です。

非同期処理 (Async Support) 🚀

Webスクレイピングでは、多数のURLに対してリクエストを送信する場面がよくあります。同期処理(一つずつ順番にリクエストを送り、レスポンスを待つ)では、ネットワークの待ち時間が積み重なり、全体の処理時間が非常に長くなってしまいます。

requests-htmlは、Pythonの `asyncio` ライブラリをベースにした非同期処理をサポートしており、これにより複数のリクエストを並行して効率的に処理できます。パフォーマンスの大幅な向上が期待できます!

`AsyncHTMLSession` の使い方

非同期処理を行うには、通常の `HTMLSession` の代わりに `AsyncHTMLSession` を使用します。基本的な使い方は `HTMLSession` と似ていますが、リクエスト送信などのI/O関連の操作は `async`/`await` 構文を使って記述します。

import asyncio
from requests_html import AsyncHTMLSession

async def fetch_url(session, url):
    """指定されたURLから非同期にコンテンツを取得する関数"""
    try:
        # 非同期でGETリクエストを送信
        r = await session.get(url)
        # レスポンスが成功したか確認
        r.raise_for_status() # エラーがあれば例外を発生させる
        print(f"取得成功: {url} (ステータス: {r.status_code})")
        # ここでHTMLのパースやデータ抽出を行う (例)
        title = r.html.find('title', first=True)
        if title:
            print(f"  タイトル: {title.text}")
        return r # レスポンスオブジェクトを返す
    except Exception as e:
        print(f"取得失敗: {url} - {e}")
        return None

async def main():
    # 非同期セッションを開始
    asession = AsyncHTMLSession()

    urls = [
        'https://python.org/',
        'https://github.com/',
        'https://httpbin.org/get',
        'https://invalid-url-example.xyz', # 失敗する例
    ]

    # 各URLに対する非同期タスクを作成
    tasks = [fetch_url(asession, url) for url in urls]

    # すべてのタスクを並行して実行し、結果を待つ
    results = await asyncio.gather(*tasks)

    print("\n--- 全ての処理が完了しました ---")
    # 結果を処理 (resultsには各fetch_urlの戻り値(レスポンス or None)が入る)
    successful_fetches = [res for res in results if res is not None]
    print(f"成功したリクエスト数: {len(successful_fetches)}")

    # 非同期セッションを閉じる (重要)
    await asession.close()

# イベントループを開始して main() 関数を実行
if __name__ == "__main__":
    # Python 3.7以降では asyncio.run() が推奨される
    asyncio.run(main())
    # それ以前のバージョンでは loop = asyncio.get_event_loop(); loop.run_until_complete(main())

`AsyncHTMLSession().get()` はコルーチン(`await` で待機可能なオブジェクト)を返します。`asyncio.gather()` を使うことで、複数のコルーチンを並行に実行し、すべての完了を待つことができます。これにより、I/O待ち時間を有効活用し、全体の処理時間を短縮できます。

非同期JavaScriptレンダリング: `arender()`

JavaScriptレンダリングも非同期で行うことができます。`render()` の代わりに `arender()` メソッドを使用します。引数は `render()` と同様です。

import asyncio
from requests_html import AsyncHTMLSession

async def fetch_and_render(session, url):
    try:
        r = await session.get(url)
        r.raise_for_status()
        print(f"取得成功: {url}")

        # 非同期でJavaScriptレンダリングを実行
        # sleepやscrolldownなどの引数も同様に指定可能
        await r.html.arender(sleep=1)
        print(f"レンダリング完了: {url}")

        # レンダリング後のHTMLからデータを抽出
        dynamic_content = r.html.find('#dynamic-content', first=True)
        if dynamic_content:
            print(f"  動的コンテンツ発見: {dynamic_content.text[:50]}...") # 長すぎる場合は一部表示
        else:
            print(f"  動的コンテンツが見つかりません。")
        return r
    except Exception as e:
        print(f"処理失敗: {url} - {e}")
        return None

async def main():
    asession = AsyncHTMLSession()
    urls = [
        'https://example.com/dynamic-page1',
        'https://example.com/dynamic-page2',
    ]
    tasks = [fetch_and_render(asession, url) for url in urls]
    results = await asyncio.gather(*tasks)
    print("\n--- 全てのレンダリング処理が完了しました ---")
    await asession.close()

if __name__ == "__main__":
    asyncio.run(main())

`arender()` を使うことで、時間のかかるJavaScriptレンダリング処理も並行して実行でき、全体のパフォーマンスをさらに向上させることが可能です。ただし、`render()` 同様、リソース消費には注意が必要です。同時に実行するレンダリング処理の数を適切に制限しないと、マシンが不安定になる可能性があります。`asyncio.Semaphore` などを使って同時実行数を制御することを検討してください。

非同期処理は、特に多数のWebページを効率的にスクレイピングしたい場合に非常に有効なテクニックです。requests-htmlがこれをサポートしている点は大きな利点と言えるでしょう。

便利な機能と高度な使い方 🧐

requests-htmlには、基本的な機能に加えて、スクレイピング作業をさらに効率化・高度化するための便利な機能がいくつか備わっています。

要素検索の深掘り

  • `find(…, first=True)`: すでに紹介しましたが、最初に見つかった要素のみを取得したい場合に非常に便利です。`html.find(‘#main-content’)[0]` のようにインデックスでアクセスするよりも安全です(要素がない場合に `IndexError` が発生しない)。
  • `find(…, containing=’特定のテキスト’)`: 指定したテキストを含む要素を検索できます。これはCSSセレクタだけでは難しい場合に役立ちます。
    # 'ログイン' というテキストを含むボタン要素を探す
    login_button = html.find('button', containing='ログイン', first=True)
    if login_button:
        print("ログインボタンが見つかりました。")
    
    # '価格' というテキストを含むすべての li 要素を探す
    price_items = html.find('li', containing='価格')
    for item in price_items:
        print(f"価格関連の項目: {item.text}")

    ※ `containing` は内部的にXPathの `contains()` 関数を使っているため、要素の直接の子テキストノードだけでなく、子孫要素のテキストも検索対象になります。

ページ内検索: `search()` と `search_all()`

`find()` や `xpath()` がHTMLの構造に基づいて要素を検索するのに対し、`search()` と `search_all()` は、パースされたHTMLテキスト全体に対して、Pythonのフォーマット文字列に似た構文でパターンマッチングを行い、データを抽出します。

html_content = """
    <div class="product">
        <h2>商品A</h2>
        <p>価格: <span class="price">1,980</span>円</p>
        <p>コード: PX-12345</p>
    </div>
    <div class="product">
        <h2>商品B</h2>
        <p>価格: <span class="price">2,500</span>円</p>
        <p>コード: QZ-67890</p>
    </div>
    """

from requests_html import HTML

html = HTML(html=html_content) # 文字列からHTMLオブジェクトを作成

# 最初の商品の価格とコードを抽出
# {} は抽出したい部分を示すプレースホルダ
pattern = '価格: <span class="price">{}</span>円</p>{}<p>コード: {}</p>'
result = html.search(pattern)

if result:
    print(f"最初の商品の価格: {result[0]}") # '1,980'
    # result[1] には  と 

の間の空白や改行が含まれる可能性がある print(f"最初の商品のコード: {result[2]}") # 'PX-12345' # すべての商品の名前、価格、コードを抽出 # search_all はイテレータを返す pattern_all = '<h2>{}</h2>{}価格: <span class="price">{}</span>円{}コード: {}</p>' all_results = html.search_all(pattern_all) print("\nすべての商品情報:") for res in all_results: print(f"- 名前: {res[0]}, 価格: {res[2]}, コード: {res[4]}")

`search()` は最初に見つかったマッチの結果 (固定長のタプル) を返します。`search_all()` はすべてのマッチ結果を反復処理できるイテレータを返します。

この機能は、HTML構造が少し複雑でCSSセレクタやXPathで狙ったデータを抽出しにくい場合に、特定のテキストパターンに基づいてデータを抜き出す代替手段として役立ちます。ただし、HTML構造のわずかな変更でパターンが壊れやすいという欠点もあります。

Requestsの機能をそのまま活用

`HTMLSession` や `AsyncHTMLSession` は、それぞれ `requests.Session` と `requests.Session` (非同期版は内部実装が異なるがインターフェースは類似) をベースにしています。そのため、Requestsライブラリが提供する豊富な機能をそのまま利用できます。

  • カスタムヘッダー: User-Agentの偽装など。
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
    session = HTMLSession()
    session.headers.update(headers)
    r = session.get(url)
  • プロキシ設定:
    proxies = {
      'http': 'http://10.10.1.10:3128',
      'https': 'http://10.10.1.10:1080',
    }
    session = HTMLSession()
    session.proxies.update(proxies)
    r = session.get(url) # プロキシ経由でリクエスト
  • タイムアウト設定:
    try:
        # 接続5秒、読み取り10秒のタイムアウト
        r = session.get(url, timeout=(5, 10))
    except requests.exceptions.Timeout:
        print("リクエストがタイムアウトしました。")
  • 認証: Basic認証やDigest認証など。
    from requests.auth import HTTPBasicAuth
    r = session.get(url, auth=HTTPBasicAuth('user', 'pass'))
  • POSTリクエストとデータ送信:
    payload = {'key1': 'value1', 'key2': 'value2'}
    r = session.post(login_url, data=payload)
    # POST後のページで要素を探す
    welcome_message = r.html.find('#welcome', first=True)

    (※ フォーム送信に関しては、requests-html自体にはSeleniumのような入力・クリック機能はありません。単純なPOSTリクエストとしてデータを送る形になります。CSRFトークンなどが必要な場合は、事前に取得してデータに含める必要があります。)

このように、Requestsでできることは基本的にrequests-htmlでもできると考えて良いでしょう。これにより、柔軟なHTTPリクエスト制御とHTML解析を一つのライブラリで完結させることができます。

他のライブラリとの比較 🆚

PythonにはWebスクレイピングのためのライブラリが多数存在します。requests-htmlを他の主要なライブラリと比較し、それぞれの長所・短所、そして使い分けについて見ていきましょう。

ライブラリ主な特徴長所 👍短所 👎適した用途
requests-htmlRequests + HTML解析 + JSレンダリング (Pyppeteer)
  • Requestsライクで手軽
  • JSレンダリング機能内蔵
  • CSSセレクタ/XPath対応
  • 非同期対応
  • JSレンダリングが重い
  • 開発がやや停滞気味?
  • Pyppeteer依存の問題
  • 大規模クロールには機能不足
  • 小〜中規模のスクレイピング
  • JSレンダリングが必要なサイト
  • Requestsに慣れている人
  • 手軽に始めたい場合
requests + Beautiful Soup 4定番の組み合わせ (HTTP + HTML解析)
  • シンプルで理解しやすい
  • Beautiful SoupのHTML/XML解析が柔軟
  • 安定しており情報が多い
  • 依存関係が少ない
  • JSレンダリング不可
  • 2つのライブラリを組み合わせる必要あり
  • 非同期処理は別途asyncio + aiohttpなどが必要
  • 静的なWebサイトのスクレイピング
  • 学習用、基本的なスクレイピング
  • 壊れたHTMLの解析
Scrapy高機能スクレイピングフレームワーク
  • 大規模クロールに最適
  • 非同期ベースで高速
  • 拡張性が高い (Middleware, Pipeline)
  • データ抽出、保存機能が豊富
  • スケジューリング、リトライなど内蔵
  • 学習コストが高い
  • フレームワーク特有の作法がある
  • 単純なタスクにはやや大げさ
  • JSレンダリングは別途対応 (scrapy-splash, scrapy-playwrightなど) が必要
  • 大規模、継続的なクローリング
  • 複雑なデータ処理パイプラインが必要な場合
  • パフォーマンスが重要なプロジェクト
Seleniumブラウザ自動操作ツール (テスト用途が主)
  • 完全なブラウザ環境で動作
  • JSレンダリング、クリック、入力など複雑な操作が可能
  • デバッグが視覚的に行いやすい (非Headless時)
  • 複数ブラウザ対応 (Chrome, Firefox, etc.)
  • 動作が重く、リソース消費大
  • スクレイピング専用ではない
  • 設定やWebDriverの管理が必要
  • 速度は遅め
  • 非同期処理は扱いにくい
  • 複雑なユーザー操作が必要なサイト
  • Webアプリケーションのテスト自動化
  • JSレンダリングが必須で、requests-html/Pyppeteerで不十分な場合
Playwrightモダンなブラウザ自動操作ツール (Microsoft製)
  • Seleniumより高速な場合が多い
  • 強力な非同期API
  • 自動待機機能が賢い
  • ネットワーク要求の傍受/変更など高度な機能
  • 複数ブラウザ対応 (Chromium, Firefox, WebKit)
  • インストールが比較的容易
  • まだSeleniumほど情報が多くない可能性
  • 動作はrequests系より重い
  • 比較的新しいライブラリ
  • Seleniumの代替として
  • 非同期処理を多用するブラウザ操作
  • 高度なブラウザ操作やテスト
  • JSレンダリングが必須で高機能が求められる場合

requests-htmlの立ち位置

上記の比較から、requests-htmlは以下のような立ち位置にあると言えます。

「手軽さ」と「JavaScriptレンダリング」のバランスが良い選択肢

  • Requests + Beautiful Soup よりは高機能(特にJSレンダリング)。
  • Scrapy よりは手軽で学習コストが低いが、大規模処理や拡張性では劣る。
  • Selenium/Playwright よりは軽量でスクレイピングに特化している部分もあるが、ブラウザ操作の自由度や安定性では劣る可能性がある。

したがって、「Requestsライブラリは使い慣れている」「少しJavaScriptで動的に変わるサイトからデータを取得したい」「でもSeleniumやScrapyを学ぶほどではない」といった場合に、requests-htmlは最初の候補として検討する価値があるでしょう。

ただし、後述する開発状況の懸念から、長期的なプロジェクトや、より安定性・高機能性が求められる場面では、PlaywrightやScrapy (+ scrapy-playwrightなど) といった他の選択肢を検討する方が良いかもしれません。

注意点とベストプラクティス ⚠️

requests-htmlを使ってWebスクレイピングを行う際には、技術的な側面だけでなく、倫理的・法的な側面にも注意を払う必要があります。また、ライブラリ自体の特性に関する注意点も存在します。

Webサイトへの配慮

  • 負荷をかけすぎない: 短時間に大量のリクエストを送ると、相手のサーバーに過大な負荷をかけてしまい、サービス停止につながる可能性があります。これは絶対に避けるべきです。
    • 対策: リクエスト間に適切な待機時間 (`time.sleep()`) を設けましょう。非同期処理の場合も、同時接続数を制限する (`asyncio.Semaphore`) などの工夫が必要です。
  • `robots.txt` を尊重する: 多くのWebサイトは、ルートディレクトリに `robots.txt` というファイルを設置し、クローラーがアクセスして良い範囲や、アクセス頻度のガイドラインを示しています。この指示には従うべきです。
    • 対策: スクレイピング対象サイトの `robots.txt` を確認し、`Disallow` で指定されたパスへのアクセスは避けましょう。`Crawl-delay` が指定されていれば、その秒数以上の間隔を空けてアクセスします。Pythonには `urllib.robotparser` などのライブラリがあります。
  • 利用規約を確認する: サイトによっては、利用規約でスクレイピングを明示的に禁止している場合があります。規約違反は法的な問題に発展する可能性もあるため、必ず確認しましょう。

技術的なベストプラクティス

  • User-Agentを設定する: デフォルトの `python-requests` のようなUser-Agentだと、ボットとして認識されアクセスをブロックされることがあります。一般的なブラウザのUser-Agent文字列を設定することが推奨されます。可能であれば、スクリプトの目的や連絡先をUser-Agentに含めることも考慮しましょう (例: `MyCoolScraperBot/1.0 (+http://example.com/botinfo)`)。
    session = HTMLSession()
    session.headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...' # 適切なUA文字列に
  • エラーハンドリングを実装する: ネットワークエラー、HTTPエラー(404 Not Found, 503 Service Unavailableなど)、HTML構造の変更による要素の見つからないエラーなど、スクレイピングには様々なエラーがつきものです。`try-except` ブロックを使って適切にエラーを捕捉し、処理(リトライ、スキップ、ログ記録など)を行うようにしましょう。
    try:
        r = session.get(url, timeout=10)
        r.raise_for_status() # 4xx, 5xx エラーで例外を発生させる
        element = r.html.find('#target', first=True)
        if not element:
            print("要素が見つかりませんでした。")
            # または raise ValueError("Target element not found") など
        # ... データ処理 ...
    except requests.exceptions.RequestException as e:
        print(f"リクエストエラー: {e}")
    except Exception as e:
        print(f"予期せぬエラー: {e}")
  • JavaScriptレンダリングのリソース管理: `render()` や `arender()` はリソースを大量に消費します。不要なレンダリングは避け、必要な場合にのみ使用しましょう。非同期で多数のレンダリングを行う場合は、前述の通り同時実行数を制限することが重要です。`keep_page=True` を使用した場合は、不要になったページオブジェクト (`r.html.page`) を適切に閉じる (`await page.close()`) 必要があります。
  • セレクタの堅牢性: HTML構造は変更される可能性があります。ID属性など、変更されにくい要素を基点にしたり、複数のセレクタ候補を用意したりするなど、変更に強いセレクタの書き方を心がけましょう。`find(…, containing=…)` や `search()` は便利ですが、テキストの変更にも弱い点に注意が必要です。

⚠️ ライブラリのメンテナンス状況に関する注意

requests-htmlは非常に便利なライブラリですが、2024年初頭現在、開発が活発とは言えない状況にあります。

  • GitHubリポジトリ (https://github.com/psf/requests-html) を見ると、コミットやIssueへの対応頻度が低下している傾向が見られます (2021年頃から顕著)。
  • 依存しているPyppeteer自体も、オリジナルのPuppeteer (Node.js) に比べて開発が遅れがちであり、最新のChrome/Chromiumとの互換性問題やバグが放置されている可能性があります。

この状況が意味すること:

  • 新たなPythonバージョンやOS、依存ライブラリのアップデートによって、予期せぬエラーが発生するリスクが高まる可能性があります。
  • 発見されたバグが修正されるまでに時間がかかる、あるいは修正されない可能性があります。
  • 新機能の追加はあまり期待できません。

もちろん、現時点でライブラリが全く使えないわけではありませんし、多くの基本的なユースケースでは依然として有効です。しかし、長期的なプロジェクトや、安定性・将来性が重要なシステムで利用する際には、この開発状況を十分に考慮する必要があります。 代替として、より活発に開発されている Playwright や、安定した Scrapy (+ scrapy-playwright など) の利用を検討する方が賢明かもしれません。

まとめ 🎉

requests-htmlは、PythonでWebスクレイピングを行うための強力で使いやすいライブラリです。その主な特徴を再確認しましょう。

メリット 👍

  • Requestsライクな直感的なAPI
  • HTMLパース機能の統合
  • CSSセレクタとXPathによる柔軟な要素検索
  • JavaScriptレンダリング機能の内蔵
  • 非同期処理 (asyncio) のサポート
  • Requestsの豊富な機能を継承

デメリット/注意点 👎

  • JavaScriptレンダリングはリソース消費が大きく、時間がかかる
  • Pyppeteer/Chromiumへの依存と、それに伴うセットアップや互換性の問題
  • ライブラリの開発が近年停滞気味であることによる将来的なリスク
  • 大規模・複雑なクローリングには機能不足な面も

どんなプロジェクトに適しているか?

requests-htmlは、以下のような場合に特に輝きます。

  • 小〜中規模 のWebサイトからのデータ収集
  • JavaScript で動的にコンテンツが生成されるサイトのスクレイピング
  • Requests の使い方に慣れており、手軽にスクレイピングを始めたい
  • 非同期処理 で効率的に複数のページを取得したい(レンダリングなし、または少数)
  • 開発の停滞リスクを許容できる、あるいは短期的な利用

一方で、非常に大規模なクローリング、長期的な安定性が求められるシステム、複雑なブラウザ操作が必要な場合などは、ScrapyやPlaywrightといった他の選択肢を検討すべきでしょう。

Webスクレイピングは強力なツールですが、常にWebサイトの利用規約や `robots.txt` を尊重し、サーバーに負荷をかけないよう配慮して利用することが重要です。

requests-htmlは、その手軽さとJavaScriptレンダリング機能により、多くのスクレイピングタスクを効率化できる可能性を秘めています。このガイドが、あなたのrequests-htmlを使った快適なスクレイピングライフの一助となれば幸いです! 😊

コメント

タイトルとURLをコピーしました