25.

PHP Web スクレイピング完全ガイド — cURL/DOMDocument/Goutte/Headless Chrome

編集
この記事の要点
  • PHP でのスクレイピングは cURL でリクエスト → DOMDocument / Symfony DomCrawler でパースが基本
  • 静的 HTMLfile_get_contents() + DOMDocument で十分。 動的 (JS 生成) は Headless Chrome 連携が必要
  • Goutte / Symfony BrowserKit は cURL + DomCrawler をラップした高レベル API(現在は symfony/browser-kit + symfony/http-client
  • 必ず守ること: robots.txt 遵守、 利用規約確認、 レート制限 (sleep())、 User-Agent 明示
  • Python との比較: BeautifulSoup / Scrapy が成熟。 PHP は CMS と統合しやすいのが強み

方式一覧

方式用途JS 実行難易度
file_get_contents()シンプルな GET不可
cURL本格的 HTTP(Cookie / 認証)不可
Guzzleモダンな PHP HTTP クライアント不可
DOMDocument + XPathHTML パース(標準同梱)-
Symfony DomCrawlerCSS セレクタ対応-
Goutte / symfony/browser-kitcURL + DomCrawler 高レベル不可
php-webdriver (Selenium)ブラウザ自動操作
chrome-php/chromeHeadless Chrome 直接制御
Puppeteer (Node.js)JS 製・PHP から exec

1. file_get_contents + DOMDocument

loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();

// XPath で抽出
$xpath = new DOMXPath($dom);
$titles = $xpath->query('//h2[@class="article-title"]/a');

foreach ($titles as $node) {
    echo $node->textContent . " => " . $node->getAttribute('href') . "\n";
}

// 全 img タグの src
$imgs = $xpath->query('//img');
foreach ($imgs as $img) {
    echo $img->getAttribute('src') . "\n";
}

2. cURL(Cookie・認証・User-Agent)

 $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; MyBot/1.0; +https://example.com/bot)',
        CURLOPT_COOKIEJAR      => '/tmp/cookies.txt',
        CURLOPT_COOKIEFILE     => '/tmp/cookies.txt',
        CURLOPT_HTTPHEADER     => array_merge([
            'Accept: text/html,application/xhtml+xml',
            'Accept-Language: ja,en;q=0.9',
        ], $headers),
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_ENCODING       => '',           // gzip 等自動展開
    ]);

    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    if ($body === false) throw new RuntimeException("cURL: $err");
    if ($code >= 400)    throw new RuntimeException("HTTP $code");
    return $body;
}

// 使用
$html = fetch('https://example.com/login');

3. Guzzle(モダン HTTP クライアント)

 $jar,
    'timeout' => 30,
    'headers' => [
        'User-Agent' => 'MyBot/1.0',
    ],
]);

// GET
$response = $client->get('https://example.com/');
$html = (string) $response->getBody();

// POST フォーム
$response = $client->post('https://example.com/login', [
    'form_params' => [
        'username' => 'taro',
        'password' => 'secret',
    ],
]);

// JSON API
$response = $client->get('https://api.example.com/items', [
    'headers' => ['Authorization' => 'Bearer xxx'],
]);
$data = json_decode((string) $response->getBody(), true);

4. Symfony DomCrawler (CSS セレクタ)

filter('h2.article-title a')->each(function (Crawler $node) {
    echo $node->text() . " => " . $node->attr('href') . "\n";
});

// XPath
$crawler->filterXPath('//meta[@property="og:image"]')->each(function ($node) {
    echo $node->attr('content') . "\n";
});

// 属性・テキスト
$title = $crawler->filter('title')->text();
$canonical = $crawler->filter('link[rel="canonical"]')->attr('href');

// 子要素
$crawler->filter('.product')->each(function (Crawler $product) {
    $name  = $product->filter('.name')->text();
    $price = $product->filter('.price')->text();
    echo "$name : $price\n";
});

5. Goutte / BrowserKit(高レベル API)

request('GET', 'https://example.com/login');

// フォーム
$form = $crawler->selectButton('ログイン')->form([
    'username' => 'taro',
    'password' => 'secret',
]);
$crawler = $browser->submit($form);

// リンククリック
$link = $crawler->selectLink('マイページ')->link();
$crawler = $browser->click($link);

// 結果のパース
$crawler->filter('.user-info')->each(function ($node) {
    echo $node->text() . "\n";
});

6. 動的サイト: Headless Chrome

JavaScript で動的にレンダリングされる SPA や React/Vue サイトには Headless Chrome が必要です:

createBrowser([
    'headless' => true,
    'noSandbox' => true,
    'windowSize' => [1920, 1080],
]);

try {
    $page = $browser->createPage();
    $page->navigate('https://spa.example.com')->waitForNavigation();

    // JS 実行完了を待つ
    $page->waitUntilContainsElement('.product-list');

    // HTML 取得
    $html = $page->getHtml();

    // JS 実行で値取得
    $title = $page->evaluate('document.title')->getReturnValue();

    // スクリーンショット
    $page->screenshot()->saveToFile('screenshot.png');
} finally {
    $browser->close();
}

7. Puppeteer 連携(Node.js 経由)

// scraper.js (Node.js)
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: 'new' });
  const page = await browser.newPage();
  await page.goto(process.argv[2], { waitUntil: 'networkidle0' });

  const data = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.product')).map(el => ({
      name:  el.querySelector('.name').innerText,
      price: el.querySelector('.price').innerText,
    }));
  });

  console.log(JSON.stringify(data));
  await browser.close();
})();

8. robots.txt を尊重する

9. レート制限とマナー

  • sleep を入れる: 連続リクエストは最低 1〜2 秒待機
  • User-Agent を明示: 連絡先 URL も含める(ブロック回避)
  • Retry-After ヘッダ尊重: 429 / 503 のときは指定秒待つ
  • 並列度を抑える: 同時 1〜3 接続まで
  • キャッシュ活用: 同じ URL を何度も取らない
  • 利用規約確認: スクレイピング禁止サイトでは API を探す
minInterval = $minIntervalSec;
    }

    public function wait(): void {
        if ($this->lastRequest !== null) {
            $elapsed = microtime(true) - $this->lastRequest;
            if ($elapsed < $this->minInterval) {
                usleep((int)(($this->minInterval - $elapsed) * 1_000_000));
            }
        }
        $this->lastRequest = microtime(true);
    }
}

$rl = new RateLimiter(2.0);
foreach ($urls as $url) {
    $rl->wait();
    $html = fetch($url);
    // ...
}

PHP vs Python スクレイピング比較

項目PHPPython
HTML パースDOMDocument / DomCrawlerBeautifulSoup / lxml
HTTP クライアントGuzzle / cURLrequests / httpx
クローラフレームワークGoutte (基本機能のみ)Scrapy (フル機能)
JS レンダchrome-php / Puppeteer 連携Selenium / Playwright
並列処理Guzzle Poolasyncio / aiohttp
エコシステムCMS 統合に強いデータ分析と連携

FAQ

Q: 文字化けする
A: mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') を DOMDocument に渡す前にかける。 元 HTML が Shift_JIS なら mb_convert_encoding($html, 'UTF-8', 'SJIS-win')

Q: 403 / 429 で弾かれる
A: User-Agent を本物のブラウザ風に、 Referer 設定、 Cookie 維持、 リクエスト間隔を空ける。 それでもダメなら API 利用を検討。

Q: 利用規約で禁止されているサイトをスクレイピングしてもよい?
A: 法的・倫理的に問題があります。 公式 API、 RSS、 データ提供サービスの利用を最優先に検討してください。

編集
Post Share
子ページ
  1. phpQueryの導入と使い方
同階層のページ
  1. インストール方法
  2. 文法
  3. Composerのインストール
  4. 内部関数
  5. フレームワーク
  6. エラー一覧
  7. 改行出力
  8. printとechoの違い
  9. シングルクォートとダブルクォートの違い
  10. returnとyieldの違い
  11. var_dumpをログ出力
  12. CSV読み込み
  13. 待機・処理の遅延
  14. ログファイルにエラーを出力する方法
  15. エラーログ出力関数
  16. URLパラメータの配列化
  17. empty, is_null. issetの判定比較表
  18. httpステータスコードの付与
  19. バージョンの確認
  20. php.ini
  21. APIを呼び出す方法
  22. 外部ファイルを呼び出す方法
  23. カンマ区切りの文字列を配列に変換
  24. 配列からランダムに値を取り出す方法
  25. Webスクレイピング