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

<?php
// 静的 HTML 取得
$url = 'https://example.com/news';
$html = file_get_contents($url);

// DOMDocument でパース (HTML5 のパースエラーは抑制)
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->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)

<?php
function fetch(string $url, array $headers = []): string {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => $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 クライアント)

<?php
require 'vendor/autoload.php';
// composer require guzzlehttp/guzzle

use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;

$jar = new CookieJar();
$client = new Client([
    'cookies' => $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 セレクタ)

<?php
require 'vendor/autoload.php';
// composer require symfony/dom-crawler symfony/css-selector

use Symfony\Component\DomCrawler\Crawler;

$html = file_get_contents('https://example.com');
$crawler = new Crawler($html);

// CSS セレクタ
$crawler->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)

<?php
require 'vendor/autoload.php';
// composer require symfony/browser-kit symfony/http-client

use Symfony\Component\BrowserKit\HttpBrowser;
use Symfony\Component\HttpClient\HttpClient;

$browser = new HttpBrowser(HttpClient::create());

// クリック・フォーム送信が直感的
$crawler = $browser->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 が必要です:

<?php
require 'vendor/autoload.php';
// composer require chrome-php/chrome

use HeadlessChromium\BrowserFactory;

$browserFactory = new BrowserFactory('/usr/bin/google-chrome');
$browser = $browserFactory->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();
})();
<?php
// PHP から実行
$url = 'https://spa.example.com';
$json = shell_exec('node scraper.js ' . escapeshellarg($url));
$products = json_decode($json, true);

foreach ($products as $p) {
    echo "{$p['name']} : {$p['price']}\n";
}

8. robots.txt を尊重する

<?php
function isAllowed(string $url, string $userAgent = '*'): bool {
    $parts = parse_url($url);
    $robotsUrl = "{$parts['scheme']}://{$parts['host']}/robots.txt";

    $robots = @file_get_contents($robotsUrl);
    if ($robots === false) return true;          // robots.txt 無ければ許可

    $path = $parts['path'] ?? '/';
    $disallowed = [];
    $currentAgent = '*';
    $matchAgent = false;

    foreach (preg_split('/\r?\n/', $robots) as $line) {
        $line = trim($line);
        if ($line === '' || str_starts_with($line, '#')) continue;
        if (preg_match('/^User-agent:\s*(.*)$/i', $line, $m)) {
            $currentAgent = strtolower(trim($m[1]));
            $matchAgent = ($currentAgent === '*' || $currentAgent === strtolower($userAgent));
        } elseif ($matchAgent && preg_match('/^Disallow:\s*(.*)$/i', $line, $m)) {
            $rule = trim($m[1]);
            if ($rule !== '' && str_starts_with($path, $rule)) {
                return false;
            }
        }
    }
    return true;
}

if (!isAllowed('https://example.com/private/')) {
    die("robots.txt で禁止されています");
}

9. レート制限とマナー

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

    public function __construct(float $minIntervalSec = 1.5) {
        $this->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スクレイピング

最近更新/作成されたページ