llm-scraper 評価: Zod スキーマで Playwright のページを構造化データに変える
Turn any webpage into structured data using LLMs
ひと目でわかる
- これは何?
- Playwright の Page オブジェクトを LLM に渡し、Zod スキーマに沿ったオブジェクトを取り出す TypeScript ライブラリ。CSS セレクタを書かずに済む代わりに、抽出のたびに推論コストと非決定性を引き受ける設計になっている。
- 誰に向いている?
- 採用を検討すべきなのは、Playwright で既にページを開いており、対象サイトの DOM が頻繁に変わるためにセレクタの保守に疲れているチームである。逆に、1 回の抽出コストを数円単位で見積もる必要がある大量バッチ、あるいは出力の決定性が要件に含まれる処理には向かない。
- 商用利用できる?
- できます。MIT は寛容なライセンスで、著作権表示とライセンス表示を残せば、使用・改変・販売が可能です。
- 今もメンテナンスされている?
- されています。最後のコミットは 6 日前です。
- 何の言語で書かれている?
- 主に TypeScript です(GitHub の言語統計による)。
回答はプロジェクトの GitHub データ(最終同期:2026年9月15日)と当サイトの分析に基づくもので、法的助言ではありません。
オープンソース詳細解説
セレクタの保守ではなくスキーマの保守を選ぶという発想
通常のスクレイピングは、ページの構造を CSS セレクタや XPath で書き下し、その後に文字列を型変換する。サイトのマークアップが変わればセレクタは静かに壊れ、テストがなければ気づくのは本番の欠損データになる。llm-scraper はこの順序を逆にする。抽出したい形を Zod スキーマとして先に宣言し、ページの内容を LLM に渡して、その形に合うオブジェクトを返させる。README の例では z.object({ top: z.array(z.object({ title: z.string(), points: z.number(), by: z.string(), commentsURL: z.string() })).length(5).describe('Top 5 stories on Hacker News') }) というスキーマが示されている。対象は Hacker News のトップページで、得られるのは title、points、by、commentsURL を持つ 5 件の配列である。
読み手として想定されているのは、すでに Node.js と Playwright を動かしていて、TypeScript の型を捨てたくない開発者だ。Python の BeautifulSoup で完結する小規模な取得や、RSS や公式 API が存在するサイトは、そもそもこのライブラリの対象外である。API があるなら API を使うほうが安く、速く、壊れにくい。
run が受け取る Page と 6 つのフォーマットモード
中心となる API は scraper.run(page, Output.object({ schema }), options) である。第一引数は Playwright の Page そのもので、URL ではなくページ実体を渡す。つまり遷移、待機、認証、Cookie の処理はすべて呼び出し側の Playwright コードが担う。ライブラリは取得を代行しない。
第二引数は Vercel AI SDK の Output.object({ schema }) で、スキーマは Zod でも JSON Schema でもよいと README は説明している。第三引数の options には format を指定し、README は 6 種類を挙げる。html は前処理済み HTML、raw_html は前処理なしの生 HTML、markdown は Markdown、text は Readability.js で抽出した本文テキスト、image はスクリーンショット(マルチモーダル限定)、custom は独自関数で組み立てた内容である。
この 6 モードの存在が、実運用での設計判断をほぼ決める。text を選べばトークン数は小さくなるが、Readability.js が本文と判定しなかった要素はモデルに届かない。ナビゲーションや価格表の一部が落ちる可能性がある。raw_html を選べば情報は落ちないが、トークン数とノイズが増える。README はどのモードがどの用途に適するかまでは踏み込んでおらず、ここは自分で試して決める領域である。
LLM プロバイダの差し替えは AI SDK のファクトリ関数に委ねる
モデルの指定は llm-scraper の仕事ではない。README の手順では、まず npm i zod playwright llm-scraper を実行し、そのうえでプロバイダごとのパッケージを追加してモデルオブジェクトを作る。OpenAI なら @ai-sdk/openai の openai('gpt-4o')、Anthropic なら @ai-sdk/anthropic の anthropic('claude-3-5-sonnet-20240620')、Google なら @ai-sdk/google の google('gemini-1.5-flash') といった具合である。
OpenAI 互換のエンドポイントは createOpenAI に baseURL と apiKey を渡して差し替える。README の Groq の例では baseURL: 'https://api.groq.com/openai/v1'、apiKey: process.env.GROQ_API_KEY、モデル名 llama3-8b-8192 が使われている。ローカル推論は ollama-ai-provider-v2 の ollama('llama3') で接続する。つまり OpenAI 形式の API を持つサービスであれば、llm-scraper 側のコードを変えずにモデルだけを入れ替えられる。
この構造の副作用として、抽出品質の責任範囲がはっきりしない。スキーマが悪いのか、フォーマットモードが悪いのか、モデルが弱いのかを切り分けるには、同じページと同じスキーマでモデルだけを変えて比較する必要がある。README はその比較手順も、推奨モデルの一覧も提供していない。
stream と generate は別の用途に向いた別の道具である
run は完成したオブジェクトを返す。stream は同じ呼び出しを部分的オブジェクトのストリームに変え、for await (const data of stream) で断片を受け取る。README の例では console.log(data.top) を繰り返す形になっており、長い配列を扱うときに最初の数件を早く表示したい場面で意味を持つ。ただし途中経過のオブジェクトはスキーマを満たしていない可能性がある。UI に流すなら、各断片を検証せずに表示する前提で設計する必要がある。
generate は性質がまったく違う。ページとスキーマを渡すと、そのページから同じ内容を取り出すための Playwright スクリプトを文字列として返す。README の例では const { code } = await scraper.generate(page, Output.object({ schema })) の後、const result = await page.evaluate(code) を実行し、最後に schema.parse(result) で検証している。つまり LLM の推論を一度だけ払い、以降は通常の DOM 操作として繰り返し実行する。
ここには明確なトレードオフがある。generate が出力したコードは、その時点のページ構造に依存する。サイトがマークアップを変えれば、生成されたスクリプトは run と同じように壊れる。しかも壊れ方は静かで、schema.parse が例外を投げるまで気づかない可能性がある。LLM を毎回呼ぶコストを削る代わりに、生成コードの再生成タイミングを自分で管理する責任を引き受けることになる。
コストと決定性を引き受けるという制約
このライブラリの最も大きな制約は、抽出のたびに LLM の推論が発生することである。1000 ページを処理すれば 1000 回の推論が走る。README には価格の記載も、トークン数の目安も、レート制限への対処もない。raw_html を選べば HTML 全体がコンテキストに入るため、ページによっては入力トークンが大きく膨らむ。format の選択は精度だけでなく費用の問題でもある。
第二の制約は決定性である。同じページ、同じスキーマ、同じモデルでも、生成モデルの出力は毎回同一とは限らない。z.number() で宣言した points が文字列で返れば検証に失敗する。スキーマの .describe() はモデルへの指示として機能するが、制約をどこまで守らせるかはモデル依存である。決済金額や在庫数のように 1 桁の誤りが業務に影響する値を扱うなら、抽出結果をそのまま信頼する設計は取れない。
第三に、ページ内容がそのまま外部の LLM プロバイダに送られる。社内システムやログイン後の画面を対象にする場合、この経路が組織のポリシーに適合するかを先に確認する必要がある。README はこの点に触れていない。
Firecrawl との違いはブラウザを誰が持つかにある
比較対象として分かりやすいのは Firecrawl である。どちらも LLM で Web ページを構造化データに変換するが、責務の境界が違う。Firecrawl は URL を渡すと取得から変換までをサービス側で行う。llm-scraper はブラウザを持たず、呼び出し側が起動した Playwright の Page を受け取る。
この差は、ログインが必要な画面、JavaScript の実行後にしか内容が現れない画面、社内ネットワーク内のホストなど、外部サービスから到達できないページを扱えるかどうかに直結する。llm-scraper は Playwright が到達できるページなら対象にできる。逆に、大量の URL を並列で処理するバッチや、ブラウザのライフサイクル管理をしたくない用途では、取得まで含めて面倒を見るサービスに分がある。
もう一つの差は出力の検証方法である。llm-scraper は Zod スキーマを呼び出し側のコード内に置き、TypeScript の型としてそのまま使える。スキーマがコードの一部になるため、抽出ロジックのレビューが通常のプルリクエストの流れに乗る。
MIT ライセンスと保守コストの見積もり
ライセンスは MIT で、商用利用、改変、再配布が許される。著作権表示とライセンス全文の保持が条件になる。ここで注意すべきは、llm-scraper 自体のライセンスが、依存する Playwright(Apache-2.0)や各 AI SDK プロバイダのパッケージ、利用するモデルのサービス利用規約を上書きしないことである。特にモデル側の規約は、スクレイピング対象サイトの利用規約とは別に確認が必要になる。法的判断はここでは扱わない。
保守の観点では、README が version 2.0 で Vercel AI SDK 6 対応と例の更新を行ったと述べており、依存先のメジャーバージョンに追随する形で API が動いてきたことが読み取れる。つまり AI SDK 側の破壊的変更が llm-scraper の呼び出しコードにも波及する可能性がある。Output.object({ schema }) という書き方自体が AI SDK の API なので、この部分は AI SDK のリリースノートを追う対象になる。
リポジトリにはリリースノートが取得できておらず、バージョンごとの変更点をここで示すことはできない。採用前に tags と CHANGELOG の有無を確認することを勧める。README にはスターを促す一文があり、コントリビューションは issue と pull request で受け付けると書かれているが、応答時間やメンテナンス体制についての記載はない。
編集部の結論
採用を検討すべきなのは、Playwright で既にページを開いており、対象サイトの DOM が頻繁に変わるためにセレクタの保守に疲れているチームである。逆に、1 回の抽出コストを数円単位で見積もる必要がある大量バッチ、あるいは出力の決定性が要件に含まれる処理には向かない。導入前に確認するのは 3 点で、第一に対象ページが format: 'text' の Readability.js 処理で本文を保持できるか、第二に Output.object({ schema }) のスキーマが .length(5) のような検証制約を含めてモデルの出力と噛み合うか、第三に generate が返す Playwright スクリプトを page.evaluate で実行した結果が schema.parse を通るか。この 3 つが通れば、セレクタを書く仕事はスキーマを書く仕事に置き換わる。
コミュニティノート