nodriverを使ったWebスクレイピング:プロキシ統合の完全ガイド

nodriverを使ったスクレイピングの完全ガイド。CDP直接通信でWebDriver検知を回避し、ProxyHat住宅プロキシでIPローテーションを実現する開発者向けの実装手順とスケーリング戦略を解説。

Scraping With nodriver: A Practical Guide to Undetected Async Browser Automation
この記事の内容

nodriverプロキシとは:WebDriver不要の次世代スクレイピング

nodriverを使ったスクレイピングは、SeleniumやWebDriver層を完全に排除し、Chrome DevTools Protocol(CDP)をWebSocket経由で直接操作する次世代のブラウザ自動化手法です。undetected-chromedriver(通称uc)の開発者が後継として設計したnodriverは、navigator.webdriverフラグの除去、CDPリークの修正、そして非同期アーキテクチャによる高速化を実現しています。本記事では、nodriverプロキシの統合、ProxyHat住宅プロキシによるIPローテーション、そしてスケーリング戦略までを詳解します。

法的注意事項:本記事の内容は公開データの収集と、明示的な許可を得た自動化のみを対象としています。Computer Fraud and Abuse Act(CFAA)やGDPRの下で、不正アクセスや個人情報の違法収集は処罰対象となります。スクレイピング前に各サイトの利用規約とrobots.txtを確認してください。

nodriverのアーキテクチャ:なぜWebDriverが検知されるのか

従来のSeleniumベースの自動化は、W3C WebDriverプロトコルに依存しています。WebDriverはChromeに--remote-debugging-port--webdriverフラグを追加し、ブラウザのnavigator.webdriverプロパティをtrueに設定します。これがCloudflareやImpervaのボット検知エンジンに即座に検出される根本原因です。

nodriverはこの問題を設計レベルで解決します。具体的には:

  • WebDriverバイナリ不要: chromedriverやgeckodriverを実行しません。Chrome DevTools Protocolを直接WebSocketで呼び出します(CDP公式仕様参照)。
  • navigator.webdriverの除去: WebDriverプロトコルを使用しないため、このプロパティが最初から存在しません。
  • CDPリークの修正: 従来のCDP接続でもRuntime.enableの呼び出しパターンがフィンガープリントされる問題があります。nodriverは最小限のCDPドメインのみ有効化し、検知されるパターンを減らします。
  • 非同期設計: asyncioベースで単一イベントループ内で複数タブを並行操作できます。Seleniumの同期待ち(WebDriverWait)が不要です。

これにより、nodriverはnavigator.webdriverチェック、CDPフィンガープリント、自動化フラグの3層の検知を回避します。ただし、IPベースの検知は別問題であり、ここでプロキシ統合が不可欠になります。

nodriverのAPI:uc.start()からイベントフックまで

nodriverのAPIはSeleniumとは大きく異なります。以下が主要なコンポーネントです:

ブラウザの起動

import nodriver as uc
import asyncio

async def main():
    browser = await uc.start(
        headless=False,
        browser_args=["--disable-blink-features=AutomationControlled"]
    )
    page = await browser.get("https://example.com")
    await asyncio.sleep(3)
    browser.stop()

asyncio.run(main())

uc.start()Browserオブジェクトを返します。これはSeleniumのWebDriverに相当しますが、非同期メソッドのみを持ちます。

要素の検索と操作

tab = await browser.get("https://example.com/search")

# find()は最初の一致要素を返す
search_box = await tab.select("input[name='q']")
await search_box.send_keys("nodriver proxy")

# 複数要素の取得
results = await tab.find_all("div.result", timeout=10)
for r in results:
    title = await r.evaluate("el => el.textContent")
    print(title)

SeleniumのWebDriverWaitexpected_conditionsは存在しません。nodriverは要素が現れるまで自動的に待機し、timeoutパラメータで制御します。

イベントフックによる待機

ページ遷移や動的コンテンツの待機には、イベントフックを使用します:

tab = await browser.get("https://example.com")

# ページロード完了を待機
await tab.wait_for("div.content-loaded")

# ネットワークリクエストを傍受
tab.add_handler(
    uc.cdp.network.ResponseReceived,
    lambda event: print(f"Response: {event.response.url} ({event.response.status})")
)

# JavaScript実行
data = await tab.evaluate("() => window.__INITIAL_STATE__")
print(data)

このイベント駆動モデルは、W3C WebDriver仕様のポーリングベースアプローチより効率的で、リソース消費も少なくなります。nodriver asyncの特性を活かすことで、待機時間を大幅に削減できます。

nodriverプロキシの設定:ProxyHat住宅プロキシの統合

nodriverには組み込みのプロキシローテーション機能がありません。プロキシはChromeの--proxy-serverフラグで指定しますが、ここが重要な設計判断点になります。

なぜ住宅プロキシが必須か

nodriverがブラウザレベルの検知を回避しても、IPレピュテーションベースの検知は残ります。データセンターIPはASN(Autonomous System Number)で即座に識別され、CloudflareはデータセンターIPに対して追加のチャレンジ(CAPTCHA、JS実行)を要求します。

住宅プロキシはISPから割り当てられた実際の住宅IPアドレスを使用するため、通常のユーザートラフィックと区別できません。これがnodriver undetectedのステルス機能を最大限に活用するための必須条件です。

プロキシURLの構築

ProxyHatの住宅プロキシエンドポイントは以下の形式で指定します:

import urllib.parse

def build_proxy_url(country="US", session=None):
    username = f"user-country-{country}"
    if session:
        username += f"-session-{session}"
    password = "YOUR_PASSWORD"
    
    encoded_auth = urllib.parse.quote(f"{username}:{password}")
    return f"http://{encoded_auth}@gate.proxyhat.com:8080"

# 例: 米国IP + スティッキーセッション
proxy_url = build_proxy_url(country="US", session="abc123")
# → http://user-country-US-session-abc123%3AYOUR_PASSWORD@gate.proxyhat.com:8080

--proxy-serverフラグは認証ヘッダーをサポートしません。そのため、Chrome拡張機能またはCDPのFetch.requestPausedイベントで認証を処理する必要があります。nodriverでは、CDPのFetch.enableを使用してプロキシ認証を注入します。

認証付きプロキシの完全設定

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():
    proxy_server = f"http={PROXY_HOST}:{PROXY_PORT}"
    
    browser = await uc.start(
        headless=True,
        browser_args=[
            f"--proxy-server={proxy_server}",
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
            "--disable-gpu",
        ]
    )
    
    tab = await browser.get("https://example.com")
    
    # CDPでプロキシ認証を設定
    await tab.send(uc.cdp.network.enable())
    await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
    
    async def auth_handler(event):
        await tab.send(
            uc.cdp.fetch.continue_with_auth(
                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, auth_handler)
    
    # ページの操作
    await tab.wait_for("body")
    content = await tab.get_content()
    print(content[:500])
    
    browser.stop()

asyncio.run(main())

このパターンはnodriverプロキシ統合の標準的なアプローチです。Fetch.enablehandle_auth_requests=Trueを設定し、AuthRequiredイベントで認証情報を注入します。これにより、--proxy-serverフラグと認証が分離され、Chromeがプロキシに接続するたびにCDP経由で認証が行われます。

実行可能な例:保護されたページのスクレイピング

以下は、ProxyHatの住宅プロキシエンドポイント経由でCloudflare保護されたページにアクセスし、JSONデータを抽出する完全な例です:

import nodriver as uc
import asyncio
import json

class ProxyHatScraper:
    def __init__(self, country="US", session_id=None):
        self.country = country
        self.session_id = session_id or f"sess-{asyncio.get_event_loop().time()}"
        self.proxy_user = f"user-country-{country}-session-{self.session_id}"
        self.proxy_pass = "YOUR_PASSWORD"
    
    def get_proxy_args(self):
        return [
            "--proxy-server=http://gate.proxyhat.com:8080",
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
        ]
    
    async def setup_auth(self, tab):
        await tab.send(uc.cdp.network.enable())
        await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
        
        async def on_auth(event):
            await tab.send(
                uc.cdp.fetch.continue_with_auth(
                    request_id=event.request_id,
                    auth_challenge_response=uc.cdp.fetch.AuthChallengeResponse(
                        response="ProvideCredentials",
                        username=self.proxy_user,
                        password=self.proxy_pass,
                    )
                )
            )
        
        tab.add_handler(uc.cdp.fetch.AuthRequired, on_auth)
    
    async def scrape(self, url):
        browser = await uc.start(
            headless=True,
            browser_args=self.get_proxy_args()
        )
        try:
            tab = await browser.get(url)
            await self.setup_auth(tab)
            
            # Cloudflareチャレンジの通過を待機(最大15秒)
            await tab.wait_for("body", timeout=15)
            await asyncio.sleep(2)
            
            # ページ内のJSONデータを抽出
            raw_data = await tab.evaluate(
                "() => {"
                "  const s = document.querySelector('script[type=\"application/json\"]');"
                "  return s ? s.textContent : null;"
                "}"
            )
            
            if raw_data:
                return json.loads(raw_data)
            return await tab.get_content()
        finally:
            browser.stop()

async def main():
    scraper = ProxyHatScraper(country="US", session_id="abc123")
    result = await scraper.scrape("https://protected-site.example.com/api/data")
    print(json.dumps(result, indent=2, ensure_ascii=False))

asyncio.run(main())

この例の重要ポイント:

  • スティッキーセッション: session-abc123で同じIPを維持し、ログイン後のセッション一貫性を確保します。
  • 国指定: country-USで米国IPを指定し、地域限定コンテンツにアクセスします。ProxyHatは多数の国と都市をサポートしています。
  • Cloudflare待機: wait_for("body")と追加のsleep(2)で、JSチャレンジの解決を待ちます。
  • graceful shutdown: finallyブロックでbrowser.stop()を呼び、プロセスが残存しないようにします。

スケーリング:並行タブ、Dockerフリート、コンテキスト別プロキシ

並行タブの管理

nodriverは単一イベントループで複数タブを並行操作できます。ただし、各タブに異なるプロキシを割り当てるには、ブラウザインスタンスを分離する必要があります(--proxy-serverはブラウザレベルの設定のため):

import nodriver as uc
import asyncio

async def scrape_with_proxy(country, session_id, url):
    proxy_user = f"user-country-{country}-session-{session_id}"
    proxy_pass = "YOUR_PASSWORD"
    
    browser = await uc.start(
        headless=True,
        browser_args=[
            "--proxy-server=http://gate.proxyhat.com:8080",
            "--disable-blink-features=AutomationControlled",
            "--no-sandbox",
            "--disable-gpu",
        ]
    )
    try:
        tab = await browser.get(url)
        await tab.send(uc.cdp.network.enable())
        await tab.send(uc.cdp.fetch.enable(handle_auth_requests=True))
        
        async def on_auth(event):
            await tab.send(
                uc.cdp.fetch.continue_with_auth(
                    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.wait_for("body", timeout=15)
        
        title = await tab.evaluate("() => document.title")
        return {"country": country, "session": session_id, "title": title}
    finally:
        browser.stop()

async def main():
    targets = [
        ("US", "s1", "https://example.com"),
        ("DE", "s2", "https://example.com"),
        ("JP", "s3", "https://example.com"),
    ]
    
    # 最大3並行
    semaphore = asyncio.Semaphore(3)
    
    async def bounded_scrape(args):
        async with semaphore:
            return await scrape_with_proxy(*args)
    
    results = await asyncio.gather(*[bounded_scrape(t) for t in targets])
    for r in results:
        print(r)

asyncio.run(main())

Dockerでのヘッドレスフリート

本格的なスケーリングにはDockerコンテナでブラウザフリートを構築します:

# Dockerfile
FROM python:3.12-slim

RUN apt-get update && apt-get install -y \
    chromium \
    fonts-liberation \
    libnss3 \
    libxss1 \
    libasound2 \
    libatk-bridge2.0-0 \
    libgtk-3-0 \
    && rm -rf /var/lib/apt/lists/*

RUN pip install nodriver

ENV CHROME_PATH=/usr/bin/chromium

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

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

コンテナごとに異なるプロキシセッションを割り当てることで、IP分散が実現します。KubernetesやDocker Composeでスケールさせ、各コンテナに環境変数PROXY_SESSIONを渡してプロキシセッションを切り替えます。各ブラウザインスタンスは150-300 MBのメモリを消費するため、コンテナのリソース制限を適切に設定してください。

graceful shutdown

シグナルハンドラで全ブラウザを確実に終了させます:

import signal
import sys

browsers = []

def shutdown(signum, frame):
    for b in browsers:
        try:
            b.stop()
        except:
            pass
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

コンテナ環境ではSIGTERMを受信後にgracefulに終了することで、プロキシセッションのリソースリークを防ぎます。

いつブラウザを使わないべきか:HTTP + curl_cffiの比較

nodriverは強力ですが、常に最適な選択とは限りません。単純なAPI呼び出しや、CloudflareのJSチャレンジが不要なページでは、HTTPクライアントの方が10倍以上高速でリソース消費も少ないです。

観点 nodriver(ブラウザ) curl_cffi(HTTP)
メモリ使用量 150-300 MB / インスタンス 10-20 MB / リクエスト
1リクエストのレイテンシ 2-5 秒(ページロード含む) 200-500 ms
Cloudflare JS Challenge 通過可能 通過不可(TLSフィンガープリントのみ)
並行処理 3-10 インスタンス 100-500 並行リクエスト
プロキシ認証 CDP経由で注入 URL内認証で直接指定
適用場面 JS重いページ、SPA、保護サイト API、静的ページ、JSONエンドポイント

判断基準はシンプルです。ページがJavaScriptを実行してコンテンツをレンダリングする、またはCloudflare/Impervaのアクティブチャレンジがある場合はnodriverを使用します。それ以外はcurl_cffiとProxyHatプロキシの組み合わせで十分です。詳細なユースケースはWebスクレイピングのユースケースを参照してください。

Key Takeaways

  • nodriverはWebDriver不要: CDP直接通信でnavigator.webdriver検知を根本的に回避します。Seleniumからの移行で最大のメリットはステルス性の向上です。
  • プロキシ認証はCDP経由: --proxy-serverフラグ+Fetch.AuthRequiredイベントハンドラがnodriverプロキシ統合の標準パターンです。
  • 住宅プロキシが必須: データセンターIPではnodriverのステルス性が活きません。ProxyHatの住宅プロキシでIPレピュテーションを確保してください。料金プランは月額$10から。
  • スティッキーセッションで一貫性: session-xxxフラグで同一IPを維持し、ログインセッションや複数ページ遷移の整合性を保ちます。
  • スケーリングはコンテナ単位: ブラウザは1インスタンス150-300 MB消費するため、Docker + asyncio.Semaphoreで並行度を制御します。
  • 常にブラウザが最適ではない: JS不要なページではcurl_cffi + プロキシで10倍のスループットを得られます。

nodriverプロキシの設定やProxyHatの統合について詳しくは、ProxyHatドキュメントを参照してください。SERPトラッキングや価格モニタリングの具体的な実装は、SERPトラッキングのユースケースもご覧ください。

FAQ

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

nodriverを使ったスクレイピングは、Selenium/WebDriverを使わずにChrome DevTools Protocol(CDP)を直接WebSocketで操作し、Webページからデータを抽出する手法です。従来のundetected-chromedriverの後継として設計され、navigator.webdriverフラグの不在、CDPリークの修正、asyncioベースの非同期操作により、CloudflareやImpervaなどのボット検知システムを回避しながら効率的にスクレイピングできます。

nodriverプロキシはなぜ重要なのですか?

nodriverはブラウザレベルの検知は回避できますが、IPベースの検知は回避できません。データセンターIPはASNで即座に識別され、Cloudflareは追加のJSチャレンジを要求します。住宅プロキシを組み合わせることで、nodriverのステルス性が最大限に活かされ、通常のユーザートラフィックとして認識されます。プロキシなしのnodriverは、IPベースのブロックに対して無力です。

nodriverプロキシでどのプロキシタイプが最適ですか?

CloudflareやImpervaで保護されたサイトには住宅プロキシが最適です。データセンターIPはASNベースで即座に識別されます。モバイルプロキシはさらに信頼性が高いですがコストが高くなります。ProxyHatの住宅プロキシはgate.proxyhat.com:8080でアクセス可能で、国・都市レベルのジオターゲティングとスティッキーセッションをサポートします。SERPスクレイピングには住宅プロキシを推奨します。

nodriverプロキシでブロックを回避するにはどうすればよいですか?

ブロック回避には複層の対策が必要です:(1) 住宅プロキシでIPレピュテーションを確保、(2) --disable-blink-features=AutomationControlledで自動化フラグを無効化、(3) スティッキーセッションで同一IPを維持し異常なIP切り替えを防止、(4) リクエスト間隔を1-3秒空けて人間らしいアクセスパターンを模倣、(5) 適切なUser-Agentとタイムゾーンを設定。詳細な実装は本記事のコード例を参照してください。

nodriverはSeleniumより高速ですか?

はい。nodriverはasyncioベースの非同期設計で、ポーリング不要のイベント駆動モデルを採用しています。SeleniumのWebDriverWait(最大10秒のポーリング)に対し、nodriverは要素の出現をイベントで検知するため、待機時間が短縮されます。ただし、ブラウザ自体のメモリ消費(150-300 MB/インスタンス)はSeleniumと同等のため、スケーリング時のリソース計画は同様に必要です。

よくある質問

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

nodriverを使ったスクレイピングは、Selenium/WebDriverを使わずにChrome DevTools Protocol(CDP)を直接WebSocketで操作し、Webページからデータを抽出する手法です。従来のundetected-chromedriverの後継として設計され、navigator.webdriverフラグの不在、CDPリークの修正、asyncioベースの非同期操作により、CloudflareやImpervaなどのボット検知システムを回避しながら効率的にスクレイピングできます。

nodriverプロキシはなぜ重要なのですか?

nodriverはブラウザレベルの検知は回避できますが、IPベースの検知は回避できません。データセンターIPはASNで即座に識別され、Cloudflareは追加のJSチャレンジを要求します。住宅プロキシを組み合わせることで、nodriverのステルス性が最大限に活かされ、通常のユーザートラフィックとして認識されます。プロキシなしのnodriverは、IPベースのブロックに対して無力です。

nodriverプロキシでどのプロキシタイプが最適ですか?

CloudflareやImpervaで保護されたサイトには住宅プロキシが最適です。データセンターIPはASNベースで即座に識別されます。モバイルプロキシはさらに信頼性が高いですがコストが高くなります。ProxyHatの住宅プロキシはgate.proxyhat.com:8080でアクセス可能で、国・都市レベルのジオターゲティングとスティッキーセッションをサポートします。SERPスクレイピングには住宅プロキシを推奨します。

nodriverプロキシでブロックを回避するにはどうすればよいですか?

ブロック回避には複層の対策が必要です:(1) 住宅プロキシでIPレピュテーションを確保、(2) --disable-blink-features=AutomationControlledで自動化フラグを無効化、(3) スティッキーセッションで同一IPを維持し異常なIP切り替えを防止、(4) リクエスト間隔を1-3秒空けて人間らしいアクセスパターンを模倣、(5) 適切なUser-Agentとタイムゾーンを設定。

nodriverはSeleniumより高速ですか?

はい。nodriverはasyncioベースの非同期設計で、ポーリング不要のイベント駆動モデルを採用しています。SeleniumのWebDriverWait(最大10秒のポーリング)に対し、nodriverは要素の出現をイベントで検知するため、待機時間が短縮されます。ただし、ブラウザ自体のメモリ消費(150-300 MB/インスタンス)はSeleniumと同等のため、スケーリング時のリソース計画は同様に必要です。

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

148か国以上の住宅用・ISP・モバイルプロキシ。無料アカウントを作成。

無料アカウントを作成
← ブログに戻る