PHPウェブスクレイピング:実践ガイド
Expert in Web Scraping Technologies
TL;DR:
- PHPは基本的なスクレイパーに必要なすべてを提供します。
ext-curlでデータを取得し、DOMDocumentとDOMXPathで解析します。Composerパッケージは関与しません。動作する抽出は約15行です。 - デフォルトの
php-cliインストールは不十分です。 クリーンなUbuntuボックスのphp-cliはjsonとlibxmlのみを公開しており、curl、dom、mbstringはそれぞれ個別にインストールする必要がありました。コードを書く前に確認してください、後ではありません。 DOMDocument::loadHTML()は出力を警告で満たします。 実ページはHTML5であり、パーサーはHTML4を期待しています。libxml_use_internal_errors(true)でロードをラップしないと、ノイズは何も失敗していないときの失敗のように聞こえます。- Scrapeless Universal Scraping APIはJSONエンベロープを返します。 マークアップは
dataにあり、最初にデコードし、次に解析します。 chrome-phpはリモートブラウザを操作しますが、そのデフォルトのタイムアウトはローカルのものに合わせたサイズです。 クラウドCDPエンドポイントに対して測定すると、ライブラリの5秒のデフォルトは6回中2回成功しました;sendSyncDefaultTimeoutを上げると6回中5回になりました。- 無料で始める: Scrapelessダッシュボード は、以下のすべての例で機能するキーを発行します。
必要なもの
ここにあるすべての例は、PHP 8.3.6 (cli) で quotes.toscrape.com に対して実行されました。このサイトは特にスクレイピングの練習のために公開されています。
クリーンなマシンでの最初の驚きは、基本的なPHPインストールに含まれるものがいかに少ないかです。新たにインストールされた php-cli は、ここで重要なセットから json と libxml だけを報告しました。本当に必要な3つのものは別々に届きます:
bash
# Ubuntu/Debian — php-cli alone is not enough
apt-get install -y php-cli php-curl php-xml php-mbstring
php -m | grep -E '^(curl|dom|libxml|mbstring)$'
# curl
# dom
# libxml
# mbstring
php-xml は DOMDocument を提供するパッケージです。拡張は dom と呼ばれ、xml をグレップしても何も見つからず、人々を迷わせる理由です。
ext-curlを使用したページの取得
file_get_contents() は簡単なケースには機能しますが、ステータスコードもヘッダー制御も、名前に値するタイムアウトもありません。 ext-curl は、実際のすべての基礎です:
php
<?php
$ch = curl_init('https://quotes.toscrape.com/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; php-guide/1.0)',
]);
$html = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
echo "http={$status} bytes=" . strlen($html) . "\n";
それは http=200 bytes=11064 を印刷します。二つのオプションは見た目以上の重要性があります: CURLOPT_RETURNTRANSFER は curl_exec() が本文を返すようにし、 CURLOPT_USERAGENT がないと多くのサイトが素のPHPクライアントに対して異なる、またはまったく応答しません。
DOMDocumentとDOMXPathを使用した解析
PHPのDOM拡張は、W3C DOM標準 の完全な実装であり、DOMXPath は専用のスクレイピングライブラリと同じクエリ機能を提供します。罠はローダーです。
php
<?php
$doc = new DOMDocument();
libxml_use_internal_errors(true); // without this, every HTML5 tag warns
$doc->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($doc);
$quotes = $xpath->query("//div[@class='quote']");
echo "quotes=" . $quotes->length . "\n";
$first = $quotes->item(0);
$text = trim($xpath->query(".//span[@class='text']", $first)->item(0)->textContent);
$author = trim($xpath->query(".//small[@class='author']", $first)->item(0)->textContent);
echo "first_author={$author}\n";
echo "first_text=" . mb_substr($text, 0, 40) . "\n";
出力:
text
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr
三つのことが引き出される価値があります。
libxml_use_internal_errors(true) は実際にはオプションではありません。 DOMDocument はHTML4解析を実装しているため、最新のページでHTML5要素や引用符のない属性はすべて警告を引き起こします。呼び出しをスキップすると、成功したスクレイピングはパーサーのノイズの下にその出力を埋めてしまいます — PHPマニュアルはこのスイッチを文書化しています、制御を取得するためのサポートされた方法として。
DOMXPath::query() に渡す第二引数はクエリをノードにスコープします。これがないと、.//span[@class='text'] は文書全体を検索し、各行に対して最初の引用のテキストを取得します。その単一の引数は、行ごとの抽出と微妙に間違ったデータセットの違いです。
mb_substr() よりも substr() が重要です、なぜならページがカール引用符を使用しているからです。 substr() はバイトでカットし、マルチバイトキャラクターを無効なUTF-8に分割します。これは、データベースで三ステップ後に現れる腐敗の種類です。
プレーンPHPの限界
上記のスクリプトは、ターゲットがサーバーサイドでそのコンテンツをレンダリングし、誰が尋ねているのかは気にしないため機能します。それを終わらせるには二つのことがあります:JavaScriptが実行された後にのみ存在するコンテンツと、素のHTTPクライアントがブラウザでないと判断するサイトです。どちらもPHPの問題ではありません — どのHTTPクライアントも言語に関係なく、どちらも解決しません。
プロキシプールを介してリクエストをルーティングすることで、二番目のケースの一部を処理できます。フレームワーク内で作業している場合は、Laravelプロキシ統合ガイド が、この記事よりもその構成を詳しくカバーしています。
その時点で二つのエスカレーションがありますが、これらはお互いのより良いバージョンではなく異なるツールです。機能するところではプレーンHTTPパスを維持してください; それは広範にわたって最も速く最も安価なオプションです。
エスカレーション1: ユニバーサルスクレイピングAPI
最初のエスカレーションは、コードをHTTPクライアントの形に保ちながら、難しい部分をサーバー側に移動させます。リクエストは普通の ext-curl であり、レスポンスが違いを示します。
php
<?php
$key = getenv('SCRAPELESS_API_KEY');
$payload = json_encode([
'actor' => 'unlocker.webunlocker',
'input' => ['url' => 'https://quotes.toscrape.com/', 'js_render' => false],
]);
$ch = curl_init('https://api.scrapeless.com/api/v2/unlocker/request');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-api-token: {$key}"],
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$body = json_decode($raw, true);
echo "http={$status} envelope_keys=" . implode(',', array_keys($body)) . "\n";
$apiHtml = $body['data']; // the markup lives here, not in $raw
echo "bytes=" . strlen($apiHtml) . "\n";
text
http=200 envelope_keys=code,data
bytes=11064
エンベロープは内面化すべきものです。$rawはJSONであり、DOMDocumentに渡すと、一致するノードがなくエラーもないドキュメントが生成されます — セレクタは単にゼロ行を返すだけです。デコードし、dataを取り、その後解析します。そのアンラップを1つのヘルパーに保ち、json_decode(...)['data']を呼び出し元で繰り返さないようにします。
バイト数は別として、解析コードは変更されていません。同じDOMXPathブロックに$apiHtmlをフィードすると、同じ10の引用が返されます。これは重要な点です:取得方法だけが変わります。
エスカレーション2:PHPからの本物のブラウザ
コンテンツが本当にJavaScriptを必要とする場合、ブラウザが必要です。 chrome-php/chromeはChrome DevToolsプロトコルを使用し、ローカルのChromeを起動するか、WebSocket経由でリモートのChromeに接続することができます。
bash
composer require chrome-php/chrome
# Using version ^1.16 for chrome-php/chrome → v1.16.1
php
<?php
require __DIR__ . '/vendor/autoload.php';
use HeadlessChromium\BrowserFactory;
$key = getenv('SCRAPELESS_API_KEY');
$uri = "wss://browser.scrapeless.com/api/v2/browser?token={$key}";
$browser = BrowserFactory::connectToBrowser($uri, [
'sendSyncDefaultTimeout' => 60000, // see below — the default is 5000
]);
$page = $browser->createPage();
$page->navigate('https://quotes.toscrape.com/')->waitForNavigation();
echo "title: " . $page->evaluate('document.title')->getReturnValue() . "\n";
echo "quotes on page: " . $page->evaluate('document.querySelectorAll(".quote").length')->getReturnValue() . "\n";
$browser->close();
出力:
text
title: Quotes to Scrape
quotes on page: 10
デフォルトのタイムアウトはローカルブラウザに合わせたサイズ
そのsendSyncDefaultTimeout行はこの記事において価値のある部分です。chrome-phpはそれを5000 msにデフォルト設定し、同じマシンで動作するChromeには寛大ですが、TLS接続のものにはわずかです。6回の実行のマッチした2つのセット、同じスクリプト、同じターゲット、変更されたのはそのオプションだけです:
| 設定 | 結果 | 失敗モード |
|---|---|---|
| ライブラリデフォルト (5000 ms) | 2成功, 4失敗 | すべての失敗 OperationTimedOut: Operation timed out after 5s |
sendSyncDefaultTimeout => 60000 |
5成功, 1失敗 | 単一の失敗は接続時の別のエラーです |
2つの結論が続き、2つ目は人々が見落としがちなものです。
タイムアウトを上げることは必要です。デフォルトのままにすると、大多数の試行が同じメッセージで死亡し、それは誤解を招くメッセージです — タイムアウトを指名しているため、ページやネットワークが遅いように読まれますが、実際に期限切れになったのはライブラリ自身のプロトコルラウンドトリップの待機です。
上げることも十分ではありません。生き残った唯一の失敗はCannot connect to the browser, make sure it was not closedであり、接続がまだ確立されている間に約5秒後に発生しました。より大きなコマンドタイムアウトはそれには影響を与えません。ブラウザの取得を、その設計の中での独自の失敗可能なステップと考え、実際に持ってからの作業とは区別し、$browser->close()をfinallyに保持して、中途の失敗でセッションが孤立しないようにします。
誤っていた1つの診断
これらのタイムアウトに関する最初の理論は、URIのクエリ文字列がWebSocketハンドシェイクに到達せず、?token=を持っていくというものでした。これには実際の証拠があります:Protocol::validateSocketUri()は[$scheme, $host, $port]のみを返し、パスとクエリを完全に破棄します。
しかし、これは依然として間違っています。ハンドシェイクは他の場所で構築され、Protocol::getRequestHandshake()が呼び出し、5つの要素を返す別のvalidateUri()を呼び出し、リクエストラインの前で明示的にクエリを再追加します。トークンは到着します。失敗はレイテンシであり、同じURIを異なる深さで解析する2つの関数は、バグのように見える偶然です。
複数のURIパーサーを持つライブラリでは、データを破棄するものを見つけることが、あなたが気にかけているパスの上のものであることを意味するわけではないという有用なリマインダーです。
3つの間での選択
| アプローチ | 使用するシーン | コスト |
|---|---|---|
ext-curl + DOMXPath |
サーバーレンダリングされたHTML、寛容なターゲット | 最も低い; 依存関係なし |
| Universal Scraping API | ブロックされたり挑戦を受けている、JSは不要 | 1回のHTTP呼び出し、アンラップすべきエンベロープ |
chrome-php + スクレイピングブラウザ |
コンテンツがJavaScriptの実行を必要とする | 最も高い; 作業ごとにブラウザセッション |
リストを上からではなく、下へ作業してください。ブラウザが必要そうに見えるほとんどのページは、データを<script>タグに埋め込んでいることが多く、DOMXPathクエリとjson_decode()はすべての軸でブラウザセッションを上回ります。
結論
PHPは十分に合理的なスクレイピング言語であり、人々が欠けていると期待する部分は標準ライブラリにあります。ext-curlとDOMXPathはサーバーレンダリングされたページを完全にカバーし、libxml_use_internal_errors(true)とquery()のスコープ付き第二引数が、動作するコードと静かに間違っているコードを分ける2つの詳細です。
ターゲットが協力を停止した場合は、意図的にエスカレーションを行ってください。APIパスはあなたのコードをHTTPクライアントのままにし、ただエンベロープをアンラップするように求めます。ブラウザパスはセッションを要し、そのブラウザがリモートである場合、特定の1つの設定が必要です:chrome-phpの5秒のデフォルトはローカルChromeの仮定であり、ここではそれをそのままにしておくと、6つの接続のうち4つが失われた原因となりました。
PHPからスクレイピングの準備はできましたか?
Scrapelessダッシュボードにキーを作成し、すでに収集しているページに対してext-curlの例を実行します。期待通りの結果が得られれば、完了です — 依存関係もブラウザも必要ありません。Universal Scraping APIは、期待通りに動作しないページ用に用意されており、現在の料金は料金ページで確認できます。
FAQ
Q: PHPでスクレイピングをするのにComposerは必要ですか?
いいえ。ext-curlを使った取得とDOMDocumentおよびDOMXPathを用いた解析は、標準インストールにある拡張機能以上のものは必要ありません。Composerはchrome-php/chromeのようなブラウザドライバ用にのみ必要です。
Q: なぜDOMDocumentはすべてのページで警告を表示するのですか?
それはHTML4の解析を実装しており、現代のページはHTML5だからです。警告は失敗ではなく情報としてのものです。libxml_use_internal_errors(true)をloadHTML()の前に呼び出し、libxml_clear_errors()を後に呼び出し、実際に表示したい場合はlibxml_get_errors()を検査してください。
Q: APIリクエストが成功したのに、なぜ解析が何も返さないのですか?
ほぼ確実にエンベロープを解析しています。Universal Scraping APIはdataの中にマークアップを含むJSONを返すため、DOMDocumentはJSON文字列を受け取り、エラーを発生させずに一致するノードを見つけることができません。レスポンスをデコードし、dataを解析してください。
Q: クラス属性にはどのXPathを使えばよいですか?
//div[@class='quote']は正確な属性値のみと一致します。複数のクラスを持つ要素の場合は、//div[contains(concat(' ', normalize-space(@class), ' '), ' quote ')]を使用してください。これにより、裸のcontains()が行うようなquote-footerとの一致を避けることができます。
Q: 大規模なスクレイピングプロジェクトにPHPを使用すべきですか?
スケールでの取得と解析作業には、はい — curl_multi_*は実際の並行性を提供し、DOM拡張は高速です。PHPが弱い点は、PlaywrightやPuppeteer周辺のツールがより成熟している長期のブラウザ自動化です。一般的な分割は、HTTPパスにはPHPを使用し、実際にブラウザを必要とするページにはブラウザサービスを使用することです。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



