Tony Wang9 分で読めますNode.js での Web スクレイピング — fetch・Cheerio・Puppeteer
Node.js でのスクレイピング: 静的ページはネイティブ fetch と Cheerio、JavaScript ページは Puppeteer。スクレイピング API が有利になる場面もわかります。
一日中 JavaScript を書いているなら、Web をスクレイピングするために Python を学ぶ必要はありません。Node.js は基本的なスクレイパーに必要なものをすべて備えています。fetch は Node 18 以降で組み込まれており、Cheerio は生のマークアップに対して jQuery スタイルの HTML パースを提供し、Puppeteer はページが JavaScript の実行後にしか存在しないときに本物の Chrome を操作します。このガイドでは、これらそれぞれを使って、1 つの一貫した練習用サイト上で、この順番で動作するスクレイパーを組み立てます。そのうえで、自作の Node スクレイパーが本番環境でどこに壁を突き当たるのかを正直にお伝えします。これはPython での Web スクレイピングガイドの JavaScript 版です。同じ道のりを、あなたの言語で進みます。
静的ページ: ネイティブ fetch + Cheerio
ほとんどのスクレイピングチュートリアルは HTTP クライアントのインストールから始まります。それは飛ばしましょう。Node 18 以降、fetch はグローバルに使えます。静的ページのスクレイパーで唯一の依存関係は Cheerio だけです。
npm install cheerio
Cheerio 1.x は CommonJS/ESM の両対応パッケージで、Node 18.17 以降を必要とします。これはネイティブ fetch とうまくかみ合います。ここでは books.toscrape.com をスクレイピングします。まさにこの種の練習のために作られたサンドボックスの書店で、1,000 冊の本、50 ページ、ログインなし、仕掛けなしです。
パターンは決して変わりません。HTML を取得し、Cheerio に読み込み、CSS セレクターで要素を選択し、それぞれを素のオブジェクトに変換します。
// scrape-books.mjs — Node 18+, run with: node scrape-books.mjs
import * as cheerio from "cheerio";
import { writeFile } from "node:fs/promises";
const BASE = "https://books.toscrape.com/";
const res = await fetch(BASE);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const $ = cheerio.load(html);
const books = $("article.product_pod")
.map((_, el) => {
const $el = $(el);
const link = $el.find("h3 a");
return {
title: link.attr("title"),
price: $el.find(".price_color").text(),
inStock: $el.find(".availability").text().trim().startsWith("In stock"),
url: new URL(link.attr("href"), BASE).href,
};
})
.get();
await writeFile("books.json", JSON.stringify(books, null, 2));
console.log(`Saved ${books.length} books`);
注目すべき点がいくつかあります。
cheerio.load(html)は$を返します — jQuery とまったく同じように使う関数です。$("article.product_pod")はすべての本のカードを選択し、.find()、.text()、.attr()がそれぞれの中へと入っていきます。.map(...).get()は Cheerio の選択結果を本物の JavaScript 配列に変換します。コールバックの内側ではelは生の DOM ノードなので、クエリする前に$()で包み直してください。new URL(href, BASE)は相対リンクを解決します。このサイトはcatalogue/some-book_1/index.htmlへリンクしており、これを後で実際に取得できる絶対 URL に変えます。- パースする前に
res.okを確認しましょう。 404 ページやエラーページもやはり HTML です。Cheerio は喜んでそれをパースし、空の配列を返してきます。これは、例外が投げられるよりもずっと分かりにくい失敗です。
JSON ではなく CSV が欲しいですか? このようなフラットな構造なら、ライブラリは要りません。
const header = "title,price,inStock,url";
const rows = books.map((b) =>
[JSON.stringify(b.title), b.price, b.inStock, b.url].join(",")
);
await writeFile("books.csv", [header, ...rows].join("\n"));
タイトルに対する JSON.stringify は、カンマを含むかもしれないフィールドを引用符で囲んでエスケープする安価な手段です。ネストした、あるいは雑然としたものには、きちんとした CSV パッケージを使いましょう。ただし、必要になる前に手を出さないでください。
Cheerio が決してしないことが 1 つあります。JavaScript の実行です。サーバーが送ってきたマークアップをパースする、それがすべてです。だからこそ高速で(ブラウザなし、レンダリングなし)、データが「ページのソースを表示」で見えるあらゆるページに対する適切なデフォルトです。データがそこに無いなら、Puppeteer のセクションへ飛んでください。
ページネーションと礼儀
1 ページはデモにすぎません。本物のスクレイパーは 50 ページすべてを巡ります。そして、どう巡るかが、有用なツールとブロックされた IP との分かれ道です。3 つの習慣が重要です。名乗ること、ペースを保つこと、そして大きな声で失敗することです。
// scrape-all.mjs
import * as cheerio from "cheerio";
import { writeFile } from "node:fs/promises";
const CATALOGUE = "https://books.toscrape.com/catalogue/";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const books = [];
for (let page = 1; ; page++) {
const res = await fetch(`${CATALOGUE}page-${page}.html`, {
headers: { "user-agent": "books-demo/1.0 (learning project)" },
});
if (res.status === 404) break; // ran past the last page — we're done
if (!res.ok) throw new Error(`HTTP ${res.status} on page ${page}`);
const $ = cheerio.load(await res.text());
$("article.product_pod").each((_, el) => {
books.push({
title: $(el).find("h3 a").attr("title"),
price: $(el).find(".price_color").text(),
});
});
console.log(`page ${page}: ${books.length} books so far`);
await sleep(1000); // one request per second is a polite default
}
await writeFile("books.json", JSON.stringify(books, null, 2));
このループは 3 つの習慣を体現しています。
- 説明的な
user-agent。 自分のトラフィックをログで見るサイト運営者が、それが何なのかを判別できるべきです。空の、あるいは偽装したブラウザ UA は、真っ先に不正に見えるものです。 - リクエスト間の遅延。
sleepヘルパーは、同じことをするどのパッケージよりも 4 行短く済みます。1 秒に 1 リクエストなら、このサイトでは 1 分未満で済み、あらゆるレート制限のしきい値から遠く離れていられます。Node の非同期モデルは、Promise.allで 50 件の fetch を一斉に撃ちたくさせますが、こらえてください。並行処理は自分のサーバーのためであって、他人のためではありません。 - 明示的なエラー処理。 ここでの 404 は「最後のページを越えた」を意味するので、ループを終えます。それ以外の非 OK は、メッセージにページ番号を添えて投げます。静かな失敗は、数週間後に気づく穴の空いたデータセットを生みます。
JavaScript で描画されるページ: Puppeteer
遅かれ早かれ、ページを取得し、Cheerio に読み込み、何も得られない、という場面に出くわします。DevTools には明らかに存在するセレクターが、空の配列を返してくるのです。それはクライアントで描画されるページです。サーバーは骨組みを送り、データは後から JavaScript 経由で届いたのです。Cheerio は決して JavaScript を実行しないので、その観点ではデータは存在しません。
解決策は headless ブラウザです。ウィンドウのない本物の Chrome を、スクリプトから操作します。Node での標準的な選択肢は Puppeteer です。
npm install puppeteer
// scrape-rendered.mjs
import puppeteer from "puppeteer";
const browser = await puppeteer.launch(); // headless by default
const page = await browser.newPage();
await page.goto("https://quotes.toscrape.com/js/", {
waitUntil: "networkidle2",
});
await page.waitForSelector(".quote"); // wait for the JS to render them
const quotes = await page.$$eval(".quote", (cards) =>
cards.map((card) => ({
text: card.querySelector(".text")?.textContent.trim(),
author: card.querySelector(".author")?.textContent.trim(),
}))
);
await browser.close();
console.log(quotes);
ここでのターゲットは、私たちの書店の姉妹サイトの JavaScript 描画版です。クライアントサイドのレンダリング後にしか存在しない引用です。2 行がすべての重みを担います。waitUntil: "networkidle2" はネットワークがほぼ静かになるまで goto を保留し、waitForSelector は、あなたが気にする具体的な要素が読み取り前に実際に描画されたことを確認します。page.$$eval はその後、ブラウザの内側であなたのコールバックを実行するので、ごく普通の querySelector コードを書いて、素の JSON を受け取れます。
とはいえ、コストは現実のものです。Cheerio のスクレイプはパースに数ミリ秒ですが、Puppeteer のスクレイプは丸ごとの Chrome を起動し、すべてのアセットを読み込み、すべてのスクリプトを実行します。おおよそ 2 桁遅く、メモリもはるかに重くなります。必要とするページにのみ使い、それ以外はすべて fetch + Cheerio を使い続けましょう。多くの本番構成は両者を混ぜます。Puppeteer で描画し、そのうえで cheerio.load(await page.content()) を使い、より心地よい API でパースするのです。
もう 1 つの主流の選択肢は Playwright です。一級の JavaScript API、自動待機するロケーター、そして Chromium・Firefox・WebKit をまたぐ単一のインターフェースを備えています。私たちの Playwright スクレイピングガイドは Python を中心に書かれていますが、その概念(待機戦略、ロケーター対セレクター、headless ブラウザがコストに見合うのはいつか)は、ほぼ 1 行 1 行そのまま JS API に移せます。スクレイピングに限れば、Puppeteer と Playwright は十分に近く、どちらを選んでも良いでしょう。Playwright の自動待機は不安定なセレクターのバグを一群まるごと取り除き、一方 Puppeteer はより軽量で Chrome 専用です。
Web スクレイピングにおける Node 対 Python
先に正直な答えを言えば、どちらも優れており、決め手はほとんど言語ではありません。各要素がどう並ぶかを見てみましょう。
| Node.js | Python | |
|---|---|---|
| HTTP クライアント | fetch 組み込み(Node 18+) | requests / httpx(インストール) |
| HTML パース | Cheerio | BeautifulSoup、lxml、parsel |
| headless ブラウザ | Puppeteer、Playwright | Playwright、Selenium |
| フルクロールフレームワーク | Crawlee | Scrapy(より成熟) |
| 非同期モデル | 既定で非同期 — 並行処理が自然 | 既定で同期。asyncio はオプトイン |
| スクレイプ後のデータ加工 | 薄い(pandas 相当なし) | pandas、データスタック一式 |
| 最適なとき | アプリがすでに JS/TS。JS 中心のページのスクレイピング | データパイプライン、分析、下流の ML |
Node の本当の強み。依存ゼロの fetch、礼儀正しい並行処理(たとえば 3 サイトを並行し、それぞれをペース配分する)が後付けではなく自然に書ける非同期ランタイム、そして、あなたがスクレイピングするページ自体が JavaScript で書かれているという事実です。クライアント描画のアプリのデバッグは、その言語を話せるほうが自然です。Python の本当の強み。Scrapy はその成熟度で Node に匹敵するものがなく、スクレイプが分析へと流れ込む瞬間に、pandas と科学技術スタックが引き継ぎます。
しかし、決着をつける要因は退屈です。プロジェクトがすでに使っている言語を使いましょう。 スクレイパーはコンポーネントであって、アイデンティティではありません。スタックが TypeScript なら、スクレイパーを Python で書くことは、保守すべき第 2 のツールチェーンを買うだけで、他には何ももたらしません。データ中心のプロジェクトで、本当にゼロから始めるのなら、Python ガイドが反対側の言い分を述べています。
本番の壁 — そして API という橋
上記のすべては練習用サイトで完璧に動きます。そしてそれは偶然ではありません。サンドボックスのサイトが存在するのは、実在するターゲットが反撃してくるからです。同じコードを大規模な EC サイトや旅行サイトに向ければ、本番の壁に突き当たります。Cloudflare、DataDome、PerimeterX とその仲間たちが、あなたのリクエストがページに届く前に、TLS ハンドシェイク、ブラウザフィンガープリント、IP レピュテーション、そして挙動を検査します。ネイティブ fetch は TLS チェックだけでも落ちます。素の Puppeteer は、フィンガープリントのスクリプトが直接テストする自動化の痕跡を漏らします。それと戦うことはできます。ステルスプラグイン、ローテーションする residential プロキシ、ヘッダーの偽装。そして私たちのボットをブロックするサイトをスクレイピングするガイドが、この軍拡競争を正直に扱っています。しかしそれは軍拡競争であり、やがて一つの仕事になります。
もう 1 つの選択肢は、Web スクレイピング API に代わりに戦わせることです。あなたのコードは、すでに知っているものそのまま(fetch の呼び出し 1 回)ですが、プロキシ、フィンガープリント、レンダリング、リトライはエンドポイントの向こう側で起こり、パースすべき HTML の代わりに構造化された JSON が返ってきます。
// Same fetch you've used all guide — the hard part moved behind the endpoint
const keyword = encodeURIComponent("mechanical keyboard");
const res = await fetch(
`https://api.crawlora.net/api/v1/amazon/search?k=${keyword}`,
{ headers: { "x-api-key": process.env.CRAWLORA_API_KEY } }
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
for (const item of data) {
console.log(item.title, item.price);
}
Cheerio もセレクターも、レイアウトが変わったときに直すパーサーもありません。エンドポイントは Amazon の検索結果に対して正規化されたフィールドを返し、他のプラットフォーム向けの同等品がドキュメントにあります。課金は成功時課金です。成功した 2xx レスポンスにのみ課金されるので、ブロックされたり失敗したりしたリクエストには費用がかかりません。DIY と API がそれぞれどこで理にかなうかは、本物のトレードオフを伴う本物の問い(Web スクレイピング対 API がそれを論じます)ですが、要約すればこうです。学習のため、そして協力的なサイトには DIY を。ターゲットが反撃してくるとき、あるいはパーサーの保守がスプリントを食い始めたときには API を。料金は利用量に応じてスケールし、無料枠は月あたり 2,000 クレジット、カード不要です。
- Node 18+ をインストールし、fetch がグローバルに使えるようにした
- 静的ページ: fetch + cheerio.load、CSS セレクターで選択、オブジェクトに変換、JSON か CSV を書き出す
- ページネーション: sleep ヘルパー、説明的な user-agent、大きな声のエラー処理を伴うループ
- 実在するサイトをスクレイピングする前に robots.txt と規約を確認した
- JS 描画ページ: waitForSelector を使う Puppeteer、または自動待機ロケーターの Playwright
- anti-bot の壁に当たったら: フィンガープリントと戦うのをやめ、スクレイピング API のエンドポイントに切り替える
anti-bot の軍拡競争を飛ばそう
JavaScript を書き続けよう — 正規化された JSON を返すドキュメント化されたエンドポイントを呼び、プロキシ・レンダリング・リトライはあなたの代わりに処理される。月あたり 2,000 クレジット、カード不要。
関連記事
- Python での Web スクレイピング — 同じ道のりをもう一方の言語で、BeautifulSoup と Scrapy とともに。
- Playwright での Web スクレイピング — headless ブラウザ、待機戦略、ロケーターについてより深く。
- ボットをブロックするサイトのスクレイピング — anti-bot システムが実際に何を検査し、通過に何が必要か。
- Web スクレイピング対 API — 内製か購入かの判断のためのフレームワーク。
よくある質問
Node.js は Web スクレイピングに向いていますか?
はい。Node 18+ は fetch をネイティブで備え、Cheerio は jQuery スタイルの API で HTML パースを扱い、Puppeteer や Playwright は JavaScript 描画ページのために headless Chrome を操作します。既定で非同期のランタイムは、ペース配分した並行処理も自然にします。スタックがすでに JavaScript か TypeScript なら、スクレイピングのために言語を切り替える理由はありません。
Cheerio と Puppeteer、どちらを使うべきですか?
データがページのソースに見えるときは常に Cheerio を使いましょう。ブラウザなしでミリ秒単位で HTML をパースします。ページがクライアントサイドの JavaScript でデータを描画するときにのみ Puppeteer を使いましょう。Chrome の起動はおよそ 2 桁遅く、はるかに重いからです。多くのスクレイパーは両者を混ぜます。Puppeteer が描画し、そのうえで Cheerio が page.content() をパースするのです。
Node.js でスクレイピングするのに axios など別の HTTP ライブラリは必要ですか?
いいえ。Node 18 以降、fetch はグローバルに使えるので、静的ページのスクレイパーに必要な依存関係は Cheerio だけです。HTTP ライブラリはインターセプターや高度なリトライロジックには便利ですが、HTML の取得や JSON API の呼び出しなら、ネイティブ fetch でまかなえます。
Node.js で JavaScript 描画ページをスクレイピングするには?
headless ブラウザを使います。Puppeteer なら、ブラウザを起動し、waitUntil networkidle2 で page.goto し、必要な要素に waitForSelector し、そして page.$$eval で素のオブジェクトに変換します。主な代替は Playwright で、自動待機ロケーターとクロスブラウザ対応を備え、その JS API は同じ概念を映し出します。
Web スクレイピングには Node.js と Python のどちらが良いですか?
両者は同じくらい有能なので、プロジェクトがすでに話している言語を使いましょう。Node は組み込みの fetch、自然な非同期並行処理、そして JS 中心のサイトをその言語自身でデバッグできる点で勝ります。Python は Scrapy の成熟度と下流の pandas データスタックで勝ります。言語が決め手になることはめったにありません。
なぜ私の Node.js スクレイパーは実在するサイトでブロックされるのですか?
本番のサイトは、TLS ハンドシェイク、ブラウザフィンガープリント、IP レピュテーション、そして挙動を検査する anti-bot システムの後ろにあります。ネイティブ fetch は TLS チェックだけでも落ち、素の Puppeteer は自動化の痕跡を漏らします。ステルスプラグインやローテーションプロキシで戦うこともできますし、プロキシ・フィンガープリント・リトライを処理して構造化 JSON を返すスクレイピング API 経由にすることもできます。
Node.js でスクレイピングするとき、どう礼儀正しくすればよいですか?
運営者があなたのトラフィックを識別できるよう説明的な user-agent を送り、リクエスト間に遅延を入れ(1 秒に 1 回が安全なデフォルト)、robots.txt とサイトの規約を読んで尊重し、公開データだけを集め、エラー時はリトライを叩き込むのではなく大きな声で失敗しましょう。