動的サイトも逃さない。Puppeteer + Cheerio ハイブリッドスクレイピング戦略
スクレイピングの課題:「fetchだけじゃ足りない」
「指定されたURLのコンテンツを読み取ってAIに診断させる」という機能を作る際、最初に思いつくのは fetch(url) してHTMLを取得する方法です。
しかし、現代のWebは SPA (Single Page Application) だらけです。
ReactやVueで作られたサイトを fetch すると、返ってくるのは中身のない <div id="root"></div> だけ。これでは診断できません。
そこで Puppeteer(Headless Chrome) の出番ですが、すべてをブラウザで処理すると重すぎます。今回は 「Puppeteerで描画し、解析はCheerioに任せる」 ハイブリッド戦略を取りました。
アーキテクチャ
- Puppeteer: JSを実行し、DOMが生成されるまで待機してHTML文字列を取得
- Cheerio: 取得したHTML文字列を高速にパースして要素抽出
実装のポイント
1. Vercel環境でのPuppeteer起動
ローカル(macOS/Windows)と本番(Vercel Linux)ではChromeのパスが異なります。@sparticuz/chromium を条件分岐で読み込むのが最大のハマりどころです。
typescript
// scraper.ts
const browserOptions = isVercel
? {
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
}
: {
executablePath: '/Applications/Google Chrome.app/...', // ローカル用
headless: true,
}; 2. 「無駄なもの」を徹底的にブロックする
診断に必要なのは「テキスト」と「レイアウト構造」だけです。画像、CSS、フォント、Analyticsのスクリプトなどはリソースの無駄です。
typescript
await page.setRequestInterception(true);
page.on('request', (req) => {
const resourceType = req.resourceType();
if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
req.abort(); // 容赦なく捨てる
} else {
req.continue();
}
}); これで実行速度が3倍くらい速くなります。
3. Bodyのテキスト抽出ロジック
「記事の本文」を抜き出すのは意外と難しいです。navやfooter、広告を除外したいからです。 今回はCheerioで以下のようなヒューリスティックな抽出を行いました。
typescript
// 意味のありそうなタグを優先
const contentSelectors = [
'article',
'main',
'[role="main"]',
'.content',
'#content'
];
// 文字数が少ないpタグはゴミ(メニュー項目など)として捨てる
$('p').each((_, el) => {
const text = $(el).text().trim();
if (text.length > 30) {
bodyText += text + '\n';
}
}); 結論
- 動的サイト対応 ならPuppeteer一択
- 解析速度 ならCheerio一択
この2つを「HTML文字列のパス」で繋ぐのが、現代のサーバーレス環境におけるスクレイピングの最適解です。Playwrightも素晴らしいですが、Vercel上のサイズ制限(50MB)と戦うなら、枯れた技術であるPuppeteer + Sparticuzの方がまだ分があると感じました。