Webスクレイピングの標準的なスタックは長らく「Node.js + Puppeteer + headless Chrome」だった。手軽で実績もあるが、起動に約2秒かかり、メモリを200MB以上消費し、Chromeバイナリ(300MB超)の配布が前提というのは、大規模な自動化や軽量なAIエージェントには少々重い。
2026年4月にv0.1.0をリリースしたObscura(★2,400)はこの前提を崩す。Rustで書かれたヘッドレスブラウザエンジンで、V8エンジンを組み込んでJavaScriptを完全実行しながら、Chrome DevTools Protocol(CDP)を独自実装することでPuppeteer・Playwrightとの互換性を維持する。結果として起動時間は即時、ページ読み込みは85ms(Chromeの約6分の1)、メモリ使用量は30MB(Chromeの約15%)を達成した。
本記事では、ObscuraのCDPアーキテクチャ、ステルスモードの実装、CLIの基本操作、Puppeteer/Playwrightとの接続、そしてAIエージェント自動化への応用までを公式READMEをもとに解説する。
Obscuraとは:RustとV8で実装したPuppeteer互換ヘッドレスブラウザ
ObscuraはRust製のヘッドレスブラウザエンジンで、GitHubリポジトリ h4ckf0r0day/obscura でApache 2.0ライセンスの下で公開されている。コードベースはRust 100%で構成され、JavaScript実行エンジンとしてV8を組み込んでいる。
ヘッドレスブラウザとはGUI(画面描画)なしで動作するブラウザエンジン全般を指す。「headless Chrome」はChromeブラウザをGUIなしモードで起動したものだが、Chromeのバイナリが必要で起動コストが高い。ObscuraはChromeバイナリを一切使わず、RustとV8で独立したブラウザエンジンを実装している。
なぜRustとV8を選んだのか
Rustを採用した理由はパフォーマンスとメモリ安全性にある。C++で書かれた多くのブラウザエンジンとは異なり、Rustはゼロコスト抽象化とメモリ安全性をコンパイル時に保証する。並列スクレイピング時のデータ競合や解放済みメモリへのアクセスを言語レベルで防げる。
V8(Google製のJavaScriptエンジン)を組み込むことで、モダンなJavaScriptを完全実行できる。これにより、React・Vue・Next.jsなどのSPA(シングルページアプリケーション)が動的に生成するDOMコンテンツも正確に取得できる。静的HTMLパーサーとは根本的に異なる。
Obsucraの3つの動作モード
Obscuraは用途に応じて3つのモードで動作する。
| モード | コマンド | 用途 |
|---|---|---|
| CLIフェッチ | obscura fetch <URL> |
単一ページの取得・JS評価 |
| 並列スクレイピング | obscura scrape <URL...> |
複数URLの並列処理 |
| CDPサーバー | obscura serve |
Puppeteer/Playwright連携 |
CLIモードはシェルスクリプトやPythonパイプラインからの呼び出しに適している。CDPサーバーモードは既存のPuppeteer/Playwrightコードをほぼ無改修で動かせる。用途に応じて使い分けることで、スクレイピングパイプラインを段階的に移行できる。
Chrome DevTools Protocol(CDP)の実装アーキテクチャ
ObscuraがPuppeteer・Playwrightと互換性を持つ理由は、Chrome DevTools Protocol(CDP)を独自実装しているからだ。CDPはGoogleが策定したブラウザ制御プロトコルで、WebSocketを通じてブラウザの内部機能を操作する。
実装済みのCDPドメイン
Obscura v0.1.0が実装するCDPドメインは以下の通りだ:
Target ドメイン:createTarget・closeTarget・attachToTarget・createBrowserContext・disposeBrowserContext を実装。複数タブ・複数ブラウザコンテキストの管理ができる。
Page ドメイン:navigate・getFrameTree・addScriptToEvaluateOnNewDocument・lifecycleEvents を実装。ページナビゲーションの制御と、ページ読み込みのライフサイクルイベント(load・domcontentloaded・networkidle0)を検出できる。
Runtime ドメイン:evaluate・callFunctionOn・getProperties・addBinding を実装。ページコンテキストでJavaScriptを実行し、結果を取得する中核機能だ。
DOM ドメイン:getDocument・querySelector・querySelectorAll・getOuterHTML・resolveNode を実装。DOM要素へのアクセスとHTMLの取得を可能にする。
Network ドメイン:enable・setCookies・getCookies・setExtraHTTPHeaders・setUserAgentOverride を実装。HTTPヘッダーのカスタマイズやCookieの管理ができる。
Fetch ドメイン:enable・continueRequest・fulfillRequest・failRequest を実装。リクエストのライブインターセプトが可能で、レスポンスのモック・書き換えができる。
Storage ドメイン:Cookie・localStorage等のストレージへのアクセスを実装。ログイン状態の維持・セッション管理に使う。
Input ドメイン:dispatchMouseEvent・dispatchKeyEvent を実装。マウスクリックやキーボード入力をシミュレートする。
LP ドメイン:getMarkdown を実装。DOMをMarkdownに変換するカスタムメソッドで、LLMへのコンテキスト供給に直接活用できる。
標準CDPには存在しないLP(Language Processing)ドメインの
getMarkdown メソッドは、Obscura独自の拡張だ。ページのDOM構造をMarkdown形式で返すため、取得したコンテンツをClaude・GPT-4等のLLMに渡す際にトークン数を削減できる。AIエージェントがWebページを「読む」用途に最適化された設計だ。
インストールとコマンドライン基本操作
バイナリのインストール
Obscuraはプリコンパイル済みバイナリをリリースページで配布している。Rustのインストールなしに動作する。
# Linux x86_64
curl -LO https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-x86_64-linux.tar.gz
tar xzf obscura-x86_64-linux.tar.gz
./obscura fetch https://example.com --eval "document.title"
# macOS Apple Silicon
curl -LO https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-aarch64-macos.tar.gz
tar xzf obscura-aarch64-macos.tar.gz
# macOS Intel
curl -LO https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-x86_64-macos.tar.gz
tar xzf obscura-x86_64-macos.tar.gz
ソースからのビルド(ステルスモード有効化)
ステルスモードを含めてビルドする場合はCargoを使う。初回ビルドではV8をソースからコンパイルするため約5分かかるが、以降はキャッシュが効く。
git clone https://github.com/h4ckf0r0day/obscura.git
cd obscura
cargo build --release
# ステルスモード(フィンガープリントランダム化+トラッカーブロック)を有効にしてビルド
cargo build --release --features stealth
Rust 1.75以上が必要で、rustupから入手できる。
obscura fetch:ページ取得の基本
obscura fetch はURLを指定してページを取得するコマンドだ。
# ページタイトルをJSで取得
obscura fetch https://example.com --eval "document.title"
# ページ内の全リンクを取得
obscura fetch https://example.com --dump links
# 完全なHTMLを出力
obscura fetch https://news.ycombinator.com --dump html
# networkidle0まで待機してからスクレイピング(SPAに有効)
obscura fetch https://example.com --wait-until networkidle0
# ステルスモードで取得(フィンガープリントランダム化+トラッカーブロック)
obscura fetch https://example.com --stealth --eval "document.title"
--wait-until フラグは3段階の待機条件を指定できる:load(デフォルト)、domcontentloaded、networkidle0(ネットワーク通信が一定時間ない状態)。React・Next.js等のSPAでは networkidle0 を使うことで動的コンテンツが生成された後のDOMを取得できる。
obscura scrape:並列スクレイピング
obscura scrape は複数URLを並列処理するコマンドだ。
obscura scrape \
https://example.com/page1 \
https://example.com/page2 \
https://example.com/page3 \
--concurrency 25 \
--eval "document.querySelector('h1').textContent" \
--format json
--concurrency でワーカー数を指定できる。デフォルトは10で、最大値はシステムリソースに依存する。--format json を指定するとURL・取得結果・タイムスタンプを含むJSONを出力する。パイプラインやPythonスクリプトへの入力として扱いやすい。
CDPサーバーモード:Puppeteer・Playwrightからの接続
obscura serve はWebSocketサーバーを起動し、Puppeteer・Playwrightからの接続を受け付けるモードだ。既存のスクレイピングコードをほぼ無改修でObscuraに移行できる。
# 基本起動(ポート9222でWebSocket待機)
obscura serve --port 9222
# ステルスモード有効化
obscura serve --port 9222 --stealth
# 並列ワーカー数を指定
obscura serve --port 9222 --workers 4 --stealth
Puppeteerから接続する
CDPサーバーが起動したら、Puppeteerの connect() でWebSocketエンドポイントに接続する。puppeteer.launch() ではなく puppeteer.connect() を使う点が既存コードとの唯一の違いだ。
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser',
});
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com');
const stories = await page.evaluate(() =>
Array.from(document.querySelectorAll('.titleline > a'))
.map(a => ({ title: a.textContent, url: a.href }))
);
console.log(stories);
await browser.disconnect();
puppeteer-core を使うことでChromeバイナリのバンドルを避けられる。browserWSEndpoint をObscuraのWebSocketアドレスに向けるだけで既存のPuppeteerコードがObscura上で動作する。
Playwrightから接続する
PlaywrightはChromium・Firefox・WebKitを統一APIで操作するフレームワークだが、CDPエンドポイントへの接続もサポートしている。
import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP({
endpointURL: 'ws://127.0.0.1:9222',
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://en.wikipedia.org/wiki/Web_scraping');
console.log(await page.title());
await browser.close();
chromium.connectOverCDP() にObscuraのWebSocketエンドポイントを渡す。PlaywrightのAPIはPuppeteerよりも抽象度が高く、page.locator()・page.fill()・page.click() 等の高レベルAPIがそのまま使える。
ログイン自動化とセッション管理
フォーム送信・ログイン・Cookie維持もサポートしている。
const page = await browser.newPage();
await page.goto('https://quotes.toscrape.com/login');
await page.evaluate(() => {
document.querySelector('#username').value = 'admin';
document.querySelector('#password').value = 'admin';
document.querySelector('form').submit();
});
// Obscuraはフォーム送信後の302リダイレクトを追跡し、Cookieを維持する
ログイン後のCookieはセッション間で Storage.getCookies・Storage.setCookies を通じて保存・復元できる。これにより、同一アカウントで複数のスクレイピングセッションを効率よく実行できる。
ステルスモードとフィンガープリントランダム化の技術的仕組み
Obscuraのステルスモードは --stealth フラグ(またはビルド時 --features stealth)で有効化される。現代のボット検出システムに対してどのように対応しているかを見ていく。
ステルスモードはWebサービスの利用規約、著作権法、不正アクセス禁止法に反する用途には使用してはならない。自社サービスのテスト、research目的の学術利用、利用規約でスクレイピングが許可されているサービスへの適用など、明示的に許可された用途のみに使うこと。Cloudflareが推進するWeb Bot Auth(HTTPメッセージ署名)のように、正規のAIエージェントが自己証明するための仕組みも普及しつつある。
フィンガープリントランダム化の対象
ヘッドレスブラウザがChromeと異なる点は複数ある。ObscuraはこれらをセッションごとにランダムなChromeらしい値で埋める。
navigator.webdriver の除去
通常のヘッドレスブラウザでは navigator.webdriver === true になる。これはbot検出の最も基本的なシグナルだ。Obscuraはこのプロパティを undefined に設定し、通常のChromeと同一の挙動にする。
Function.prototype.toString() のマスキング
JavaScriptで関数を文字列化すると、ネイティブ実装の場合は function () { [native code] } と表示される。Puppeteerが注入するカスタム関数は通常のJS関数として見えてしまうため、これを検出するサイトがある。Obscuraは注入関数の .toString() もネイティブコード形式に偽装する。
Object.keys(window) の安全化
Puppeteerが自動的にwindowオブジェクトに追加するプロパティ($, $$, $$eval 等)は Object.keys(window) で検出できる。Obscuraはこれらの内部プロパティを隠蔽し、通常のChromeのwindow enumeration結果と一致させる。
イベントの isTrusted フラグ
スクリプトがプログラム的に生成したマウス・キーボードイベントは event.isTrusted === false になる。多くのbot検出スクリプトはこれを監視する。Obscuraは dispatchMouseEvent・dispatchKeyEvent で生成したイベントの isTrusted フラグを true に設定する。
ハードウェア情報のランダム化
| 情報の種類 | 説明 |
|---|---|
| GPU | UNMASKED_RENDERER_WEBGL / UNMASKED_VENDOR_WEBGLをセッションごとにランダムな本物のGPU名に設定 |
| スクリーン | screen.width・screen.height・screen.colorDepthを現実的な解像度でランダム化 |
| Canvas | Canvasフィンガープリントを検出不能なレベルでノイズ注入 |
| Web Audio | AudioContextのフィンガープリントをランダム化 |
| Battery | navigator.getBattery()の返す充電状態・残量をランダム化 |
navigator.userAgentData の設定
navigator.userAgentData.brands・uaFullVersion等のhigh-entropy値を、Chrome 145互換の現実的な値に設定する。これにより User-Agent Client Hints を使ったbot検出も回避できる。
3,520トラッカードメインのブロック
ステルスモードでは3,520のトラッカードメインからのリソース読み込みをブロックする。これはGoogle Analytics・Mixpanel・DoubleClick等のアナリティクス、広告ネットワーク、テレメトリサービス、そしてfpjs(FingerprintJS)のようなフィンガープリンティングライブラリを含む。
トラッカーをブロックすることには二重の効果がある。フィンガープリンティングスクリプト自体の読み込みを防ぐという直接的な回避効果に加え、不要なリソース読み込みをカットするためページ読み込み速度も向上する。
パフォーマンスベンチマーク:ヘッドレスChromeとの比較
ObscuraはChromeが持つ多くの機能(PDF印刷、拡張機能、WebGL等)を省略し、自動化に必要な機能に特化している。その結果として生まれたパフォーマンス差は顕著だ。
数値で見るObscura vs headless Chrome
| 指標 | Obscura | Headless Chrome | 差 |
|---|---|---|---|
| ページ読み込み(静的HTML) | 51 ms | ~210 ms | 約4.1倍高速 |
| ページ読み込み(JS重い) | 84 ms | ~450 ms | 約5.4倍高速 |
| ページ読み込み(動的スクリプト) | 78 ms | ~380 ms | 約4.9倍高速 |
| 平均ページ読み込み | 85 ms | ~500 ms | 約5.9倍高速 |
| 起動時間 | 即時 | ~2秒 | - |
| メモリ使用量 | 30 MB | 200+ MB | 約6.7分の1 |
| バイナリサイズ | 70 MB | 300+ MB | 約4.3分の1 |
これらの数値は公式READMEに記載されているベンチマーク結果だ。環境やページの複雑度によって変動するが、傾向として起動オーバーヘッドがゼロであることと、不要なChromeコンポーネントを省いた軽量実装によるものだ。
Chromeの起動2秒というコストは、1000URLを処理するバッチジョブで顕著に響く。単一プロセスを再利用すれば問題ないが、コンテナ環境でインスタンスを動的にスケールする場合は起動コストが積み重なる。Obscuraは起動が即時のため、オートスケーリング環境でのコールドスタートコストがゼロになる。
AIエージェント自動化での活用パターン
ObscuraのCDP互換性と軽量性は、AIエージェントがWebを操作するワークフローに特に適している。agent-browserのようなAIエージェント専用ブラウザ自動化ツールと同様に、ObscuraもAIエージェントの「目と手」として機能する。
LLMにWebコンテキストを供給する
Obscuraの LP.getMarkdown カスタムCDPメソッドは、取得したページをMarkdown形式に変換してLLMへの入力に適した形式で返す。HTMLのタグを除去しながら見出し・リスト・リンクの構造を保持するため、コンテキストとして渡した際のトークン消費を抑えられる。
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser',
});
const page = await browser.newPage();
await page.goto('https://example.com/article');
// Obscura独自の LP.getMarkdown でページをMarkdown化
const client = await page.createCDPSession();
const { markdown } = await client.send('LP.getMarkdown');
// → LLMへのプロンプトにそのまま渡せるMarkdown文字列
await browser.disconnect();
並列AIエージェントによる大規模リサーチ
obscura serve --workers 4 で複数ワーカーを立ち上げ、異なるURLを並列処理するAIエージェントパイプラインを構築できる。
# 4ワーカーのCDPサーバーをステルスモードで起動
obscura serve --port 9222 --workers 4 --stealth
import asyncio
from pyppeteer import connect
async def scrape_page(semaphore, url):
async with semaphore:
browser = await connect(browserURL='http://127.0.0.1:9222')
page = await browser.newPage()
await page.goto(url)
content = await page.evaluate('document.body.innerText')
await page.close()
return {'url': url, 'content': content}
async def main(urls):
semaphore = asyncio.Semaphore(10) # 同時接続数制限
tasks = [scrape_page(semaphore, url) for url in urls]
return await asyncio.gather(*tasks)
スクレイピングパイプラインのコンテナデプロイ
Obscuraは単一バイナリ(70MB)のため、Dockerイメージへの組み込みが簡単だ。Chromeをインストールするよりも大幅にイメージサイズを削減できる。
FROM rust:1.75-alpine AS builder
WORKDIR /app
COPY . .
RUN cargo build --release --features stealth
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /app/target/release/obscura /usr/local/bin/
EXPOSE 9222
CMD ["obscura", "serve", "--port", "9222", "--stealth"]
Chromeを含むPuppeteer用Dockerイメージが一般的に1GB以上になるのと比較して、Obscuraベースのイメージは大幅にスリムになる。
セキュリティと倫理的なスクレイピングの考慮事項
ステルスモードを持つ自動化ツールを使う際には、法的・倫理的な考慮事項を理解しておく必要がある。
Webサービスの利用規約とrobots.txt
obscura serve は --obey-robots フラグをサポートしており、これを有効にすることでrobots.txtを尊重した動作が可能だ。robots.txtはWebサイトがクローラーに対して「取得してはいけないページ」を指定する標準的な仕組みだ。
法的観点では、利用規約でスクレイピングを禁じているサービスへの自動化アクセスは、国・地域によっては不正アクセスとみなされる場合がある。対象サービスの利用規約を確認してから使うことが前提だ。
ボット検出との「軍拡競争」
CloudflareのPrivacy Pass・Web Bot Authなどの次世代ボット認証技術が普及しつつある中で、フィンガープリントの回避だけでは正規アクセスを証明できなくなっていく。正規の自動化ツールであれば、将来的にはHTTPメッセージ署名(RFC 9421)による自己証明を組み込むことが求められるようになる可能性がある。
Obscuraのステルスモードは防御テスト・研究用途・許可されたスクレイピングのための機能であり、利用規約に反するアクセスを助長するためのものではない。開発者が自社サービスのbot検出品質を検証するためのレッドチームツールとして、あるいはAIエージェントのテスト環境として活用するのが本来の用途だ。
レート制限とサーバー負荷への配慮
--concurrency 25 のような高い並列数を指定する場合、対象サーバーへの負荷を考慮すること。礼儀として適切なウェイトを挟む設計が求められる。
// リクエスト間に遅延を挟む例
async function politeScrap(urls) {
const results = [];
for (const url of urls) {
results.push(await scrapPage(url));
await new Promise(r => setTimeout(r, 1000)); // 1秒待機
}
return results;
}
ヘッドレスブラウザエコシステムでのObscuraの位置づけ
ヘッドレスブラウザツールはここ数年で多様化している。Obscuraがどこに位置するかを整理する。
| ツール | 言語 | CDP互換 | ステルス | 特徴 |
|---|---|---|---|---|
| Puppeteer | Node.js | Chrome CDPネイティブ | 設定次第 | Chromeバイナリ必要 |
| Playwright | Node.js/Python/C# | Chrome/Firefox/WebKit | 設定次第 | マルチブラウザ対応 |
| Obscura | Rust | 独自CDP実装 | 組み込み | Chrome不要・軽量 |
| Selenium | 複数 | WebDriver | 設定次第 | 古いが広く使われる |
| Splash | Python/Lua | 独自 | なし | Scrapy連携特化 |
| Ferrum | Rust | Chrome CDP | なし | Chromeバイナリ必要 |
ObscuraはRust製でChrome不要という点で独自のニッチを持つ。Ferrumも同じくRust製だが、ChromeのCDPをクライアントとして使うため実際のChromeバイナリが必要だ。ObscuraはChromeバイナリなしに完全な自己完結ができる唯一のRust製ヘッドレスブラウザエンジンだ。
既存のPuppeteer・Playwrightコードを持つチームが、Chromeバイナリを排除してコンテナイメージを軽量化したい、またはChromeの起動コストを解消したいというユースケースに特に適している。
まとめ:Obscuraが解決する具体的な課題
Obscura v0.1.0(Apache 2.0)は以下の課題を持つプロジェクトに向いている。
① コンテナ・サーバーレス環境:Chromeバイナリ不要でDockerイメージが軽量になり、コールドスタートが即時になる。
② 大規模並列スクレイピング:起動コストゼロ・メモリ30MBのため、同一マシンでChromeより多くのワーカーを動かせる。
③ AIエージェント組み込み:LP.getMarkdownカスタムCDPメソッドでページをLLM向けMarkdownに変換できる。既存のPuppeteer/Playwrightコードはほぼ無改修で動作する。
v0.1.0はまだ実験的なリリースだが、2,400スターという初動は開発者コミュニティの高い関心を示している。CDPの対応範囲が広がれば、軽量ヘッドレスブラウザの選択肢として本格的に普及する可能性がある。