動的サイトも逃さない。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に任せる」 ハイブリッド戦略を取りました。

アーキテクチャ

  1. Puppeteer: JSを実行し、DOMが生成されるまで待機してHTML文字列を取得
  2. 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の方がまだ分があると感じました。