[PHP]tidy_get_root完全解説|TidyのDOMツリーのルートノードを取得してHTML構造を走査する方法

PHP

1. 関数概要

tidy_get_root は、PHP の Tidy 拡張が解析した HTML/XHTML ドキュメントの DOM ツリーのルートノードtidyNode オブジェクトとして返す関数です。返された tidyNode を起点にして子ノードを再帰的に辿ることで、HTML 構造全体をプログラムで走査・検査・抽出できます。

項目内容
関数名tidy_get_root
所属拡張Tidy
戻り値の型tidyNode|false
手続き型 / OOP手続き型(OOP 版: $tidy->root()
PHP バージョンPHP 5 以降
公式ドキュメントhttps://www.php.net/manual/ja/tidy.root.php

2. 構文

// 手続き型
tidy_get_root(tidy $tidy): tidyNode|false

// オブジェクト指向型
$tidy->root(): tidyNode|false

パラメータ

パラメータ説明
$tidytidytidy_parse_string() 等で生成した tidy オブジェクト

戻り値

  • 成功時: DOM ツリーの最上位を表す tidyNode オブジェクト
  • 失敗時: false

3. tidyNode の主なプロパティ

tidy_get_root() が返す tidyNode オブジェクトは以下のプロパティを持ちます。

プロパティ内容
$node->namestringノード名(タグ名、例: "html", "body", "p" など)
$node->valuestring|nullテキストノードの場合の文字列値
$node->typeintノードの種別(下表参照)
$node->idintHTML タグの ID(TIDY_TAG_* 定数に対応)
$node->attributearray|null属性の連想配列(例: ["href" => "https://..."]
$node->hasChildren()bool子ノードを持つかどうか
$node->hasSiblings()bool兄弟ノードを持つかどうか
$node->childarray|nulltidyNode の配列

ノード種別($node->type)

定数意味
TIDY_NODETYPE_ROOT0ルートノード
TIDY_NODETYPE_DOCTYPE1DOCTYPE 宣言
TIDY_NODETYPE_COMMENT2コメントノード
TIDY_NODETYPE_PROCINS3処理命令
TIDY_NODETYPE_TEXT4テキストノード
TIDY_NODETYPE_START5開始タグ
TIDY_NODETYPE_END6終了タグ
TIDY_NODETYPE_STARTEND7空要素タグ(<br /> 等)
TIDY_NODETYPE_CDATA8CDATA セクション
TIDY_NODETYPE_SECTION9XML セクション
TIDY_NODETYPE_ASP10ASP ノード
TIDY_NODETYPE_JSTE11JSTE ノード
TIDY_NODETYPE_PHP12PHP ノード
TIDY_NODETYPE_XMLDECL13XML 宣言

4. 動作概念図

tidy_get_root($tidy)
        │
        ▼
┌──────────────────────────────────────────────┐
│ tidyNode (type=ROOT, name="")                │  ← ルートノード
│                                              │
│  ├── tidyNode (type=DOCTYPE)                 │  DOCTYPE 宣言
│  │                                           │
│  └── tidyNode (type=START, name="html")      │  <html>
│       │                                      │
│       ├── tidyNode (name="head")             │  <head>
│       │    └── tidyNode (name="title")       │  <title>
│       │         └── tidyNode (type=TEXT)     │  テキスト
│       │                                      │
│       └── tidyNode (name="body")             │  <body>
│            ├── tidyNode (name="h1")          │  <h1>
│            │    └── tidyNode (type=TEXT)     │  テキスト
│            └── tidyNode (name="p")           │  <p>
│                 └── tidyNode (type=TEXT)     │  テキスト
└──────────────────────────────────────────────┘

  hasChildren() / $node->child[] で子ノードへ再帰的に辿れる

5. 基本的な使い方

<?php
$html = '<!DOCTYPE html>
<html>
<head><title>サンプル</title></head>
<body><h1>見出し</h1><p>本文テキスト</p></body>
</html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$root = tidy_get_root($tidy);

if ($root !== false) {
    echo "ルートノード名 : " . ($root->name  ?? '(空)') . PHP_EOL;
    echo "ノード種別     : " . $root->type               . PHP_EOL;
    echo "子ノード有無   : " . ($root->hasChildren() ? 'あり' : 'なし') . PHP_EOL;
}

出力例:

ルートノード名 : (空)
ノード種別     : 0
子ノード有無   : あり

6. 実践的なコード例

例1: DOM ツリーを再帰的に表示するクラス

<?php
class TidyTreePrinter
{
    public function print(tidy $tidy): void
    {
        $root = tidy_get_root($tidy);
        if ($root === false) {
            echo "ルートノードの取得に失敗しました。" . PHP_EOL;
            return;
        }
        $this->walk($root, 0);
    }

    private function walk(tidyNode $node, int $depth): void
    {
        $indent = str_repeat('  ', $depth);
        $label  = match($node->type) {
            TIDY_NODETYPE_ROOT    => '[ROOT]',
            TIDY_NODETYPE_DOCTYPE => '[DOCTYPE]',
            TIDY_NODETYPE_TEXT    => '[TEXT] ' . trim($node->value ?? ''),
            TIDY_NODETYPE_START,
            TIDY_NODETYPE_STARTEND => '<' . $node->name . '>',
            default                => '[type=' . $node->type . ']',
        };
        echo $indent . $label . PHP_EOL;

        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walk($child, $depth + 1);
            }
        }
    }
}

$html = '<!DOCTYPE html><html><head><title>テスト</title></head>
<body><h1>見出し</h1><p>本文</p></body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$printer = new TidyTreePrinter();
$printer->print($tidy);

出力例:

[ROOT]
  [DOCTYPE]
  <html>
    <head>
      <title>
        [TEXT] テスト
    <body>
      <h1>
        [TEXT] 見出し
      <p>
        [TEXT] 本文

例2: OOP スタイルで $tidy->root() を使う

<?php
class TidyOopRootFetcher
{
    private tidy $tidy;

    public function __construct(string $html, array $config = [])
    {
        $this->tidy = new tidy();
        $this->tidy->parseString($html, $config, 'UTF8');
        $this->tidy->cleanRepair();
    }

    public function getRoot(): tidyNode|false
    {
        return $this->tidy->root();
    }

    public function describeRoot(): void
    {
        $root = $this->getRoot();
        if ($root === false) {
            echo "取得失敗" . PHP_EOL;
            return;
        }
        echo "ノード種別    : " . $root->type . " (ROOT=" . TIDY_NODETYPE_ROOT . ")" . PHP_EOL;
        echo "子ノード数    : " . count($root->child ?? []) . PHP_EOL;
    }
}

$html = '<!DOCTYPE html><html><head><title>OOP</title></head><body><p>本文</p></body></html>';
$fetcher = new TidyOopRootFetcher($html);
$fetcher->describeRoot();

出力例:

ノード種別    : 0 (ROOT=0)
子ノード数    : 2

例3: 特定のタグ名を持つノードをすべて収集するクラス

<?php
class TidyNodeCollector
{
    /** @var tidyNode[] */
    private array $found = [];

    public function collect(tidy $tidy, string $tagName): array
    {
        $this->found = [];
        $root = tidy_get_root($tidy);
        if ($root !== false) {
            $this->search($root, strtolower($tagName));
        }
        return $this->found;
    }

    private function search(tidyNode $node, string $tagName): void
    {
        if (strtolower($node->name ?? '') === $tagName) {
            $this->found[] = $node;
        }
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->search($child, $tagName);
            }
        }
    }
}

$html = '<!DOCTYPE html><html><body>
<p>段落1</p><p>段落2</p><div><p>段落3</p></div>
</body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$collector = new TidyNodeCollector();
$pNodes    = $collector->collect($tidy, 'p');

echo "見つかった <p> タグの数: " . count($pNodes) . PHP_EOL;
foreach ($pNodes as $i => $node) {
    $text = '';
    if ($node->hasChildren()) {
        foreach ($node->child as $child) {
            if ($child->type === TIDY_NODETYPE_TEXT) {
                $text .= trim($child->value ?? '');
            }
        }
    }
    echo "  [{$i}] " . $text . PHP_EOL;
}

出力例:

見つかった <p> タグの数: 3
  [0] 段落1
  [1] 段落2
  [2] 段落3

例4: すべてのリンク(href)を抽出するクラス

<?php
class TidyLinkExtractor
{
    /** @var string[] */
    private array $links = [];

    public function extract(tidy $tidy): array
    {
        $this->links = [];
        $root = tidy_get_root($tidy);
        if ($root !== false) {
            $this->walk($root);
        }
        return array_unique($this->links);
    }

    private function walk(tidyNode $node): void
    {
        if (strtolower($node->name ?? '') === 'a') {
            $href = $node->attribute['href'] ?? null;
            if ($href !== null && $href !== '') {
                $this->links[] = $href;
            }
        }
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walk($child);
            }
        }
    }
}

$html = '<!DOCTYPE html><html><body>
<a href="https://example.com">Example</a>
<a href="https://php.net">PHP</a>
<a href="https://example.com">Example 重複</a>
<p>リンクなし</p>
</body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$extractor = new TidyLinkExtractor();
$links     = $extractor->extract($tidy);

echo "抽出されたリンク一覧:" . PHP_EOL;
foreach ($links as $link) {
    echo "  - " . $link . PHP_EOL;
}

出力例:

抽出されたリンク一覧:
  - https://example.com
  - https://php.net

例5: テキストノードの内容をすべて結合して取得するクラス

<?php
class TidyTextExtractor
{
    private string $buffer = '';

    public function extractText(tidy $tidy): string
    {
        $this->buffer = '';
        $root = tidy_get_root($tidy);
        if ($root !== false) {
            $this->walk($root);
        }
        return trim($this->buffer);
    }

    private function walk(tidyNode $node): void
    {
        if ($node->type === TIDY_NODETYPE_TEXT) {
            $this->buffer .= $node->value ?? '';
        }
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walk($child);
            }
        }
    }
}

$html = '<!DOCTYPE html><html><head><title>タイトル</title></head>
<body><h1>見出し</h1><p>段落のテキスト。</p><p>2つ目の段落。</p></body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$extractor = new TidyTextExtractor();
echo "抽出テキスト:" . PHP_EOL;
echo $extractor->extractText($tidy) . PHP_EOL;

出力例:

抽出テキスト:
タイトル
見出し
段落のテキスト。
2つ目の段落。

例6: ノード数・深さ・タグ種別を集計するクラス

<?php
class TidyTreeAnalyzer
{
    private int $totalNodes = 0;
    private int $maxDepth   = 0;
    /** @var array<string, int> */
    private array $tagCounts = [];

    public function analyze(tidy $tidy): void
    {
        $this->totalNodes = 0;
        $this->maxDepth   = 0;
        $this->tagCounts  = [];

        $root = tidy_get_root($tidy);
        if ($root !== false) {
            $this->walk($root, 0);
        }
    }

    private function walk(tidyNode $node, int $depth): void
    {
        $this->totalNodes++;
        $this->maxDepth = max($this->maxDepth, $depth);

        $name = $node->name ?? '';
        if ($name !== '' && $node->type === TIDY_NODETYPE_START) {
            $this->tagCounts[$name] = ($this->tagCounts[$name] ?? 0) + 1;
        }

        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walk($child, $depth + 1);
            }
        }
    }

    public function report(): void
    {
        echo "=== DOM ツリー解析レポート ===" . PHP_EOL;
        echo "総ノード数   : " . $this->totalNodes . PHP_EOL;
        echo "最大深さ     : " . $this->maxDepth   . PHP_EOL;
        echo "タグ別集計   :" . PHP_EOL;
        arsort($this->tagCounts);
        foreach ($this->tagCounts as $tag => $count) {
            printf("  %-15s: %d 個%s", "<{$tag}>", $count, PHP_EOL);
        }
    }
}

$html = '<!DOCTYPE html><html><head><title>解析</title></head>
<body>
  <h1>見出し</h1>
  <p>段落1</p><p>段落2</p>
  <ul><li>項目1</li><li>項目2</li><li>項目3</li></ul>
  <a href="#">リンク</a>
</body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$analyzer = new TidyTreeAnalyzer();
$analyzer->analyze($tidy);
$analyzer->report();

出力例:

=== DOM ツリー解析レポート ===
総ノード数   : 28
最大深さ     : 5
タグ別集計   :
  <li>           : 3 個
  <p>            : 2 個
  <html>         : 1 個
  <head>         : 1 個
  <title>        : 1 個
  <body>         : 1 個
  <h1>           : 1 個
  <ul>           : 1 個
  <a>            : 1 個

例7: 属性値でノードを検索するクラス

<?php
class TidyAttributeSearcher
{
    /** @var tidyNode[] */
    private array $results = [];

    /**
     * 指定した属性名と値(部分一致)でノードを検索する
     */
    public function search(tidy $tidy, string $attrName, string $attrValue = ''): array
    {
        $this->results = [];
        $root = tidy_get_root($tidy);
        if ($root !== false) {
            $this->walk($root, $attrName, $attrValue);
        }
        return $this->results;
    }

    private function walk(tidyNode $node, string $attrName, string $attrValue): void
    {
        $attrs = $node->attribute ?? [];
        if (isset($attrs[$attrName])) {
            if ($attrValue === '' || str_contains($attrs[$attrName], $attrValue)) {
                $this->results[] = $node;
            }
        }
        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->walk($child, $attrName, $attrValue);
            }
        }
    }
}

$html = '<!DOCTYPE html><html><body>
  <div class="container main">メインコンテナ</div>
  <div class="sidebar">サイドバー</div>
  <p class="container sub">サブ段落</p>
  <a href="https://example.com" class="link">リンク</a>
</body></html>';

$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);

$searcher = new TidyAttributeSearcher();

// class 属性に "container" を含むノードを検索
$nodes = $searcher->search($tidy, 'class', 'container');
echo "class に 'container' を含むノード: " . count($nodes) . " 件" . PHP_EOL;
foreach ($nodes as $node) {
    echo "  <" . $node->name . " class=\"" . ($node->attribute['class'] ?? '') . "\">" . PHP_EOL;
}

出力例:

class に 'container' を含むノード: 2 件
  <div class="container main">
  <p class="container sub">

7. 関連関数との比較

関数名取得対象戻り値
tidy_get_root()DOM ツリー全体のルートノードtidyNode|false
tidy_get_html()<html> 要素ノードtidyNode|false
tidy_get_head()<head> 要素ノードtidyNode|false
tidy_get_body()<body> 要素ノードtidyNode|false
tidy_get_output()整形済み HTML 文字列string
tidy_get_error_buffer()エラーバッファ文字列string|false
tidy_get_status()解析ステータスint

使い分けのポイント: <body> 以下のコンテンツだけ走査したい場合は tidy_get_body()、DOCTYPE を含む文書全体を走査したい場合は tidy_get_root() を使います。


8. よくある落とし穴と注意点

① tidy_clean_repair() を呼ばなくてもノードは取得できるが修復はされない

tidy_get_root() 自体は tidy_clean_repair() なしでも動作しますが、不正な HTML の場合にノード構造が不完全になることがあります。確実な走査のためにクリーニング後に呼び出してください。

// 推奨: cleanRepair 後に root を取得
$tidy = tidy_parse_string($html, [], 'UTF8');
tidy_clean_repair($tidy);
$root = tidy_get_root($tidy);

② ルートノードは <html> タグではない

tidy_get_root() が返すノードは type=0TIDY_NODETYPE_ROOT)の仮想的なルートであり、<html> 要素そのものではありません。<html> 要素を直接取得したい場合は tidy_get_html() を使います。

$root    = tidy_get_root($tidy);  // 仮想ルート(name は空文字)
$htmlTag = tidy_get_html($tidy);  // <html> 要素ノード

③ テキストノードの name は空文字になる

テキストノード(type=TIDY_NODETYPE_TEXT)は name プロパティが空文字または null になります。$node->name で分岐する際は type も合わせて確認してください。

④ 再帰が深いと stack overflow に注意

非常に深くネストした HTML を処理する場合、再帰的な走査でスタックオーバーフローが起きることがあります。ini_set('xdebug.max_nesting_level', 500) の調整や、スタックを使った反復処理への置き換えを検討してください。


9. まとめ

項目内容
主な用途Tidy が解析した DOM ツリーのルートノードを取得し再帰走査の起点とする
戻り値tidyNode|false
ルートノードの種別TIDY_NODETYPE_ROOT(=0)、<html> タグ自体ではない
OOP 版$tidy->root()
子ノードへのアクセス$node->child 配列 / $node->hasChildren()
よく使う組み合わせtidy_get_body(), tidy_get_head(), tidy_get_html(), tidy_clean_repair()

tidy_get_root() は Tidy の DOM 走査機能の入口となる関数です。返された tidyNode を再帰的に辿ることで、リンク抽出・テキスト収集・タグ集計・属性検索など、HTML 構造に基づいた多様な処理を実装できます。シンプルなコンテンツ抽出には tidy_get_body() との組み合わせも効果的です。

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