【PHP実践|実務向け】PHPでHTML/XMLを自在に操る!DOMDocumentとXPathによるデータ抽出の極意

1. 導入: なぜHTML/XML解析が重要なのか?

PHPバックエンド開発において、外部データとの連携は避けて通れない道です。特に、Webサイトから特定の情報を取得するWebスクレイピングや、XML形式のAPIレスポンスを解析する場面では、効率的かつ堅牢なデータ抽出が求められます。手動で正規表現を駆使して文字列解析を行うことも可能ですが、HTML/XMLの構造が少しでも変わるとすぐに破綻してしまいます。

そこで登場するのが、PHP標準のDOM拡張モジュールとXPathです。これらを活用することで、HTML/XMLドキュメントをオブジェクトツリーとして扱い、構造的な変更にも強い柔軟なデータ抽出を実現できます。本記事では、実務で役立つDOMDocumentとXPathを使ったHTML/XML解析の基礎から応用までを、具体的なコード例を交えて解説します。

2. 基礎知識: DOMDocumentとXPathの基本

DOMDocumentとは?

DOMDocumentは、HTMLやXMLドキュメントを「Document Object Model (DOM)」の形式で表現するPHPのクラスです。DOMは、ドキュメント全体をツリー構造のオブジェクトとしてメモリ上に展開し、各要素、属性、テキストなどをノードとして扱います。これにより、プログラマはツリーを辿りながら、特定のノードの検索、追加、削除、変更といった操作をオブジェクト指向的に行えるようになります。

XPathとは?

XPath(XML Path Language)は、XMLドキュメント(HTMLも含む)内のノードを検索するための言語です。ファイルシステムにおけるパス指定のように、特定の要素や属性を効率的に指定できます。DOMDocumentと組み合わせることで、複雑な条件でのノード検索を非常に簡潔に記述できるため、データ抽出において強力なツールとなります。

基本的なXPathの記法

  • `/` (ルートからの絶対パス): ドキュメントのルート要素からのパスを指定します。例: `/html/body/div`
  • `//` (任意の場所からのパス): ドキュメント内の任意の場所から、指定したノードを検索します。例: `//div` (すべての `div` 要素)
  • `@` (属性): ノードの属性を指定します。例: `//a/@href` (すべての `a` 要素の `href` 属性)
  • `[]` (条件): 検索するノードに条件を付与します。例: `//div[@id=”content”]` (`id`属性が”content”の`div`要素)
  • `text()` (テキストノード): ノードのテキスト内容を指定します。例: `//p[text()=”Hello”]` (テキストが”Hello”の`p`要素)
  • `contains(@attr, ‘value’)` (部分一致): 属性値が特定の部分文字列を含むノードを検索します。例: `//img[contains(@src, ‘thumbnail’)]`
  • `starts-with(@attr, ‘value’)` (前方一致): 属性値が特定の文字列で始まるノードを検索します。例: `//a[starts-with(@href, ‘http’)]`
  • `.` (現在のノード): コンテキストノード(現在の検索起点となるノード)を表します。

3. 実装/解決策: DOMDocumentとXPathによるデータ抽出

HTML/XMLの解析は、以下のステップで進めます。

1. DOMDocumentオブジェクトの初期化: `new DOMDocument()` でインスタンスを作成します。
2. HTML/XMLの読み込み:

  • 文字列からの読み込み: `loadHTML($htmlString)` または `loadXML($xmlString)`
  • ファイルからの読み込み: `loadHTMLFile($filePath)` または `load($filePath)`

HTMLの場合、`mb_convert_encoding()` で文字エンコーディングを適切に処理し、`@`演算子で不正なHTMLによるWarningを抑制するのが一般的です。
3. DOMXPathオブジェクトの作成: `new DOMXPath($domDocument)` でインスタンスを作成し、XPathクエリを実行できるようにします。
4. XPathクエリの実行: `DOMXPath`オブジェクトの `query($xpathQuery, $contextNode)` メソッドを使用します。

  • `$xpathQuery`: 検索条件を記述したXPath文字列。
  • `$contextNode` (オプション): 検索の起点となるDOMノード。これを指定すると、そのノード以下から相対的に検索します。

`query()` メソッドは、条件に一致したすべてのノードを含む `DOMNodeList` オブジェクトを返します。
5. 結果の処理: `DOMNodeList` から `item($index)` メソッドで個々の `DOMNode` を取得し、`nodeValue` でテキスト内容、`getAttribute($attrName)` で属性値などを取り出します。

XPath内でPHP関数を利用する

XPath 1.0では、文字列の大文字小文字を無視した比較など、一部の高度な処理が直接できません。しかし、PHPのDOMXPathは、PHP関数をXPathクエリ内で利用できる機能を提供します。

  • `$xpath->registerNamespace(“php”, “http://php.net/xpath”);`:`php`という名前空間を登録します。
  • `$xpath->registerPHPFunctions();`:すべてのPHP関数をXPath内で利用できるようにします。特定の関数のみを登録することも可能です(例: `registerPHPFunctions(‘strcasecmp’)`)。

これにより、`php:functionString(‘strcasecmp’, name(), ‘dl’)=0` のように、XPathクエリ内でPHPの `strcasecmp` 関数を呼び出し、要素名の大文字小文字を無視した比較ができるようになります。

4. サンプルプログラム

以下のサンプルコードは、与えられたHTML文字列から様々な情報をXPathを使って抽出する方法を示しています。

  • ノードの内部HTMLコンテンツを取得するヘルパー関数
  • DOMNode::saveHTML() を利用して子ノードのHTMLを結合します。
  • @param DOMNode $node HTMLを取得したいノード
  • @return string ノードの内部HTML文字列
  • /
    function getInnerHtml(DOMNode $node): string
    {
    $html = ”;
    // 子ノードを全て走査し、それぞれのHTMLを結合
    foreach ($node->childNodes as $child) {
    $html .= $node->ownerDocument->saveHTML($child);
    }
    return $html;
    }

    // 解析対象のHTML文字列
    $html = <<

    ページのタイトル

    "Hello, World!"

    サンプル テキスト

    • 項目A
    • 項目B
    • 項目C


    用語

    説明



    EOD;

    // DOMDocumentオブジェクトを初期化
    $dom = new DOMDocument(‘1.0’, ‘UTF-8’);
    // HTMLエンティティを適切に処理するため、エンコーディングを変換
    // ‘auto’は検出に任せるが、明確な場合は ‘UTF-8’ など指定
    $html = mb_convert_encoding($html, “HTML-ENTITIES”, ‘auto’);
    // HTMLを読み込む。@でHTMLの構文エラーによるWarningを抑制
    @$dom->loadHTML($html);

    // DOMXPathオブジェクトを初期化
    $xpath = new DOMXPath($dom);
    // PHP関数をXPathクエリ内で利用するための準備
    $xpath->registerNamespace(“php”, “http://php.net/xpath”);
    $xpath->registerPHPFunctions();

    echo “— ページの基本情報 —” . PHP_EOL;

    // ページのタイトルを取得
    // XPath: //head/title[1] – head要素直下の最初のtitle要素
    $titleNode = $xpath->query(‘//head/title[1]’);
    if ($titleNode->length > 0) {
    echo “タイトル: ” . $titleNode->item(0)->nodeValue . PHP_EOL;
    } else {
    echo “タイトルが見つかりませんでした。” . PHP_EOL;
    }

    // id=”content” の要素を取得
    // XPath: //[@id=”content”] – id属性が”content”の任意の要素
    $contentNode = $xpath->query(‘//[@id=”content”]’);
    if ($contentNode->length > 0) {
    echo “ID ‘content’ の要素: (取得成功)” . PHP_EOL;
    $content = $contentNode->item(0); // 後でコンテキストノードとして使用
    } else {
    echo “ID ‘content’ の要素が見つかりませんでした。” . PHP_EOL;
    $content = null;
    }

    echo PHP_EOL . “— 特定の要素とその内容 —” . PHP_EOL;

    // id=”sample” の要素を取得し、nodeValueと内部HTMLの両方を表示
    // XPath: //[@id=”sample”] – id属性が”sample”の任意の要素
    $sampleNode = $xpath->query(‘//[@id=”sample”]’);
    if ($sampleNode->length > 0) {
    echo “ID ‘sample’ の nodeValue: ” . $sampleNode->item(0)->nodeValue . PHP_EOL;
    echo “ID ‘sample’ の InnerHtml: ” . getInnerHtml($sampleNode->item(0)) . PHP_EOL;
    } else {
    echo “ID ‘sample’ の要素が見つかりませんでした。” . PHP_EOL;
    }

    echo PHP_EOL . “— 属性の取得 —” . PHP_EOL;

    // フッター内のリンク(aタグ)のhref属性を取得
    // XPath: //div[@id=”footer”]//a[1] – idが”footer”のdiv要素以下の最初のa要素
    $aNode = $xpath->query(‘//div[@id=”footer”]//a[1]’);
    if ($aNode->length > 0) {
    echo “フッターリンクのURL: ” . $aNode->item(0)->getAttribute(‘href’) . PHP_EOL;
    } else {
    echo “フッターリンクが見つかりませんでした。” . PHP_EOL;
    }

    // 内のすべての画像(imgタグ)のsrc属性を取得
    // XPath: //body//img – body要素以下のすべてのimg要素
    $imgNodes = $xpath->query(‘//body//img’);
    echo “画像パス一覧:” . PHP_EOL;
    if ($imgNodes->length > 0) {
    foreach ($imgNodes as $img) {
    echo “- ” . $img->getAttribute(‘src’) . PHP_EOL;
    }
    } else {
    echo “画像が見つかりませんでした。” . PHP_EOL;
    }

    echo PHP_EOL . “— コンテキストノードとリスト要素の取得 —” . PHP_EOL;

    // id=”content” の要素内にあるリスト(ul/li)を取得
    // 第二引数に$contentを渡すことで、そのノード以下から相対的に検索 (.//ul[1]/li)
    if ($content) {
    $liNodes = $xpath->query(‘.//ul[1]/li’, $content);
    echo “リスト項目:” . PHP_EOL;
    if ($liNodes->length > 0) {
    foreach ($liNodes as $li) {
    echo “- ” . $li->nodeValue . PHP_EOL;
    }
    } else {
    echo “リスト項目が見つかりませんでした。” . PHP_EOL;
    }
    }

    echo PHP_EOL . “— 大文字小文字を無視した要素名の検索 —” . PHP_EOL;

    // DLタグ(大文字・小文字を区別せず)とその子要素を取得
    // XPath: .//[php:functionString(‘strcasecmp’, name(), ‘dl’)=0]
    // name()は現在の要素名、strcasecmpで’dl’と比較し、0なら一致と判断
    if ($content) {
    $dlNode = $xpath->query(“.//[php:functionString(‘strcasecmp’, name(), ‘dl’)=0]”, $content);
    if ($dlNode->length > 0) {
    echo “DLタグの内容:” . PHP_EOL;
    foreach ($dlNode->item(0)->childNodes as $childNode) {
    // テキストノードやコメントノードを除外するため、ELEMENT_NODEのみを処理
    if ($childNode->nodeType === XML_ELEMENT_NODE) {
    $nodeName = strtolower($childNode->nodeName); // 要素名を小文字に変換
    if ($nodeName === ‘dt’) {
    echo ” DT: ” . $childNode->nodeValue . PHP_EOL;
    } elseif ($nodeName === ‘dd’) {
    echo ” DD: ” . $childNode->nodeValue . PHP_EOL;
    }
    }
    }
    } else {
    echo “DLタグが見つかりませんでした。” . PHP_EOL;
    }
    }
    ?>

    5. 応用・注意点: 現場で役立つ情報

    Webスクレイピングにおけるマナーと法的側面

    Webスクレイピングを行う際は、以下の点に注意してください。

    • robots.txtの確認: 多くのWebサイトには `robots.txt` というファイルがあり、クローラーやスクレイパーにアクセスを許可しないパスが指定されています。これを遵守しましょう。
    • サイトの利用規約: サイトによってはスクレイピング自体を禁止している場合があります。利用規約を必ず確認してください。
    • サーバーへの負荷: 短時間に大量のリクエストを送ると、相手のサーバーに負荷をかけ、アクセス制限やIPブロックの原因となります。適切な間隔を空けてリクエストを送りましょう。
    • 著作権・肖像権: 取得したデータの利用には、著作権や肖像権などの法的問題が絡む場合があります。法的な規制を理解し、適切に利用してください。

    不正なHTML構造への対応

    現実のHTMLは、閉じタグの不足や属性値の引用符忘れなど、W3C標準に準拠していないことが多々あります。

    • `@`演算子でのエラー抑制: `DOMDocument::loadHTML()` は不正なHTMLを検出するとWarningを発生させます。実運用では `@$dom->loadHTML($html);` のように `@` 演算子でWarningを抑制し、処理を継続させるのが一般的です。
    • エンコーディングの重要性: `mb_convert_encoding($html, “HTML-ENTITIES”, ‘auto’);` は、文字化けを防ぐために非常に重要です。特に外部サイトのHTMLを扱う場合は、対象サイトのエンコーディングに合わせて適切に設定してください。

    動的なコンテンツへの非対応

    DOMDocumentとXPathは、サーバーから送られてきた生のHTMLを解析します。JavaScriptによって後から生成・変更されるコンテンツ(例: AJAXで読み込まれるデータ、SPAのコンテンツ)には対応できません。このような場合は、SeleniumやPuppeteerのようなヘッドレスブラウザをPHPから制御するライブラリ(例: `php-webdriver/webdriver`)の利用を検討する必要があります。

    XPathのパフォーマンス

    非常に大規模なHTML/XMLドキュメントの場合、複雑なXPathクエリはパフォーマンスに影響を与える可能性があります。可能な限り、具体的にパスを指定したり、コンテキストノードを活用して検索範囲を限定したりすることで、効率を向上させることができます。

    DOMDocumentとXPath以外の選択肢

    XPathは強力ですが、CSSセレクタの方が直感的に感じる開発者もいるかもしれません。PHPには、CSSセレクタを使ってDOMを操作できるライブラリも存在します。例えば、SymfonyのDomCrawlerコンポーネントは、jQueryライクなAPIで要素を選択でき、XPathの知識がなくても手軽にHTML/XMLを解析できます。プロジェクトの要件や開発者のスキルセットに応じて、適切なツールを選択しましょう。

    DOMDocumentとXPathは、PHPでHTML/XMLを扱う上で非常に強力なツールです。これらの基礎をしっかりと理解し、実務で活用することで、データ連携の幅が大きく広がるでしょう。

    タイトルとURLをコピーしました