Playwrightでパーシングスクリプトやマルチアカウントを実行し、各プロキシごとにブラウザの別インスタンスを立ち上げている場合、無駄にメモリとCPUを消費しています。PlaywrightはプロキシをBrowserContextレベルで割り当てることができ、ブラウザ全体ではなく、スクリプトのアーキテクチャを根本的に変えます: 一つのブラウザプロセスで、異なるIP、クッキー、フィンガープリンツを持つ数十の隔離されたコンテキストを持つことができます。この記事では、これがどのように機能するか、PythonとNode.jsのコード、そして居住者およびモバイルプロキシを使用する際に特に重要な理由について説明します。
問題: browser.launch()でのプロキシ — 高コストで不便
多くのPlaywrightスクリプトの古典的なアプローチは次のようになります: 各プロキシごとにchromium.launch({ proxy: {...} })が呼び出され、別々のブラウザプロセスが作成され、動作し、次に新しいプロキシで次のプロセスが起動されます。WildberriesやOzonのために50のIPアドレスを同時に処理する必要がある場合、これは50の完全なChromiumプロセスを同時に意味します — 各プロセスは開始時に150MBから300MBのRAMを消費し、タブやレンダリングを考慮していません。
実際には、これは通常の8GBメモリのVPSで15-20の並行ブラウザで天井に達します。サーバーはスワップし始め、スクリプトはタイムアウトで落ち、いくつかのプロキシは処理を終えることができません。問題はプロキシやPlaywrightにあるのではなく、アーキテクチャにあります: 各ブラウザの起動は、全レンダリングエンジン、GPUプロセス、ネットワーク、そして数十の補助スレッドを引きずりますが、実際には新しいIPとクリーンなセッションだけが必要です。
Playwrightはこの問題を優雅に解決します: プロキシはブラウザの起動時ではなく、BrowserContextの作成時に指定できます — 同じブラウザプロセス内の軽量な隔離された「サンドボックス」です。一つのChromiumプロセスは、各プロキシ、各自のクッキー、localStorageを持つ数十のコンテキストを保持でき、互いに干渉しません。
ブラウザ vs BrowserContext: 違いは何か
Browserはエンジンのプロセス(Chromium、Firefox、またはWebKit)そのもので、一度起動され、長く生き続けます。BrowserContextはそのプロセス内の隔離された環境で、プライベートウィンドウ(インコグニート)のようなもので、各自のクッキー、キャッシュ、権限、そして私たちにとって重要なネットワーク設定(プロキシを含む)を持っています。
Dolphin AntyやAdsPowerのようなアンチデテクトブラウザと比較すると、そこでも各「プロファイル」は本質的にクッキーとフィンガープリンツを持つ別々の隔離されたコンテナです。Playwrightはプログラム的に似たロジックを構築することを可能にします: 一つのブラウザ — 多くのコンテキスト、各々が独自のIPを持つ「プロファイル」として。
| パラメータ | ブラウザへのプロキシ | BrowserContextへのプロキシ |
|---|---|---|
| 1つのIPあたりのメモリ | 150-300MB(新しいプロセス) | 15-40MB(新しいコンテキスト) |
| IP変更の速度 | 2-5秒(ブラウザの再起動) | 0.1-0.3秒(新しいコンテキスト) |
| クッキーの隔離 | 完全(異なるプロセス) | 完全(コンテキストはクッキーを共有しない) |
| 8GB RAMでの最大並行セッション数 | 約20-25 | 約150-200 |
コンテキストへのプロキシ: Pythonのコード
以下は基本的なパターンです: 一つのブラウザが一度起動され、その後各プロキシごとに別々のコンテキストが作成されます。これはPlaywrightの同期APIですが、ロジックはasyncバージョンでも同じです。
from playwright.sync_api import sync_playwright
proxies = [
{"server": "http://proxy1.example.com:8000", "username": "user1", "password": "pass1"},
{"server": "http://proxy2.example.com:8000", "username": "user2", "password": "pass2"},
{"server": "http://proxy3.example.com:8000", "username": "user3", "password": "pass3"},
]
with sync_playwright() as p:
# ブラウザを一度だけ起動し、プロキシなしで
browser = p.chromium.launch(headless=True)
for proxy in proxies:
# プロキシはブラウザではなくコンテキストに割り当てられます
context = browser.new_context(
proxy={
"server": proxy["server"],
"username": proxy["username"],
"password": proxy["password"],
},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
)
page = context.new_page()
page.goto("https://httpbin.org/ip", timeout=30000)
print(page.content())
page.close()
context.close() # コンテキストのみを閉じ、ブラウザは生き続けます
browser.close()
重要な点は、browser.launch()がループの外で一度だけ呼び出され、browser.new_context()がプロキシのループ内で呼び出されることです。ログインとパスワードによる認証が必要なプロキシの場合、PlaywrightはネットワークレベルでBasic Authヘッダーを自動的に挿入し、追加の設定は必要ありません。
コンテキストへのプロキシ: Node.jsのコード
同じロジックをJavaScriptでasync/awaitを使用して実装します — これはNode.jsのパーシングフレームワークと組み合わせてよく使用されます:
const { chromium } = require('playwright');
const proxies = [
{ server: 'http://proxy1.example.com:8000', username: 'user1', password: 'pass1' },
{ server: 'http://proxy2.example.com:8000', username: 'user2', password: 'pass2' },
];
(async () => {
const browser = await chromium.launch({ headless: true });
for (const proxy of proxies) {
const context = await browser.newContext({
proxy: {
server: proxy.server,
username: proxy.username,
password: proxy.password,
},
});
const page = await context.newPage();
await page.goto('https://httpbin.org/ip', { timeout: 30000 });
console.log(await page.content());
await page.close();
await context.close();
}
await browser.close();
})();
複数のプロキシを並行して処理するために、Promise.allを使用してコンテキストを作成することができ、前のものが閉じるのを待つ必要はありません — これが「一つのブラウザ、多くのコンテキスト」アプローチの主な利点です。
ブラウザを再起動せずにクリーンなプロキシローテーション
「クリーンなローテーション」とは、IPを変更する際に前のセッションの残骸(クッキー、キャッシュ、サービスワーカー)が残らないことを意味します。ブラウザ全体を再起動すると自動的に保証されますが、リソースを多く消費します。コンテキストを使用する場合、古いコンテキストを閉じて新しいものを作成することで同じことが達成されます — この操作は数分の一秒で行えます。
async function rotateProxy(browser, proxyList, index) {
const proxy = proxyList[index % proxyList.length];
const context = await browser.newContext({ proxy });
const page = await context.newPage();
return { context, page };
}
// こう使います: N回目のイテレーションごとにIPを変更
for (let i = 0; i < 100; i++) {
if (i % 10 === 0) {
const { context, page } = await rotateProxy(browser, proxies, i);
// 新しいセッションで10リクエストを処理します
}
}
このパターンは、マーケットプレイスのパーシングに特に役立ちます: たとえば、WildberriesやOzonから価格を収集する際、ボット対策が一つのIPからのリクエスト数を時間単位で追跡します。コンテキストレベルでのローテーションにより、IPを5-10リクエストごとに変更でき、ブラウザの再起動によるパフォーマンスの低下を防ぎます。
このようなシナリオには、居住者プロキシが適しています — これらは実際のユーザーのIPを提供し、マーケットプレイスが疑わしいと見なすことが少ないため、明らかにサーバーの範囲よりも優れています。
メモリと速度の比較
8GB RAMと4 vCPUのテストVPSで、異なるIPで100ページをクロールするタスクに対して2つのアプローチをテストしました。最初のケースでは、各IPごとに別々のchromium.launch()が起動され、2番目のケースでは、1つのブラウザで100コンテキストが順次作成されました。
| メトリック | 100回のbrowser.launch() | 1ブラウザでの100コンテキスト |
|---|---|---|
| ピークメモリ | 約6.8GB | 約1.9GB |
| 総実行時間 | 182秒 | 97秒 |
| タイムアウトで落ちたセッション数 | 7 | 0 |
このテストでは、メモリの節約は約70%で、全体の実行時間はChromiumプロセスの起動と終了のオーバーヘッドがないため、ほぼ半分に短縮されました。大量のIP(1日あたり数千)を扱う場合、この差はさらに顕著になり、サーバーのハードウェアをアップグレードせずに使用できるプロキシの数に直接影響します。
実用的なケース: パーシングとマルチアカウント
マーケットプレイスのパーシング。 Wildberries、Ozon、またはAvitoから価格を収集する際、同じブラウザが数十のコンテキストを開いたままにし、それぞれがユニークなIPと独自のクッキー保存を持ちます。これにより、リクエストのパターンによるブロックのリスクが低下し、IP間のプロセスの再起動を待つことなくデータを迅速に収集できます。
マルチアカウントとアカウントファーミング。 Facebook AdsやTikTok Adsの広告キャンペーンをPlaywrightスクリプトでテストするために、各アカウントは独自のプロキシとユーザーエージェントを持つ別々のコンテキストに保持するのが理にかなっています — これは、MultiloginやGoLoginのようなアンチデテクトブラウザの動作を模倣しますが、アンチデテクトのライセンスなしで、完全にプログラム的に行います。
広告の地理的テスト。 様々な国でクリエイティブがどのように表示されるかを確認する必要があるマーケティング担当者にとって、異なるプロキシを持つコンテキストは、ブラウザの1つのセッションで必要な地域を順次「訪れる」ことを可能にし、手動で数十の別ウィンドウを開く必要がありません。
モバイルトラフィックを模倣することが重要なシナリオ(たとえば、モバイル版のウェブサイトやTikTok Adsの広告の確認)では、モバイルプロキシを検討する価値があります。これらは実際の携帯電話会社からのIPを提供し、アンチフロードシステムに疑わしいと見なされることが少なくなります。
もしタスクが高い匿名性を必要としない迅速で安定したパーシング(たとえば、厳しいアンチボット保護のないウェブサイトからの公開データの収集)であるなら、データセンターのプロキシは、大量のリクエストに対して速度とコストの最適なバランスを提供します。
よくある間違いとその回避方法
1. プロキシをlaunch()で設定し、new_context()ではない。 これは初心者の最も一般的な間違いです — ブラウザ起動時に指定されたプロキシは、すべてのコンテキストに一度に適用され、個別に上書きすることはできません。IPの柔軟性が必要な場合は、常にnew_context()にプロキシを渡してください。
2. 古いコンテキストが閉じられない。 数百のコンテキストを作成し、context.close()を呼び出さない場合、ブラウザのメモリは線形に増加し、プロセスがOOMで落ちるまで続きます。各IPのタスクが完了した後は、常にコンテキストを閉じてください。
3. すべてのコンテキストに同じユーザーエージェント。 プロキシが変更されても、ブラウザのフィンガープリンツが同じまま(ユーザーエージェント、言語、タイムゾーン)であると、マーケットプレイスやソーシャルメディアのアンチボットシステムはセッションを結びつけることができます。各コンテキストのプロキシとともにユーザーエージェントとビューポートを変更してください。
4. プロキシのタイムアウトを無視する。 居住者およびモバイルプロキシは、実際のネットワークインフラストラクチャのためにデータセンターのプロキシよりも応答が遅くなることがあります。page.goto()を呼び出す際には、合理的なタイムアウト(20-40秒)を設定してください。さもなければ、スクリプトは動作中のプロキシを「死んでいる」と見なします。
5. 同時に1つのブラウザに過剰なコンテキスト。 メモリの節約にもかかわらず、500以上のアクティブなコンテキストを無制限に作成すると、プロセスの制限に達します。合理的なバランスは、サーバーのRAMの量に応じて、1つのブラウザプロセスあたり30-80の同時アクティブコンテキストです。
結論
プロキシをBrowserContextレベルで割り当てることは、アーキテクチャの解決策であり、最大70%のメモリを節約し、IPローテーションを数十倍速め、セッション間でクッキーのクリーンな隔離を提供します。マーケットプレイスのパーシング、異なる地域での広告テスト、マルチアカウントにとって、これは同じサーバーでの並行セッション数を増やす直接的な方法です。
Wildberries、Ozonからデータを収集するためのPlaywrightスクリプトを構築する場合や、Facebook AdsやTikTok Adsの広告管理に取り組む場合、プロキシプールの質はコードのアーキテクチャと同様に重要です。IPの信頼性とアンチボットシステムへの耐性が重要なタスクの場合、居住者プロキシから始める価値があります — これらは上記の「一つのブラウザ、多くのコンテキスト」アプローチとよく組み合わさります。