nodriverを使ったスクレイピング完全ガイド: プロキシ統合からスケーリングまで

nodriverを使ったスクレイピングの実装方法を完全解説。WebDriverを使わないCDP直接操作の仕組み、ProxyHatレジデンシャルプロキシとの統合、並行タブでのスケーリングまで、Python開発者向けの実践ガイドです。

Scraping With nodriver: A Practical Guide to Async CDP + Residential Proxies

nodriverを使ったスクレイピングは、undetected-chromedriverの後継として設計された完全非同期のPythonブラウザ自動化ライブラリです。Selenium WebDriverに依存せず、Chrome DevTools Protocol(CDP)を直接WebSocket経由で操作することで、CloudflareやImpervaなどの高度なボット検出システムを回避します。本記事では、nodriverのアーキテクチャからプロキシ統合、スケーリングまでをフレームワークの拡張ポイントを中心に実践的に解説します。nodriver非同期の設計思想を理解し、nodriverアンダテクテッドなステルス能力を活かす方法を学びましょう。

重要な法的注意: 自動化を行う前に、対象サイトの利用規約(ToS)とrobots.txtを確認し、Computer Fraud and Abuse Act(CFAA)およびGDPRの要件を遵守してください。本記事は公開データへのアクセスと承認された自動化のみを対象とします。

nodriverを使ったスクレイピングの基礎

nodriverは、ultrafunkamsterdam氏によって開発されたオープンソースのPythonライブラリで、undetected-chromedriverの設計思想を引き継ぎつつ、完全非同期アーキテクチャでゼロから書き直されています。SeleniumやWebDriverの中間層を完全に排除し、Chrome DevTools Protocolを直接操作することで、ブラウザの自動化における最大の検出ベクトルを根本から解決します。nodriverウェブスクレイピングの成功率は、従来のSeleniumアプローチと比較して大幅に向上しています。

従来のSelenium WebDriverアプローチでは、ChromeDriverバイナリがHTTPサーバーとして動作し、WebDriverプロトコル経由でブラウザにコマンドを送信します。この設計は便利ですが、致命的な欠点があります: navigator.webdriverプロパティがtrueに設定され、cdc_プレフィックスの変数がwindowオブジェクトに露出し、CDP検出シグナルが残ります。高度なボット検出システムはこれらのシグナルを即座に識別できます。

nodriverはChromeDriverを経由せず、Pythonから直接CDP WebSocketエンドポイントに接続します。これにより、WebDriverプロトコルに由来する検出シグナルが最初から存在しません。

特徴 nodriver undetected-chromedriver Selenium WebDriver
アーキテクチャ 直接CDP (WebSocket) ChromeDriver経由 ChromeDriver経由
非同期サポート 完全非同期 (async/await) 部分的 (同期メイン) 部分的 (Selenium 4)
navigator.webdriver 最初から非露出 パッチで隠蔽 露出 (パッチ必要)
CDP検出シグナル 最小化 一部残存 多数残存
Cloudflare Turnstile回避 高い成功率 中程度 低い

nodriverのアーキテクチャ: WebDriverを使わない理由

nodriverの中核となる設計判断は「WebDriverプロトコルを使わない」という一点に集約されます。Chrome DevTools Protocolは、Chromeブラウザに組み込まれたデバッグ・インストルメンテーションインターフェースで、WebSocket接続経由でJSONコマンドを送受信します。nodriverはこのプロトコルを直接使用し、ChromeDriverという中間プロセスを完全にバイパスします。

この設計が重要な理由は3つあります:

  • navigator.webdriverの非露出: WebDriverプロトコルを使用しないため、navigator.webdriverプロパティがtrueに設定されるメカニズム自体が存在しません。Seleniumやundetected-chromedriverはこのフラグを事後的にパッチで隠蔽しますが、パッチ自体が検出されるリスクがあります。
  • CDPリークの最小化: ChromeDriverはRuntime.enablePage.enableなどのCDPドメインを有効化しますが、これらの有効化状態自体が検出シグナルになります。nodriverは必要最小限のドメインのみを有効化し、カスタムフックで制御します。
  • パフォーマンスの向上: ChromeDriverプロセスを経由しないため、コマンドの往復レイテンシが約200ms削減されます。完全非同期設計により、複数タブの並行操作も効率的に処理できます。

CloudflareやImpervaのボット検出エンジンは、ブラウザのJavaScript環境を詳細に検査します。navigator.webdriverwindow.cdc_adoQpoasnfa76pfcZLmcfl_ArrayのようなChromeDriver変数、Runtime.enableの有効状態など、10以上のシグナルを組み合わせて判定します。nodriverはこれらのシグナルを構造的に排除するため、検出回避の成功率が大幅に向上します。

nodriverのAPI: 完全非同期のブラウザ操作

nodriverのAPIは、Seleniumの同期型APIとは根本的に異なります。すべての操作がasync/awaitパターンで記述され、BrowserオブジェクトとTabオブジェクトが中心的な役割を果たします。nodriver非同期の設計により、イベントループ上で複数のタブを効率的に並行操作できます。

基本的な起動とナビゲーション

import nodriver as uc
import asyncio

async def main():
    # ブラウザを起動
    browser = await uc.start(
        headless=False,
        browser_args=['--disable-blink-features=AutomationControlled']
    )

    # 新しいタブを開いてナビゲート
    tab = await browser.get('https://httpbin.org/headers')

    # ページのHTMLを取得
    html = await tab.get_content()
    print(html[:500])

    # ブラウザを閉じる
    browser.stop()

asyncio.run(main())

uc.start()はChromeプロセスを起動し、CDP WebSocket接続を確立してBrowserオブジェクトを返します。browser.get()は新しいTabを作成し、指定URLにナビゲートします。Seleniumのdriver.get()と異なり、これはコルーチンでありawaitが必要です。

要素の選択とイベントフック

nodriverでは、SeleniumのWebDriverWaitexpected_conditionsを使わず、イベントフックとawait tab.select()で要素を待機します。これがnodriver非同期パターンの核心です。

async def scrape_page(browser):
    tab = await browser.get('https://example.com/login')

    # 要素を待機して取得 (自動リトライ付き)
    username_input = await tab.select('input[name="username"]', timeout=10)
    password_input = await tab.select('input[name="password"]', timeout=10)

    # 入力
    await username_input.send_keys('my_username')
    await password_input.send_keys('my_password')

    # 送信ボタンをクリック
    submit_btn = await tab.select('button[type="submit"]')
    await submit_btn.click()

    # ページ遷移を待機
    await tab.wait_for('/dashboard', timeout=15)

    # ダッシュボードのデータを抽出
    data_element = await tab.select('.user-data')
    data = await data_element.get_attribute('data-json')
    return data

tab.select()は、要素がDOMに現れるまで自動的に待機します。SeleniumのWebDriverWait + expected_conditions.presence_of_element_locatedの組み合わせに相当しますが、よりシンプルに記述できます。tab.find()は待機せず即座に検索を試みる点が異なります。

イベントフックによるネットワーク傍受

nodriverの真の強みは、CDPイベントを直接サブスクライブできる点にあります。ページロード、ネットワークリクエスト、ダイアログなどをイベント駆動で処理できます。

async def monitor_network(tab):
    # ネットワークリクエストを監視
    requests = []

    async def on_request(event):
        if 'api/data' in event.request.url:
            requests.append(event.request.url)

    tab.add_handler(uc.cdp.network.RequestWillBeSent, on_request)

    await tab.get('https://example.com')
    await asyncio.sleep(3)  # リクエスト完了を待機

    for url in requests:
        print(f'API call detected: {url}')

このパターンは、XHR/Fetchリクエストを傍受してAPIエンドポイントを特定する際に特に有用です。SeleniumではPerformance Logsを有効化する必要がありましたが、nodriverではネイティブにサポートされます。これはフレームワークのミドルウェア的な拡張ポイントであり、ハックではなく公式のAPIとして設計されています。

プロキシの設定: nodriverプロキシ統合

nodriverには組み込みのプロキシローテーション機能がありません。プロキシはChromeの--proxy-server起動引数を通じて設定します。しかし、ここで重要な問題があります: --proxy-serverフラグは認証をサポートしません。ユーザー名とパスワードによるプロキシ認証は、CDPのFetchドメインの認証インターセプトを通じて処理する必要があります。これがnodriverプロキシ統合の鍵となります。

ProxyHatのレジデンシャルプロキシを使用する場合、CDP経由で認証チャレンジをインターセプトし、認証情報を注入するアプローチが最もクリーンです。これはnodriverのイベントハンドラシステムを適切に活用する、フレームワーク・イディオマティックな実装です。

import nodriver as uc
import asyncio

PROXY_HOST = 'gate.proxyhat.com'
PROXY_PORT = 8080
PROXY_USER = 'user-country-US-session-abc123'
PROXY_PASS = 'your_password'

async def main():
    browser = await uc.start(
        headless=False,
        browser_args=[
            f'--proxy-server=http://{PROXY_HOST}:{PROXY_PORT}',
            '--disable-blink-features=AutomationControlled',
        ]
    )

    tab = await browser.get('about:blank')

    # CDP Fetchドメインを有効化して認証チャレンジをインターセプト
    await tab.send(uc.cdp.fetch.enable(
        handle_auth_requests=True
    ))

    async def on_auth_required(event):
        await tab.send(uc.cdp.fetch.continueWithAuth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response='ProvideCredentials',
                username=PROXY_USER,
                password=PROXY_PASS,
            )
        ))

    tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth_required)

    # プロキシ経由でナビゲート
    await tab.get('https://httpbin.org/ip')
    content = await tab.get_content()
    print(content)

    browser.stop()

asyncio.run(main())

このパターンはnodriverの非同期アーキテクチャに完全に適合します。CDPイベントハンドラとして認証処理を登録することで、プロキシ認証を「ハック」ではなく、フレームワークの拡張ポイントとして適切に統合できます。tab.add_handler()はnodriverの公式なフック機構であり、ミドルウェア的な役割を果たします。

なぜレジデンシャルプロキシが必須なのか: nodriverがブラウザの指紋を隠蔽しても、データセンタープロキシのIPアドレスは即座に識別されます。AWS、GCP、AzureのIP範囲は公開されており、ボット検出システムはこれらを自動的にフラグ付けします。レジデンシャルプロキシは実際のISPから割り当てられたIPを使用するため、本物のユーザーと区別できません。詳細はProxyHatのロケーション一覧を参照してください。技術的な統合方法についてはProxyHat公式ドキュメントもご確認ください。

実践例: ProxyHatレジデンシャルプロキシで保護されたページをスクレイピング

以下は、ProxyHatのレジデンシャルプロキシを経由してCloudflareで保護されたページにアクセスし、JSONデータを抽出する完全な実践例です。ProxyHat SDK的なヘルパークラスでプロキシURLを構築し、nodriverのCDPフックで認証を処理します。

import nodriver as uc
import asyncio
import json
import uuid

class ProxyHatConfig:
    GATEWAY = 'gate.proxyhat.com'
    HTTP_PORT = 8080
    SOCKS5_PORT = 1080
    USERNAME = 'your_username'
    PASSWORD = 'your_password'

    @staticmethod
    def build_username(country='US', city=None, session=None):
        parts = [f'country-{country}']
        if city:
            parts.append(f'city-{city}')
        if session:
            parts.append(f'session-{session}')
        flags = '-'.join(parts)
        return f'{ProxyHatConfig.USERNAME}-{flags}'

    @staticmethod
    def get_proxy_args(country='US', session=None):
        username = ProxyHatConfig.build_username(
            country=country,
            session=session or str(uuid.uuid4())[:8]
        )
        return username, ProxyHatConfig.PASSWORD

async def scrape_protected_page(url, country='US'):
    username, password = ProxyHatConfig.get_proxy_args(
        country=country, session='abc123'
    )

    browser = await uc.start(
        headless=True,
        browser_args=[
            f'--proxy-server=http://{ProxyHatConfig.GATEWAY}:{ProxyHatConfig.HTTP_PORT}',
            '--disable-blink-features=AutomationControlled',
            '--no-sandbox',
            '--disable-dev-shm-usage',
        ]
    )

    tab = await browser.get('about:blank')

    # プロキシ認証ハンドラを設定 (フレームワーク・イディオマティック)
    await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))

    async def on_auth_required(event):
        await tab.send(uc.cdp.fetch.continueWithAuth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response='ProvideCredentials',
                username=username,
                password=password,
            )
        ))

    tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth_required)

    # 対象ページにナビゲート
    await tab.get(url)

    # Cloudflareチャレンジの解決を待機 (最大15秒)
    try:
        await tab.wait_for('body', timeout=15)
        await asyncio.sleep(2)  # 追加のレンダリング待機
    except Exception:
        print('Cloudflare challenge timeout')
        browser.stop()
        return None

    # JSON-LDデータを抽出
    try:
        script_tag = await tab.select(
            'script[type="application/ld+json"]', timeout=10
        )
        json_text = await script_tag.get_attribute('innerHTML')
        data = json.loads(json_text)
        print(f'Extracted data: {json.dumps(data, indent=2)[:200]}')
        return data
    except Exception as e:
        html = await tab.get_content()
        print(f'Fallback: {html[:300]}')
        return None
    finally:
        browser.stop()

# 実行
asyncio.run(scrape_protected_page('https://example.com/data'))

この例では、ProxyHatConfigクラスがプロキシURLの構築をカプセル化し、ジオターゲティング(country-US)とスティッキーセッション(session-abc123)をユーザー名にエンコードします。セッションIDを固定することで、同じIPアドレスを維持しつつ複数リクエストを送信できます。ローテーションが必要な場合は、リクエストごとに新しいセッションIDを生成してください。

ProxyHatの料金と利用可能なロケーションについては、料金ページWebスクレイピングのユースケースを参照してください。

スケーリング: 並行タブとDockerヘッドレスフリート

nodriverの完全非同期設計は、スケーリングにおいて大きな利点となります。単一のブラウザプロセスで複数のタブを並行操作でき、さらに複数のブラウザプロセスをコンテナ化して水平スケールできます。

並行タブによるスケーリング

import nodriver as uc
import asyncio
import uuid

async def scrape_with_proxy(browser, url, proxy_user, proxy_pass):
    """各タブで独立したプロキシセッションを使用"""
    tab = await browser.get('about:blank')

    # プロキシ認証ハンドラ
    await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))

    async def on_auth(event):
        await tab.send(uc.cdp.fetch.continueWithAuth(
            request_id=event.request_id,
            auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                response='ProvideCredentials',
                username=proxy_user,
                password=proxy_pass,
            )
        ))

    tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth)

    await tab.get(url)
    await asyncio.sleep(2)
    content = await tab.get_content()
    await tab.close()
    return content

async def parallel_scrape(urls, max_concurrent=10):
    browser = await uc.start(
        headless=True,
        browser_args=[
            '--proxy-server=http://gate.proxyhat.com:8080',
            '--no-sandbox',
            '--disable-dev-shm-usage',
        ]
    )

    semaphore = asyncio.Semaphore(max_concurrent)

    async def bounded_scrape(url):
        async with semaphore:
            session_id = str(uuid.uuid4())[:8]
            proxy_user = f'your_username-country-US-session-{session_id}'
            proxy_pass = 'your_password'
            return await scrape_with_proxy(
                browser, url, proxy_user, proxy_pass
            )

    results = await asyncio.gather(
        *[bounded_scrape(url) for url in urls],
        return_exceptions=True
    )

    browser.stop()
    return results

# 50のURLを10並行でスクレイピング
urls = [f'https://example.com/page/{i}' for i in range(50)]
results = asyncio.run(parallel_scrape(urls, max_concurrent=10))

asyncio.Semaphoreで並行数を制限し、各タスクに固有のセッションIDを割り当てることで、ProxyHatのバックエンドが各リクエストに異なるIPを割り当てます。50のURLを10並行で処理する場合、全体の処理時間は約5分に短縮されます(1リクエストあたり平均3秒の場合)。

Dockerヘッドレスフリート

本格的なスケーリングには、Dockerコンテナでブラウザフリートを構築します。各コンテナが独立したChromeプロセスとプロキシセッションを管理します。

# Dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    chromium \
    chromium-driver \
    fonts-liberation \
    libappindicator3-1 \
    libasound2 \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libcups2 \
    libdbus-1-3 \
    libgdk-pixbuf2.0-0 \
    libnspr4 \
    libnss3 \
    libx11-xcb1 \
    libxcomposite1 \
    libxdamage1 \
    libxrandr2 \
    xdg-utils \
    && rm -rf /var/lib/apt/lists/*

RUN pip install nodriver

ENV CHROME_PATH=/usr/bin/chromium
ENV HEADLESS=true

COPY scraper.py /app/scraper.py
WORKDIR /app

CMD ["python", "-u", "scraper.py"]

コンテナのレプリカ数は、プロキシの同時接続数制限と調整してください。ProxyHatでは、プランに応じて100〜500の同時セッションをサポートします。各コンテナに環境変数PROXY_SESSION_IDを割り当てることで、コンテナ間でIPローテーションを分散できます。コンテナあたり約500MBのメモリが必要なため、ホストリソースに応じてレプリカ数を決定してください。

グレースフルシャットダウン

nodriverの非同期設計では、browser.stop()が同期呼び出しなため、asyncioのシャットダウン中に適切にクリーンアップする必要があります。

async def graceful_shutdown(browser, tabs):
    """すべてのタブを閉じてからブラウザを終了"""
    for tab in tabs:
        try:
            await tab.close()
        except Exception:
            pass

    # プロセスを確実に終了
    browser.stop()

    # Chromeプロセスの完全な終了を確認
    await asyncio.sleep(1)

# シグナルハンドラと組み合わせて使用
import signal

shutdown_event = asyncio.Event()

def handle_signal(signum, frame):
    shutdown_event.set()

signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)

シグナルハンドラと組み合わせることで、SIGTERM/SIGINT時に進行中のタスクを完了させてから終了するパターンを実装できます。Kubernetes環境では、terminationGracePeriodSecondsを30秒以上に設定してください。

ブラウザを使うべきでないケース

nodriverは強力ですが、すべてのスクレイピングタスクにフルブラウザは不要です。対象ページがJavaScriptレンダリングを必要とせず、ボット検出がCAPTCHAやブラウザフィンガープリントに依存していない場合、HTTPベースのアプローチの方が10〜50倍高速でコストも低くなります。

curl_cffiのようなライブラリは、ChromeのTLSフィンガープリントを再現し、HTTPリクエストレベルでCloudflareの基本的な検出を回避できます。以下の場合はHTTPアプローチを検討してください:

  • 静的HTMLページのスクレイピング(サーバーサイドレンダリング)
  • 公開APIの直接呼び出し(認証ヘッダーのみ必要)
  • 大量のページを高速処理する必要がある場合(1秒あたり100リクエスト以上)
  • サーバーリソースが制限されている場合(ブラウザ1インスタンスあたり約500MBのメモリが必要)

一方、以下の場合はnodriverが適しています:

  • Cloudflare TurnstileやImpervaの高度なチャレンジを解決する必要がある場合
  • JavaScript実行後の動的コンテンツ(SPA、React/Vueアプリ)を取得する場合
  • ブラウザ内でのインタラクション(クリック、スクロール、フォーム送信)が必要な場合
  • XHR/Fetchリクエストを傍受してAPIエンドポイントを特定する場合

倫理と法的注意

スクレイピングは強力なツールですが、責任を持って使用する必要があります。以下のガイドラインを遵守してください:

  • 利用規約の確認: 対象サイトのToSを確認し、自動化アクセスが禁止されていないか確認してください。
  • robots.txtの尊重: robots.txtでDisallowされているパスにはアクセスしないでください。
  • レート制限の遵守: サーバーに過負荷をかけないよう、リクエスト間隔を適切に設定してください。一般的には1リクエストあたり2〜5秒の間隔が推奨されます。
  • 個人情報の取り扱い: GDPR/CCPAの要件に従い、個人を特定できる情報(PII)の収集と保存には特別な注意が必要です。
  • CFAAの遵守: 米国のComputer Fraud and Abuse Actは、不正アクセスを禁止しています。承認されていないシステムへのアクセスや、認証をバイパスする行為は法律違反の可能性があります。

SERPトラッキングや価格モニタリングなど、公開データへのアクセスは多くの国で合法ですが、対象サイトの利用規約と適用法規を確認することは開発者の責任です。詳細はSERPトラッキングのユースケースを参照してください。

Key Takeaways

  • nodriverはWebDriverを使わない: Chrome DevTools Protocolを直接WebSocket経由で操作し、navigator.webdriverフラグやCDP検出シグナルを構造的に排除します。これがnodriverアンダテクテッドなステルス能力の源泉です。
  • 完全非同期API: uc.start()Browser/Tabオブジェクト、await tab.select()、イベントフックがSeleniumの同期パターンを置き換えます。nodriver非同期の設計により、複数タブの並行操作が自然に記述できます。
  • プロキシは手動設定: nodriverに組み込みのプロキシローテーションはありません。--proxy-serverフラグとCDP認証インターセプト(Fetch.AuthRequired)を組み合わせて実装します。これはフレームワークの公式フック機構を使用したイディオマティックな統合です。
  • レジデンシャルプロキシが必須: データセンタープロキシのIPは即座に識別されます。ProxyHatのレジデンシャルプロキシでジオターゲティングとスティッキーセッションを活用してください。
  • スケーリングは並行タブ+Docker: asyncio.Semaphoreで並行数を制御し、Dockerコンテナで水平スケールします。グレースフルシャットダウンを必ず実装してください。
  • フルブラウザは常に必要ではない: 静的ページや公開APIにはcurl_cffiが適しています。nodriverは動的コンテンツと高度なボット検出回避に真価を発揮します。

FAQ

nodriverを使ったスクレイピングとは何ですか?

nodriverを使ったスクレイピングは、Selenium WebDriverに依存せずChrome DevTools Protocol(CDP)を直接WebSocket経由で操作するPythonライブラリ「nodriver」を使用したWebスクレイピング手法です。navigator.webdriverフラグが露出しないため、CloudflareやImpervaなどのボット検出システムを回避しやすく、undetected-chromedriverの後継として設計されています。完全非同期で動作し、高いパフォーマンスを提供します。

なぜnodriverを使ったスクレイピングがプロキシユーザーにとって重要なのですか?

nodriverはブラウザの指紋を最小限に抑えますが、IPアドレスの検出までは防げません。レジデンシャルプロキシと組み合わせることで、ブラウザ指紋の隠蔽とIPの偽装を同時に実現でき、高度なボット検出システムを回避する成功率が大幅に向上します。データセンタープロキシではIPがブロックされやすく、nodriverのステルス機能が活かせません。

nodriverを使ったスクレイピングに最適なプロキシタイプは何ですか?

レジデンシャルプロキシが最適です。実際のISPから割り当てられたIPアドレスを使用するため、ボット検出システムに本物のユーザーとして認識されやすくなります。モバイルプロキシも有効ですがコストが高くなります。データセンタープロキシは検出されやすく、保護されたサイトのスクレイピングには不向きです。ProxyHatでは国・都市レベルのジオターゲティングとスティッキーセッションをサポートしています。

nodriverを使ったスクレイピングでブロックを回避するにはどうすればよいですか?

レジデンシャルプロキシの使用、適切なユーザーエージェントの設定、リクエスト間隔の調整、セッションのローテーション、navigator.webdriverフラグの非表示化を組み合わせてください。nodriverはデフォルトでCDP検出シグナルを最小化しますが、プロキシのローテーションは手動で実装する必要があります。ProxyHatのスティッキーセッション機能を使えば、同じIPを維持したままセッションを管理できます。

始める準備はできましたか?

AIフィルタリングで148か国以上、5,000万以上のレジデンシャルIPにアクセス。

料金を見るレジデンシャルプロキシ
← ブログに戻る