PHPのスクレイピングライブラリ「PHP Simple HTML DOM Parser」の使い方
2020/02/08
PHPのスクレイピングライブラリ「PHP Simple HTML DOM Parser」の使い方
PHP Simple HTML DOM Parserに関連する解説、および、関連記事
PHPでスクレイピングをする際には、「PHP Simple HTML DOM Parser」というライブラリを利用すると簡単にスクレイピングをすることが出来ます。
この記事では「PHP Simple HTML DOM Parser」の基本的な使い方を始め、実用的な使い方、エラーの原因になるポイントなどを解説していきます。
また、「PHP スクレイピング」で検索すると「phpQuery」というライブラリの記事が大量にヒットするわけです。
しかし、「phpQuery」より「PHP Simple HTML DOM Parser」をおすすめする理由や、「PHP Simple HTML DOM Parser」の基本的な使い方についての解説を下記の記事に書いていますので、こちらも併せて読んでいただく方がいいかと思います。
PHPでスクレイピング。phpQueryとphp-simple-html-dom-parserの比較と設置方法
「PHP Simple HTML DOM Parser」を CakePHP3で使う場合の導入方法については下記に記事を書きました。
CakePHP3でPHP Simple HTML DOM Parserを使ってスクレイピングする方法
また、スクレイピングを行う対象のページへアクセスするときは、「file_get_contents」や「cURL」を利用します。
ですが、断然「cURL」の方をオススメする!という理由や、cURLで SSL化されたページへのアクセス、User Agentを必要とするサイトへのアクセス方法などについて、下記の記事に書いていますので、こちらも併せて読んでいただく方がいいかと思います。
PHPのcURLでAPIやWebサイトへのアクセス方法。file_get_contentsとの比較
また、この記事は、下記のオフィシャルマニュアルを参考にしています。
https://simplehtmldom.sourceforge.io/manual.htm
DOMオブジェクトを生成する方法
「PHP Simple HTML DOM Parser」では、まず最初に HTMLから DOMオブジェクトを生成します。
そして、作成した DOMオブジェクトに対して「find()」メソッドを使ってほしい項目を取得する、という流れになります。
DOMオブジェクトを生成する基本形
その最初の DOMオブジェクトを作成する方法は下記の 3種類あります。
1 2 3 4 5 6 7 8 |
// HTMLのソースコードを指定する場合 $html = str_get_html("<html><body>Hello!</body></html>"); // URLを指定する場合 $html = file_get_html("http://example.com"); // HTMLファイルを指定する場合 $html = file_get_html("example.html"); |
DOMオブジェクトを生成するサンプルソース
URLを指定して Webサイトにアクセスし DOMオブジェクトを作成する場合は、「file_get_html()」が用意されていますが、私は cURLを使い別に HTMLソースコードを取得した上で、「str_get_html()」を使って DOMオブジェクトを生成する方法をオススメします。
なぜか。
指定の URLから情報が取得できなかった場合にエラー制御をするためには cURLの方がやりやすいからです。
詳しくは、「PHPのcURLでAPIやWebサイトへのアクセス方法。file_get_contentsとの比較」か「PHPでスクレイピング。phpQueryとphp-simple-html-dom-parserの比較と設置方法」を参考にしてください。
cURLを利用した場合の DOMオブジェクトの生成するサンプルソースは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php // UserAgent define("USER_AGENT_TEXT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"); // PHP Simple HTML DOM Parser の読み込み require_once "simple_html_dom.php"; $url = "https://www.yahoo.co.jp/"; // cURLを使用して Webサイトから情報を取得する関数 $htmlSource = getApiDataCurl($url,"html"); // HTMLをオブジェクト化 $html = str_get_html( $htmlSource ); // 結果を出力 print_r($html->find("a",0)->plaintext); // APIを呼び出し、結果を受け取る処理(URLにアクセスしその結果を取得する処理) // $url :APIの URI(アクセスする URL) // $responseType:受け取る結果のタイプ(header、html、json) function getApiDataCurl($url, $responseType = "html" ){ : 中略 : } |
関数「getApiDataCurl()」の中身については、「PHPのcURLでAPIやWebサイトへのアクセス方法。file_get_contentsとの比較」か「PHPでスクレイピング。phpQueryとphp-simple-html-dom-parserの比較と設置方法」を参考にしてください。
DOMオブジェクトから find()メソッドを使ってノード(項目)を取得する方法
DOMオブジェクトから find()メソッドを使ってノード(項目)を取得する処理の基本系
DOMオブジェクトからノード(項目)を取得するときは「find()」メソッドを使います。
その使い方の基本形は下記のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// すべての「a」タグの要素を配列として取得 $result = $html->find("a"); // 「a」タグの要素のうち 1番目(0)の項目を取得 $result = $html->find("a",0); // 「a」タグの要素のうち最後(-1)の項目を取得 $result = $html->find("a",-1); // id属性を持っているすべての要素を配列として取得 $result = $html->find("[id]"); // 上記は下記のように「*」を使うことも可能 $result = $html->find("*[id]"); // 「div」タグの要素のうち id属性を持っている要素を配列として取得 $result = $html->find("div[id]"); // 「div」タグの要素のうち「id=foo」である要素を配列として取得 $result = $html->find("div[id=foo]"); // 「div」タグの要素のうち id属性を持っている要素の 6番目を取得 $result = $html->find("div[id]",5); |
要素の条件を指定するフィルタ
「$html->find("div[id=foo]");
」の「[id=foo]
」で指定している箇所はフィルタとなっておりまして、「=」以外の条件を指定することも出来ます。
指定方法とその条件は、以下のようになります。
[attribute]
指定された属性を持つ要素に一致するものを取得
[!attribute]
指定された属性を持つ要素に一致しないものを取得
[attribute=value]
指定された値である属性を持つ要素を取得
[attribute!=value]
指定された値ではない属性を持つ要素を取得
[attribute^=value]
指定された値から始まる属性を持つ要素を取得
[attribute$=value]
指定された値で終わる属性を持つ要素を取得
[attribute*=value]
指定された値を含む属性を持つ要素を取得
CSSの IDセレクタ、CLASSセレクタを指定する方法
CSSの IDセレクタや CLASSセレクタを条件として指定する方法もあります。
1 2 3 4 5 6 7 8 9 10 11 |
// 全てのタグのうち「id=foo」である要素を配列として取得 $result = $html->find("#foo"); // 下記の記述方法と同じ $result = $html->find("[id=foo]"); // 全てのタグのうち「class=foo」である要素を配列として取得 $result = $html->find(".foo"); // 下記の記述方法と同じ $result = $html->find("[class=foo]"); |
「,(カンマ)」で区切りることで、複数条件の指定ができるようになります。
1 2 3 4 5 |
// すべての「a」「img」タグの要素を配列として取得 $ret = $html->find("a, img"); // 「a」「img」タグの要素のうち title属性を持っている要素を配列として取得 $ret = $html->find("a[title], img[title]"); |
テキストブロックとコメントの取得
テキストブロックやコメントを取得するときは下記のように指定します。
1 2 3 4 5 6 7 8 |
// すべてのテキストブロックを取得 $result = $html->find("text"); // すべてのテキストを取得 $result = $html->plaintext; // すべてのコメントを取得 $result = $html->find("comment"); |
「$html->plaintext
」は、全てのタグの plaintextを取得するものです。
「$html->find("text")
」とは違う結果になります。
子孫セレクタやネストされたタグの取得
スペースで区切ることで子孫セレクタ、ネストされた要素を取得することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 |
// ulタグの中にある liタグの要素を取得 $result = $html->find("ul li"); // ネストされた divの要素を取得 $result = $html->find("div div div"); // 「class=hello」の tableタグの中にある tdタグの要素を取得 $result = $html->find("table.hello td"); // tableタグの中にある「align=center」の属性を持つ tdタグの要素を取得 $result = $html->find("table td[align=center]"); |
ネストされた要素は下記のように foreach()で取得することも可能です。
1 2 3 4 5 6 7 8 9 10 |
// ulタグの中にある liタグの要素を取得 foreach($html->find("ul") as $ul){ foreach($ul->find("li") as $li){ $result[] = $li->plaintext; } } // ネストされた要素の順番を指定して取得 // ulタグの最初の要素をの中にある、最初の liタグの要素を取得 $result = $html->find("ul", 0)->find("li", 0); |
取得した要素の属性にアクセスする方法
前項では、DOMオブジェクトから条件に従って要素を取得する方法を解説しました。
この後では、取得した要素の中から指定した属性の値を取得する方法を解説します。
要素の属性を指定して値を取得する方法の基本形
要素の属性を指定して値を取得する基本形は以下になります。
1 2 3 4 5 6 7 8 9 10 11 |
// 「a」タグの hrefの値を取得 echo $html->find("a",0)->href; // 「a」タグの classの値を取得 echo $html->find("a",0)->class; // 「a」タグの idの値を取得 echo $html->find("a",0)->id; // 「a」タグのタグの名称を取得 echo $html->find("a",0)->tag; |
要素のテキストの情報を取得する方法
指定した要素のテキスト情報を取得する方法が 3種類用意されています。
テキスト情報のみを取得するのか、タグも含めて取得するのか、などを選択することができます。
1 2 3 4 5 6 7 8 |
// 指定した要素のテキストのみを取得する echo $html->find("div",0)->plaintext; // 指定した要素に含まれる値を取得する echo $html->find("div",0)->innertext; // 指定した要素をタグを含めて取得する echo $html->find("div",0)->outertext; |
「plaintext」「innertext」「outertext」を指定した際の具体的な結果は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 |
// 下記の HTMLを対象に実行 $html = str_get_html( '<html><body><div><a href="https://example.com">リンクの例</a></div></body></html>' ); echo $html->find("div",0)->plaintext; // 取得の値:リンクの例 echo $html->find("div",0)->innertext; // 取得の値:<a href="https://example.com">リンクの例</a> echo $html->find("div",0)->outertext; // 取得の値:<div><a href="https://example.com">リンクの例</a></div> |
ちなみに、「$html->find("a",0)->plaintext;
」と「$html->find("a",0)->innertext;
」とした場合は、両方とも同じ「リンクの例」となります。
これは、「a」タグの中には「リンクの例」しかないためです。
属性を設定、削除する方法、属性の存在の有無チェックの方法
属性から値を取得するだけではなく、属性に値を設定したり、削除したりすることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 属性の値を取得(属性が非値ならば、論理値が返す) $value = $html->find("a",0)->href; // 属性の値を設定(属性が非値ならば、論理値を設定) $html->find("a",0)->href = "http://example.com"; // 属性の値を削除(値を NULLとする) $html->find("a",0)->href = null; // 属性が存在するか否かをチェック if( isset( $e->href ) ){ echo "href exist!"; } |
DOM拡張モジュールに定義された関数を使用して要素を取得することも可能
DOMオブジェクトから要素を取得する方法としては、「find()」メソッドを利用する方法を紹介しました。
ですが、PHP Simple HTML DOM Parserでは、DOM拡張モジュールに定義された関数を使用して要素を取得することもできます。
例えば、下記のように「getElementById()」で「id」の値を指定して要素を取得することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 |
$url = "https://www.yahoo.co.jp"; $htmlSource = getApiDataCurl($url,"html"); $html = str_get_html( $htmlSource ); // 「id=eyebrow」の要素を取得 print_r($html->getElementById("eyebrow")->outertext); // 「id=eyebrow」の要素を取得の 1つめの子要素を取得 print_r($html->getElementById("eyebrow")->childNodes(0)->outertext); // 「id=eyebrow」の要素を取得の 1つめの子要素の class名を取得 print_r($html->getElementById("eyebrow")->childNodes(0)->getAttribute("class")); |
DOM拡張モジュールに定義された関数は多数ありますので、下記の PHPの関数レファレンスを参考にしてください。
https://www.php.net/manual/ja/book.dom.php
取得した HTMLを保存する方法
取得した HTMLファイルを簡単にファイルとして出力(保存)する方法も用意されています。
1 2 3 4 5 6 7 8 |
// HTML全体を保存 $html->save("result.html"); // 指定した要素を保存 $html->find("a",0)->save("result3.html"); // 下記はエラーになる $html->find("a",0)->outertext->save("result3.html"); |
解説はしましたが、果たしてこの機能を使う機会はあるのだろうか?という疑問はありますね。
PHP Simple HTML DOM Parserを使うときの注意点
ここ以降は、マニュアルには載っていない内容ですが、実際に使ってみて気づいた点がいくつかありましたので、それをお伝えしたいと思います。
DOMオブジェクトの中身の確認には echo、printを使う
先のサンプルでは、取り出した値を「print_r()」を使って表示しています。
ですが、DOMオブジェクトの中身を確認したいときは、print_r()、var_dump()ではなく、echo、printを使いましょう。
具体的には、下記の記述を実行すると「Fatal error: Allowed memory size of 2097152000 bytes exhausted (tried to allocate 1046482944 bytes) in C:\xampp...
」のエラーがでます。
1 2 3 4 5 |
print_r($html); print_r($html->find("a")); foreach($html->find("a") as $val ){ print_r($val) . "<br>\n"; } |
下記の記述方法であればエラーは発生しません。
1 2 3 4 5 |
echo $html; echo implode("<br>", $html->find("a")); foreach($html->find("a") as $val ){ echo $val . "<br>\n"; } |
詳しいエラーの原因は調べていませんが、すごい勢いで CPUとメモリを消費してオーバーフローをしてしまいますので、ローカル環境以外で「print_r()」を使うのはかなり危険です。
HTMLタグの大文字、小文字は気にしなくて問題なし
HTMLタグは大文字、小文字のどちらで記述しても問題ない仕様になっています。(XHTMLは小文字で記述する仕様になっています。)
そのため、HTMLタグが大文字のサイトがあったり、小文字のサイトがあったりしますが、「$html->find("title",0)->plaintext
」などで指定するタグは、大文字小文字の区別をしません。
(HTMLのソース側も、find()に指定する側も大文字、小文字の区別は行われません。)
そのため、大文字小文字は気にせず記述することが出来ます。
逆に、大文字小文字が混在したサイトで「大文字で書かれた「H2タグ」の 3つめを取得したい」といった指定をすることは出来ません。
PHP Simple HTML DOM Parserで処理できる文字数には上限がある
PHP Simple HTML DOM Parserには処理できる文字数に上限があるようです。
とあるサイトのスクレイピングをしようとアクセスしたところ、「Fatal error: Uncaught Error: Call to a member function find() on bool in ...
」というエラーが発生しました。
エラーの内容を調査すると、「$html->find("h3",0)->plaintext
」を実行している「$html」が空であることが確認できました。
そして、「$html」が空になる理由が、HTMLのソース量が大きすぎて、「str_get_html()」で処理できていないことが分かりました。
そのため「mb_substr()」を使用して、取得した HTMLを文字数で制限して取得してみたところ、520,000文字以下であれば処理できることが確認できました。
(環境によって「520,000文字」が異なるか否かは確認をしていません。)
より具体的には...
520,261文字以上ではエラーがでました。
「mb_substr」で処理をしていますので、バイト数ではなく、全角半角の区別なく文字数です。(バイト数で制限があるか否かは確認していません。)
php.ini の memory_limit を変更しても制限される文字数が変わることはありませんでした。
このエラーの難点は、エラーメッセージが「$html が空です」というものであるため、HTMLのソース量が大きいことが原因で処理できていない、ということになかなか気づかない点が挙げられます。
大量の HTMLソースを持つページに対処するサンプルソース・その1
大量の HTMLソースを持つページの情報を取得する必要がある場合の具体的な対処方法のサンプルソースとしては以下のようになります。
1 2 3 4 5 6 7 8 |
$url = "https://www.yahoo.co.jp/"; $htmlSource = getApiDataCurl($url,"html"); // mb_substr()で 520000文字分を取得 $htmlResult = mb_substr($htmlSource,-520000,520000); $html = str_get_html( $htmlResult ); print_r($html->find("a",0)->plaintext); |
HTMLファイルの最初の方はグローバルメニューや検索条件を入力するエリアで、下の方に検索結果一覧が表示される、というサイトなどを想定しています。
そのため、検索結果が表示される HTMLの下の方だけ取得できればいい、といった場合はこの方法が簡単なのではないか、と思います。
「mb_substr」を用いて、最後から「520,000文字」を取得しています。
サンプルのヤフーのサイトは「520,000文字」もありませんが。
また、関数「getApiDataCurl()」は、オリジナルの関数です。詳細は「PHPのcURLでAPIやWebサイトへのアクセス方法。file_get_contentsとの比較」か「PHPでスクレイピング。phpQueryとphp-simple-html-dom-parserの比較と設置方法」を確認してください。
大量の HTMLソースを持つページに対処するサンプルソース・その2
前項では「mb_substr」を用いてざっくり後半部分!という感じで取得する処理でした。
その2では、例えば「<h2>一覧表示</h2>
」移行の行を対象として取得する、といった具体的な目印がある場合の対応方法です。
下記のように「foreach()」で上から 1行ずつチェックして必要な部分を取得する方法もあるかとも思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$url = "https://www.yahoo.co.jp/"; $htmlSource = getApiDataCurl($url,"html"); // 改行を区切りとして配列を生成 $htmlSourceArray = preg_split('/\r\n|\n|\r/',$htmlSource); $okFlag = "NG"; $htmlResult = ""; foreach($htmlSourceArray as $htmlLine){ if($okFlag == "OK"){ $htmlResult .= $htmlLine; } else { // 「<h2>一覧表示</h2>」がある行以降を取得する if(strpos($htmlLine,"<h2>一覧表示</h2>") !== false){ $htmlResult .= $htmlLine; $okFlag = "OK"; } } } |
長くなりましたが、以上になります。
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
ファイル変更だけ!ECCUBEの本番から開発環境をコピーする手順を解説
ECCUBEを本番から開発環境をコピーする際の手順を解説。PGMメンテに必要な開発環境を構築する手順を解説。ECCUBEの仕組みは簡単なので作業は5分ほど。
-
指定した数で文字列を丸める(n文字目で…にする)関数substr、mb_substr、mb_strimwidthの違い
文字列を指定された数で抜き出すPHPの関数、substr、mb_substr、mb_strimwidthについての解説。似た関数だが引数の指定方法が違ったり、文字数がバイト数か文字数かも違ったり、注意が必要だ。
-
ECCUBEを開発環境から本番ドメインに変更でエラーが・パス変更について
レンタルサーバでサーバ会社から割り当てられたURLで開発し、本番公開時にドメインを当てたらエラーが!そんな場合の対処方法の解説。対処方法は簡単ですが管理画面からは対応不可。
-
ECCUBE2.13.3で商品規格の在庫数が無制限から変更できないバグがある
2.13.3固有のバグである商品規格の在庫数の入力エリアがアクティブにならない不具合を解消する解説です。product_class.tplの2行を修正するだけの簡単対応です。
-
フォルダを指定してファイルのパーミッションを変更するプログラム
フォームからフォルダ、パーミッションを指定しパーミッションを変更するサンプルプログラムの解説です。
-
JSON形式の値を配列形式に変換・PHPでは json_decode()、json_encode()
JSONとは「JavaScript Object Notation」の略でテキストベースのデータフォーマット。JSONの値をPHPで配列に変換するWebツールの紹介とその処理「json_encode()」「json_decode()」関数の解説。
-
include、requireのパス指定をdirname(__FILE__)、__DIR__と書く理由
include、requireのパスの指定を dirname(__FILE__)、__DIR__で記述する理由に付いて解説。相対パス、絶対パスを直書き、パスを書かない場合は何が問題かを説明。
-
リダイレクトループが原因で「ERR_TOO_MANY_REDIRECTS」「このページを表示できません」が出たときの対策12事例+α
リダイレクトループ、自動転送設定ループの原因の解説とその対応方法を含め事例 12例を挙げて説明。
-
ECCUBEでアップロードできない。upload_max_filesizeを設定する場所
テンプレートをアップロードする際に発生するエラー「テンプレートファイルがアップロードされていません」の対処方法。これはファイル容量の制限に引っかかっています。
-
GMOペイメントゲートウェイのjava.io.IOExceptionのエラー
ECCUBEの決済でGMOペイメントゲートウェイのモジュールを使ってテスト決済を行った場合の不具合、java.io.IOExceptionと言うエラーの原因と対策方法の解説です。