In this Article

web scraping with javascript

In this Article

本ガイドでは、大規模な公開データ収集で広く使われるNode.jsを用いて、JavaScriptでウェブスクレイピングを行う方法を解説します。重要なのは、対象ページに合ったツールを選ぶことです。静的HTMLには軽量なHTTPクライアントを、スクリプトでコンテンツが読み込まれるページにはヘッドレスブラウザを使用します。

以下では、両方のケースで動作するNode.jsコードに加え、プロジェクトのセットアップ、JSONとCSVへの構造化データのエクスポート、ページネーション、バックオフを伴う再試行、レート制限、プロキシ認証、XHRのインターセプト、さらにスクレイピングツールを長期的に安定運用するための倫理とブロック対策を扱います。

DataImpulseは、195カ国で9,000万以上の住宅用、モバイル、データセンターIPアドレスを提供する、倫理的なプロキシサービスです。1GBあたり1ドルからの従量課金モデルを採用しており、トラフィックに有効期限はありません。Webスクレイピング、広告検証、価格モニタリング、市場調査、複数アカウント管理に利用されています。

主な事実

  • 2つのツールチェーン: 静的ページにはfetchやcheerioのようなHTTPクライアントとパーサーで十分ですが、ブラウザ内でコンテンツをレンダリングする動的ページには、PuppeteerやPlaywrightのようなヘッドレスブラウザが必要です。
  • 最適なプロキシタイプ: ローテーション型の住宅用プロキシ。実際の利用者のIPを使うことで、検知される可能性を抑えられます。
  • 価格: 1GBあたり1ドルから、従量課金制で、トラフィックの有効期限なし、サブスクリプション不要。
  • カバレッジ: 195カ国で9,000万以上の倫理的に調達されたIP。
  • 信頼性: 成功率99.51%、G2で5点満点中4.8の評価。
  • プロトコルとターゲティング: HTTP、HTTPS、SOCKS5、国別ターゲティングが含まれます。
静的 vs 動的: JavaScriptスクレイピングの進め方を選ぶ

JavaScriptによるWebスクレイピングとは何ですか?

JavaScriptによるウェブスクレイピングとは、JavaScriptでウェブページにプログラムからリクエストを送り、構造化データを抽出する処理です。通常はブラウザのタブ内ではなく、Node.jsランタイム上で実行されます。

JavaScriptはDOMを扱うための仕組みを備えているため、自然な選択肢です。要素の選択方法もフロントエンド開発者には馴染みやすいでしょう。スクレイピングツールの基本的な処理は3つです。HTTP経由でページを取得し、返されたマークアップを検索可能なツリーとして解析して、タイトル、価格、リンクなど必要なフィールドを取り出します。難しさは解析そのものより、対象ページがどのようにデータを配信するかに応じて適切なツールを選ぶ点にあります。静的ページは最初のHTMLレスポンスにデータを含みますが、動的ページでは読み込み後にブラウザ内でデータが組み立てられます。この2つのケースにはそれぞれ異なるツールチェーンが必要です。

Node.jsスクレイピングプロジェクトはどのようにセットアップしますか?

プロジェクトフォルダを作成し、npmで初期化し、スクレイピングのコードを書く前に必要なライブラリをインストールします。

Node.js 18以降にはグローバルのfetchが組み込まれているため、単純なGETリクエストにHTTPライブラリは不要です。一方で、多くのチームはインターセプターやプロキシの扱いやすさからaxiosを引き続き利用しています。以下のコマンドは、プロジェクトの雛形を作成し、解析用のcheerio、代替クライアントとしてのaxios、並行数制御用のp-limit、そして動的ページ用のPuppeteerとPlaywrightをインストールします:

mkdir js-scraper && cd js-scraper
npm init -y
npm install cheerio axios p-limit
npm install puppeteer playwright

# enable modern import syntax
npm pkg set type=module

type=moduleを設定すると、import文を使用できるようになります。CommonJSを好む場合は、デフォルトのままにして代わりにrequireを使用してください。このガイドの例はimport構文を使用していますが、それぞれrequire呼び出しに直接対応しています。インストールが完了すれば、1つのプロジェクトで静的スクレイピングと動的スクレイピングの両方に必要なものがすべて揃います。

fetchとcheerioで静的ページをスクレイピングするにはどうすればよいですか?

静的ページの場合、組み込みのfetch関数でHTMLをダウンロードし、サーバー上でjQueryスタイルのAPIを提供するcheerioで解析します。

静的ページは最初のHTMLレスポンスでコンテンツを直接返すため、ブラウザレンダリングは不要です。この方法は、画像やフォント、スクリプトではなくマークアップのみをダウンロードするため、高速で低コストです。以下の例はページを取得し、cheerioに読み込んで、各商品カードを読み取ります:

import * as cheerio from 'cheerio';

async function scrape(url) {
  const res = await fetch(url, {
    headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
  });
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const html = await res.text();
  const $ = cheerio.load(html);

  const items = [];
  $('.product').each((i, el) => {
    items.push({
      title: $(el).find('h2').text().trim(),
      price: $(el).find('.price').text().trim(),
      link: $(el).find('a').attr('href')
    });
  });
  return items;
}

console.log(await scrape('https://example.com/catalog'));

axiosを好む場合、解析部分は同一で、リクエストだけが変わります。axiosは2xx以外のステータスコードで自動的にエラーをスローし、プロキシ設定を簡単にするため、多くの本番スクレイパーで使用されています:

import axios from 'axios';
import * as cheerio from 'cheerio';

const { data: html } = await axios.get('https://example.com/catalog', {
  headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' },
  timeout: 15000
});
const $ = cheerio.load(html);
const titles = $('.product h2').map((i, el) => $(el).text().trim()).get();
console.log(titles);

スクレイピングを始める前に、対象サイトがそれを許可しているか確認してください。ウェブサイトがスクレイピングを許可しているか確認する方法についてのガイドでは、robots.txtと利用規約について説明しています。

構造化データをJSONやCSVに抽出するにはどうすればよいですか?

各レコードをプレーンオブジェクトの配列に集め、その配列をJSONファイルに書き出すか、CSVファイル用にカンマ区切りの行に平坦化します。

抽出した生のテキストは、予測可能な形式で保存されて初めて役に立ちます。JavaScriptの配列をそのままシリアライズできるため、JSONはもっとも手軽な保存形式です。以下のスニペットは、組み込みのファイルシステムモジュールを使って抽出したアイテムをディスクに書き込みます:

import { writeFile } from 'node:fs/promises';

const items = await scrape('https://example.com/catalog');
await writeFile('products.json', JSON.stringify(items, null, 2), 'utf-8');
console.log(`Saved ${items.length} records to products.json`);

CSVは、データがスプレッドシートやデータウェアハウスに渡される場合に適しています。引用符をエスケープしてフィールドを結合すれば、ライブラリなしでも構築できますが、価格やタイトルにカンマが含まれる可能性があるため、エスケープが重要です:

import { writeFile } from 'node:fs/promises';

function toCsv(rows) {
  const headers = Object.keys(rows[0]);
  const escape = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`;
  const lines = [headers.join(',')];
  for (const row of rows) {
    lines.push(headers.map((h) => escape(row[h])).join(','));
  }
  return lines.join('\n');
}

const items = await scrape('https://example.com/catalog');
await writeFile('products.csv', toCsv(items), 'utf-8');

ページのレイアウトが少し変わったときに下流のツールが壊れないよう、実行のたびにフィールド名を安定させておいてください。画像などのバイナリ資産も取得する必要がある場合は、ウェブサイトから画像をスクレイピングする方法についてのガイドをご覧ください。

ページネーションで複数ページをスクレイピングするにはどうすればよいですか?

サイトのページURLをループするか、各ページの次ページリンクをたどってそれぞれをスクレイピングし、結果がなくなったら停止します。

ほとんどのカタログは、?page=2のようなクエリパラメータを使って結果を番号付きページに分割するか、次へのリンクを公開しています。最も安全なパターンは、各ページを読み込み、そのレコードを収集し、次のページを探して見つからなければ停止するというものです。以下の例は、あるページが0件のアイテムを返すまで番号付きページを順に取得します:

import * as cheerio from 'cheerio';

async function scrapeAll(base) {
  const all = [];
  for (let page = 1; page <= 50; page++) {
    const res = await fetch(`${base}?page=${page}`, {
      headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
    });
    if (!res.ok) break;
    const $ = cheerio.load(await res.text());
    const items = $('.product').map((i, el) => ({
      title: $(el).find('h2').text().trim(),
      price: $(el).find('.price').text().trim()
    })).get();

    if (items.length === 0) break;
    all.push(...items);
    await new Promise((r) => setTimeout(r, 1000));
  }
  return all;
}

console.log((await scrapeAll('https://example.com/catalog')).length);

ページ間の1秒の一時停止と50回の反復というハードキャップに注目してください。これは、サイトが空のページを返さない場合に暴走ループになるのを防ぎます。番号付きページではなくスクロールによってコンテンツが読み込まれる場合は、以下のPuppeteerのセクションで扱うヘッドレスブラウザが必要です。

再試行、バックオフ、レート制限はどのように扱いますか?

各リクエストを、失敗のたびに待機時間が長くなる再試行ヘルパーでラップし、サイトの許容範囲内に収まるよう同時に実行するリクエスト数を制限します。

ネットワークは信頼できず、サーバーは一時的なエラーを返すため、最初の失敗であきらめるスクレイパーは不必要にデータを失います。指数バックオフを使った再試行ヘルパーは、試行のたびに段階的に長く待機し、これによりレート制限されたサーバーが回復する時間も確保されます:

async function fetchWithRetry(fn, retries = 4, delay = 1000) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === retries) throw err;
      const wait = delay * 2 ** (attempt - 1);
      console.warn(`Attempt ${attempt} failed, retrying in ${wait}ms`);
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}

const html = await fetchWithRetry(async () => {
  const res = await fetch('https://example.com/catalog');
  if (res.status === 429) throw new Error('Rate limited');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.text();
});

バックオフは個々のリクエストを処理しますが、並行して実行される数も制御する必要があります。何百ものリクエストを同時に送ると、対象サーバーの性能を低下させ、トラフィックにフラグが立つおそれがあります。p-limitライブラリは並行数を制限し、固定数のワーカーがURLのキューを処理するようにします:

import pLimit from 'p-limit';

const limit = pLimit(5); // at most 5 requests in flight
const urls = Array.from({ length: 100 }, (_, i) =>
  `https://example.com/item/${i + 1}`
);

const results = await Promise.all(
  urls.map((url) =>
    limit(() => fetchWithRetry(async () => {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res.text();
    }))
  )
);

console.log(`Fetched ${results.length} pages with a cap of 5 in parallel`);

バックオフと並行数の上限を組み合わせることで、負荷を妥当な範囲に保ち、成功率を高く維持できます。より広範なチェックリストについては、Webスクレイピングのベストプラクティスガイドをご覧ください。

Puppeteerで動的ページをスクレイピングするにはどうすればよいですか?

読み込み後にJavaScriptでコンテンツを構築する動的ページでは、PuppeteerでヘッドレスのChromeを操作し、対象セレクタを待ち、完全にレンダリングされたDOMを読み取ります。

多くの現代的なサイトは、ほぼ空のHTMLシェルを返し、その後バックグラウンドリクエストでデータを取得します。HTTPクライアントには有用なものは何も見えないため、ページ自体のJavaScriptを実行するエンジンが必要です。PuppeteerではヘッドレスのChromeを操作し、要素を待機したり、クリックしたり、スクロールしたりできます。以下の例はセレクタを待ってから、ブラウザコンテキスト内でデータを抽出する前に、無限スクロールでコンテンツを読み込むため、繰り返しスクロールします:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (compatible; MyScraper/1.0)');
await page.goto('https://example.com/feed', { waitUntil: 'networkidle2' });

// wait until the first batch of cards is present
await page.waitForSelector('.product');

// infinite scroll: scroll to the bottom until height stops growing
let previousHeight = 0;
for (let i = 0; i < 20; i++) {
  const height = await page.evaluate('document.body.scrollHeight');
  if (height === previousHeight) break;
  previousHeight = height;
  await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
  await new Promise((r) => setTimeout(r, 1500));
}

const items = await page.evaluate(() =>
  Array.from(document.querySelectorAll('.product')).map((el) => ({
    title: el.querySelector('h2')?.innerText.trim(),
    price: el.querySelector('.price')?.innerText.trim()
  }))
);

console.log(`Extracted ${items.length} items`);
await browser.close();

ページの高さが増加しなくなるとすぐにループが停止し、これは新しいコンテンツが読み込まれていないことを示します。20回というスクロールのハードキャップは無限ループを防ぎます。この種の対象について詳しくは、動的なWebページをスクレイピングする方法についてのガイドをご覧ください。

Puppeteerの代わりにPlaywrightを使うにはどうすればよいですか?

PlaywrightはPuppeteerとほぼ同じように機能しますが、Chromium、Firefox、WebKitを標準でサポートし、不安定なセレクタによる問題を減らす自動待機機能付きロケーターも備えています。

PlaywrightはPuppeteerの元エンジニアの一部によって作られ、そのAPIの大部分を共有しているため、移行は通常、いくつかの呼び出し名を変更する程度で済みます。そのロケーターは要素が操作可能になるのを自動的に待つため、多くの手動のwaitForSelector呼び出しが不要になります。同等のスクレイパーは次のようになります:

import { chromium } from 'playwright';

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
  userAgent: 'Mozilla/5.0 (compatible; MyScraper/1.0)'
});
const page = await context.newPage();
await page.goto('https://example.com/feed', { waitUntil: 'networkidle' });

await page.locator('.product').first().waitFor();

const items = await page.locator('.product').evaluateAll((els) =>
  els.map((el) => ({
    title: el.querySelector('h2')?.innerText.trim(),
    price: el.querySelector('.price')?.innerText.trim()
  }))
);

console.log(`Extracted ${items.length} items`);
await browser.close();

同じスクレイピングツールを複数のブラウザエンジンでテストしたい場合や、組み込みのネットワークモックが必要な場合はPlaywrightを選びましょう。Chromeだけを対象とし、依存関係を小さく保ちたい場合はPuppeteerが適しています。どちらもfetchとcheerioよりはるかに多くのメモリを消費するため、本当にレンダリングを必要とするページのために取っておいてください。

どのJavaScriptスクレイピングツールを選ぶべきですか?

静的HTMLにはcheerioと組み合わせたfetchまたはaxiosを使い、ページがブラウザ内でデータをレンダリングする場合にのみPuppeteerやPlaywrightを利用してください。

正しいツールは、ページがどのようにデータを配信するか、そして速度をレンダリングの忠実度よりどれだけ重視するかによって決まります。HTTPクライアントは高速で軽量ですがJavaScriptを実行できません。ヘッドレスブラウザは実際のユーザーが目にするすべてを実行しますが、より多くのメモリと時間を要します。以下の表はそのトレードオフをまとめたものです:

ツール JS実行 速度 最適な用途
fetch + cheerio いいえ 最速 静的HTML、高ボリューム
axios + cheerio いいえ 高速 静的HTML、簡単なプロキシ設定
Puppeteer はい 低速 Chrome専用の動的ページ
Playwright はい 低速 動的、クロスブラウザテスト

本番環境では、両者を組み合わせることがよくあります。レンダリングが必要なページやエンドポイントに到達するためだけにヘッドレスブラウザを使い、大部分のリクエストにはプレーンなHTTPクライアントを使うのです。JavaScriptはこの作業に使える複数の実行可能なスタックの1つです。エコシステムを比較しているなら、Rustによるウェブスクレイピングガイドがコンパイル型の代替手段を扱っています。

JavaScriptスクレイパーをプロキシ経由でルーティングするにはどうすればよいですか?

HTTPクライアントやブラウザの起動オプションにプロキシ設定を渡し、リクエストが別のIPアドレスから発信されるようにします。これにより負荷が分散され、単一のアドレスがレート制限される可能性が減ります。

1つのIPから多数のページをスクレイピングすると、すぐにブロックされかねません。IPアドレスのプール間でリクエストをローテーションすれば、トラフィックを複数の訪問者からのものに見せられます。axiosでは、リクエスト設定にプロキシのホスト、ポート、認証情報を指定します。以下の例はDataImpulseのゲートウェイを経由します:

import axios from 'axios';

const { data } = await axios.get('https://example.com', {
  proxy: {
    protocol: 'http',
    host: 'gw.dataimpulse.com',
    port: 823,
    auth: { username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD' }
  },
  headers: { 'User-Agent': 'Mozilla/5.0 (compatible; MyScraper/1.0)' }
});
console.log(data.length, 'bytes received through the proxy');

Puppeteerはプロキシを起動引数として受け取り、その後ページ上で認証を行います。これはChromeがプロキシURL内の認証情報を受け付けないためです。このパターンはPlaywrightでも同じで、ユーザー名とパスワードを直接持つプロキシオブジェクトを受け付けます:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--proxy-server=gw.dataimpulse.com:823']
});
const page = await browser.newPage();
await page.authenticate({
  username: 'YOUR_USERNAME',
  password: 'YOUR_PASSWORD'
});
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
console.log(await page.title());
await browser.close();

DataImpulseは、HTTP、HTTPS、SOCKS5を通じて住宅用プロキシデータセンタープロキシモバイルプロキシを提供しており、ローテーションセッションとスティッキーセッションの両方があります。国別ターゲティングは含まれていますが、州、市区町村、郵便番号、ASNによるターゲティングは有料アドオンです。プロキシリクエストが認証エラーを返す場合、HTTPエラー407についての解説で最も一般的な原因を説明しており、プロキシ認証ガイドでは認証情報の形式についてさらに詳しく扱っています。

XHRやJSONエンドポイントをインターセプトするにはどうすればよいですか?

レンダリングされたHTMLをスクレイピングする代わりに、ヘッドレスブラウザ内でネットワークトラフィックを監視し、ページがバックグラウンドで取得するJSONレスポンスを読み取ります。これはしばしばより簡潔で高速です。

動的ページは通常、JSONを返すバックグラウンドAPIからデータを取得します。そのレスポンスを直接読み取ることで、DOM解析を完全に省略し、よく構造化されたレコードが得られます。PuppeteerもPlaywrightも、レスポンスをリッスンして必要なものをキャプチャできます。以下のPuppeteerの例は、APIパスからの任意のJSONレスポンスをキャプチャします:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();

const captured = [];
page.on('response', async (response) => {
  const url = response.url();
  if (url.includes('/api/') && response.headers()['content-type']?.includes('application/json')) {
    try {
      captured.push(await response.json());
    } catch (err) {
      // non-JSON body, ignore
    }
  }
});

await page.goto('https://example.com/feed', { waitUntil: 'networkidle2' });
console.log(`Captured ${captured.length} JSON payloads`);
await browser.close();

エンドポイントのURLとそのパラメータが分かれば、多くの場合ブラウザを完全に省略し、ページが送信したのと同じヘッダーを渡しながら、プレーンなfetchでJSON APIを呼び出すことができます。これにより、静的スクレイピングの速度と、レンダリングを伴うスクレイピングの網羅性を両立できます。動的サイトでは、特に大きな性能向上につながる手法です。

スクレイピングの倫理的・法的な限界とは何ですか?

公開データだけをスクレイピングし、サイトのrobots.txtと利用規約を尊重して、個人データの収集やサーバーへの過度な負荷を避けてください。これらの実践は、あなたのプロジェクトを正当化可能かつ持続可能なものに保ちます。

ドメインのルートにあるrobots.txtを確認し、サイトがクローラーにアクセスを控えるよう求めているパスを把握して、その要請を尊重してください。適切なUser-Agentでスクレイピングツールを識別し、リクエストをスロットリングし、同じページを不必要に再取得しないようレスポンスをキャッシュしてください。インフラの側面では、IPアドレスの出所が重要です。DataImpulseは、オプトインして報酬を受け取るユーザーから調達した倫理的プロキシを運用しており、GDPRに準拠し、データ処理契約も用意されています。倫理はコンプライアンスの問題だけではありません。配慮があり透明性の高いスクレイピングは、長期的に運用しやすいスクレイピングでもあります。持続可能な収集の背後にある原則については、倫理的なウェブスクレイピングについてのガイドをご覧ください。

JavaScriptスクレイピングツールとそれぞれの使いどころ

よくある質問

JavaScriptでスクレイピングするにはNode.jsが必要ですか?

1ページだけの簡単なテストを超える用途では、はい、必要です。Node.jsを使えば、サーバー上でスクレイパーを実行し、多くのURLをループし、再試行を処理し、データをエクスポートし、プロキシを使用できます。これらはいずれもブラウザのコンソールではうまく行えません。

fetchとcheerioの代わりにPuppeteerを使うべきなのはいつですか?

データが最初のHTMLレスポンスに存在する場合はfetchまたはaxiosとcheerioを使ってください。コンテンツがページ読み込み後にJavaScriptによってレンダリングされる場合にのみ、PuppeteerまたはPlaywrightに切り替えてください。ヘッドレスブラウザははるかに重いためです。

Node.jsでスクレイピングしたデータをCSVにエクスポートするにはどうすればよいですか?

レコードをオブジェクトの配列として収集し、各オブジェクトの値を結合し、引用符とカンマをエスケープしてカンマ区切りの行を構築します。組み込みのfsモジュールを使って結果をファイルに書き込むか、csv-stringifyのようなライブラリを使用してください。

JavaScriptでスクレイピングするとブロックされるのはなぜですか?

よくある原因は、1つのIPからの過剰なリクエスト、欠落した、または疑わしいUser-Agent、リクエスト間の遅延がないことです。ローテーションプロキシ、現実的なヘッダー、バックオフ付きの再試行、レート制限がブロックを減らします。

JavaScriptスクレイピングに最適なプロキシタイプは何ですか?

データセンタープロキシは寛容な対象に対して高速で安価ですが、住宅用およびモバイルプロキシは厳格なサイトでより実際の利用者からのアクセスに近く見えます。ローテーションセッションは、多くのアドレスにリクエストを分散させるのに役立ちます。

DataImpulseが適切な選択でないのはどのような場合ですか?

静的なISPプロキシ、完全マネージドのスクレイピングAPI、または銀行や政府のサイトへのアクセスが必要な場合、DataImpulseは適切なツールではありません。公開データの収集とコンテンツへのアクセスのための、ローテーション型の住宅用、モバイル、データセンタープロキシに重点を置いています。

信頼できるプロキシでスクレイピングを始めましょう

JavaScriptのスクレイピングツールを、繰り返されるブロックに悩まされず運用する準備はできていますか?DataImpulseは、1GBあたり1ドルから、トラフィックの有効期限なし、サブスクリプション不要で、倫理的に調達された住宅用、モバイル、データセンターIPを提供します。アカウントを作成し、今日からプロキシ経由で最初のスクレイパーをルーティングしましょう。


Share article: