[PHP]tidy_get_html関数でHTMLのhtml要素をtidyNodeとして取得する方法を解説

PHP

PHPのTidy拡張には、HTMLドキュメントの各要素をノードとして取得するシリーズ関数があります。tidy_get_body()<body> 要素、tidy_get_head()<head> 要素を返すように、tidy_get_html()<html> 要素に対応する tidyNode オブジェクトを返します。

ドキュメント全体の起点ノードとして、lang 属性の確認、<head><body> 両方への同時アクセス、HTMLツリー全体の走査といった用途に使います。この記事では基本仕様から実践的な活用パターンまで解説します。

関数概要

項目内容
関数名tidy_get_html()
読み方タイディ・ゲット・エイチティーエムエル
分類Tidy関数
対応バージョンPHP 5以降(Tidy拡張が有効な場合)
引数tidyオブジェクト
戻り値tidyNode(html要素のノード)
必要な拡張tidy(多くの環境で標準バンドル)
OOPスタイル$tidy->html()

構文

// 手続き型
tidy_get_html(tidy $tidy): tidyNode

// オブジェクト指向型(推奨)
$tidy->html(): tidyNode

tidy_get_html() と tidy_get_root() の違い

混同しやすい2つの関数の違いを最初に整理します。

ドキュメントの構造:

  root(ルートノード)           ← tidy_get_root() が返す
  └── DOCTYPE
  └── <html>                    ← tidy_get_html() が返す
        ├── <head>              ← tidy_get_head() が返す
        └── <body>              ← tidy_get_body() が返す

tidy_get_root() はDOCTYPE宣言を含むドキュメント全体の根(ルート)を返すのに対し、tidy_get_html()<html> タグそのものを表すノードを返します。HTMLの構造を <head><body> に分けてアクセスする起点としては tidy_get_html() が適しています。

基本的な使い方

<?php
$html = <<<HTML
<!DOCTYPE html>
<html lang="ja">
<head><title>テストページ</title></head>
<body><p>本文</p></body>
</html>
HTML;

$tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
tidy_clean_repair($tidy);

$htmlNode = tidy_get_html($tidy);

echo "ノード名  : " . $htmlNode->name . PHP_EOL;
echo "lang 属性 : " . ($htmlNode->attribute['lang'] ?? '未設定') . PHP_EOL;
echo PHP_EOL;
echo $htmlNode->value . PHP_EOL;

実行結果:

ノード名  : html
lang 属性 : ja

<html lang="ja">
<head>
<title>テストページ</title>
</head>
<body>
<p>本文</p>
</body>
</html>

$htmlNode->value<html>...</html> 全体を含む文字列を返します。<!DOCTYPE> は含まれません。

実践的なコード例

例1:lang 属性の確認と検証

<?php
class HtmlLangChecker
{
    public function check(string $html): array
    {
        $tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
        tidy_clean_repair($tidy);

        $htmlNode = tidy_get_html($tidy);
        $attrs    = $htmlNode->attribute ?? [];

        $lang = $attrs['lang'] ?? null;

        return [
            'has_lang'  => isset($lang),
            'lang'      => $lang,
            'is_valid'  => isset($lang) && preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $lang),
        ];
    }
}

$checker = new HtmlLangChecker();

$pages = [
    '正しいlang属性'   => '<html lang="ja"><head><title>T</title></head><body></body></html>',
    'lang属性なし'     => '<html><head><title>T</title></head><body></body></html>',
    '地域コードあり'   => '<html lang="en-US"><head><title>T</title></head><body></body></html>',
];

foreach ($pages as $label => $pageHtml) {
    $result = $checker->check($pageHtml);
    printf(
        "%-18s has_lang: %-5s lang: %-8s valid: %s\n",
        $label,
        $result['has_lang'] ? 'true' : 'false',
        $result['lang'] ?? '(なし)',
        $result['is_valid'] ? 'true' : 'false'
    );
}

実行結果:

正しいlang属性     has_lang: true  lang: ja       valid: true
lang属性なし       has_lang: false lang: (なし)  valid: false
地域コードあり     has_lang: true  lang: en-US    valid: true

例2:<html> の子ノード(head・body)に同時アクセスする

<?php
class HtmlStructureAnalyzer
{
    public function analyze(string $html): array
    {
        $tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
        tidy_clean_repair($tidy);

        $htmlNode = tidy_get_html($tidy);
        $result   = ['head' => null, 'body' => null];

        if ($htmlNode->hasChildren()) {
            foreach ($htmlNode->child as $child) {
                $name = strtolower($child->name ?? '');
                if ($name === 'head') {
                    $result['head'] = $child;
                } elseif ($name === 'body') {
                    $result['body'] = $child;
                }
            }
        }

        return $result;
    }

    public function report(array $nodes): void
    {
        foreach (['head', 'body'] as $section) {
            $node = $nodes[$section];
            if ($node === null) {
                echo "{$section}: 存在しません" . PHP_EOL;
                continue;
            }
            $childCount = $node->hasChildren() ? count($node->child) : 0;
            printf("%-6s: 子ノード数 %d 件\n", $section, $childCount);
        }
    }
}

$analyzer = new HtmlStructureAnalyzer();
$nodes = $analyzer->analyze(<<<HTML
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テスト</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>ヘッダー</header>
  <main><p>本文</p></main>
  <footer>フッター</footer>
</body>
</html>
HTML);

$analyzer->report($nodes);

実行結果:

head  : 子ノード数 3 件
body  : 子ノード数 3 件

例3:HTMLツリー全体を再帰的に走査してタグ一覧を収集する

<?php
class FullTreeTagCollector
{
    private array $tagCounts = [];

    public function collect(string $html): array
    {
        $tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
        tidy_clean_repair($tidy);

        $this->tagCounts = [];
        $this->traverse(tidy_get_html($tidy));

        arsort($this->tagCounts);
        return $this->tagCounts;
    }

    private function traverse(tidyNode $node): void
    {
        $name = strtolower($node->name ?? '');
        if ($name !== '' && $node->type !== TIDY_NODETYPE_TEXT) {
            $this->tagCounts[$name] = ($this->tagCounts[$name] ?? 0) + 1;
        }

        if ($node->hasChildren()) {
            foreach ($node->child as $child) {
                $this->traverse($child);
            }
        }
    }
}

$collector = new FullTreeTagCollector();
$counts    = $collector->collect(<<<HTML
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>タイトル</title>
</head>
<body>
  <h1>見出し</h1>
  <p>段落1</p>
  <p>段落2</p>
  <ul>
    <li>項目1</li>
    <li>項目2</li>
    <li>項目3</li>
  </ul>
</body>
</html>
HTML);

foreach ($counts as $tag => $count) {
    printf("%-10s: %d 件\n", "<{$tag}>", $count);
}

実行結果:

<li>      : 3 件
<p>       : 2 件
<html>    : 1 件
<head>    : 1 件
<meta>    : 1 件
<title>   : 1 件
<body>    : 1 件
<h1>      : 1 件
<ul>      : 1 件

例4:OOPスタイル(html())での記述

<?php
class OopHtmlNodeDemo
{
    private tidy $tidy;

    public function parse(string $html): self
    {
        $this->tidy = new tidy();
        $this->tidy->parseString($html, [
            'output-xhtml' => true,
            'wrap'         => 0,
        ], 'UTF8');
        $this->tidy->cleanRepair();
        return $this;
    }

    public function getHtmlInfo(): array
    {
        // OOPスタイルの html() は tidy_get_html() と同等
        $htmlNode = $this->tidy->html();

        return [
            'name'        => $htmlNode->name,
            'attributes'  => $htmlNode->attribute ?? [],
            'child_count' => $htmlNode->hasChildren() ? count($htmlNode->child) : 0,
        ];
    }
}

$demo = new OopHtmlNodeDemo();
$info = $demo->parse('<html lang="en" dir="ltr"><head><title>T</title></head><body></body></html>')
             ->getHtmlInfo();

print_r($info);

実行結果:

Array
(
    [name] => html
    [attributes] => Array
        (
            [lang] => en
            [dir] => ltr
        )

    [child_count] => 2
)

例5:アクセシビリティ観点での lang 属性チェックをCIに組み込む

<?php
class LangAttributeCiGate
{
    public function __construct(
        private readonly bool $required = true
    ) {}

    public function evaluate(string $label, string $html): bool
    {
        $tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
        tidy_clean_repair($tidy);

        $htmlNode = tidy_get_html($tidy);
        $lang     = $htmlNode->attribute['lang'] ?? null;

        $pass = !$this->required || ($lang !== null && $lang !== '');

        printf(
            "[%s] %s — lang属性: %s\n",
            $pass ? '✓ PASS' : '✗ FAIL',
            $label,
            $lang ?? '(なし)'
        );

        return $pass;
    }
}

$gate   = new LangAttributeCiGate(required: true);
$allOk  = true;

$pages = [
    'トップ'   => '<html lang="ja"><head><title>T</title></head><body></body></html>',
    '商品ページ' => '<html><head><title>T</title></head><body></body></html>',
    'ブログ'   => '<html lang="ja-JP"><head><title>T</title></head><body></body></html>',
];

foreach ($pages as $label => $html) {
    if (!$gate->evaluate($label, $html)) {
        $allOk = false;
    }
}

echo PHP_EOL . ($allOk ? "✓ すべてのページが合格しました。" : "✗ lang属性が不足しているページがあります。") . PHP_EOL;

実行結果:

[✓ PASS] トップ — lang属性: ja
[✗ FAIL] 商品ページ — lang属性: (なし)
[✓ PASS] ブログ — lang属性: ja-JP

✗ lang属性が不足しているページがあります。

例6:<html> ノードから <head> と <body> の情報を一括レポート

<?php
class HtmlNodeSummaryReporter
{
    public function report(string $html): void
    {
        $tidy = tidy_parse_string($html, [
            'output-xhtml'        => true,
            'accessibility-check' => 1,
        ], 'UTF8');
        tidy_clean_repair($tidy);

        $htmlNode = tidy_get_html($tidy);

        echo "=== <html> 要素の情報 ===" . PHP_EOL;
        printf("ノード名    : %s\n", $htmlNode->name);
        printf("lang属性    : %s\n", $htmlNode->attribute['lang'] ?? '(未設定)');
        printf("子ノード数  : %d\n", $htmlNode->hasChildren() ? count($htmlNode->child) : 0);

        echo PHP_EOL . "=== 品質サマリー ===" . PHP_EOL;
        printf("エラー      : %d 件\n", tidy_error_count($tidy));
        printf("警告        : %d 件\n", tidy_warning_count($tidy));
        printf("A11y警告    : %d 件\n", tidy_access_count($tidy));

        if ($tidy->errorBuffer) {
            echo PHP_EOL . "=== 詳細ログ ===" . PHP_EOL;
            echo $tidy->errorBuffer . PHP_EOL;
        }
    }
}

$reporter = new HtmlNodeSummaryReporter();
$reporter->report(<<<HTML
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>レポートテスト</title>
</head>
<body>
  <img src="logo.png">
  <p>本文</p>
</body>
</html>
HTML);

実行結果:

=== <html> 要素の情報 ===
ノード名    : html
lang属性    : ja
子ノード数  : 2

=== 品質サマリー ===
エラー      : 0 件
警告        : 0 件
A11y警告    : 1 件

=== 詳細ログ ===
line 6 column 3 - Access: [1.1.1]: <img> missing 'alt' text.

例7:tidy_get_html() と tidy_get_root() の違いを確認する

<?php
class HtmlVsRootComparison
{
    public function compare(string $html): void
    {
        $tidy = tidy_parse_string($html, ['output-xhtml' => true], 'UTF8');
        tidy_clean_repair($tidy);

        $root     = tidy_get_root($tidy);
        $htmlNode = tidy_get_html($tidy);

        echo "=== tidy_get_root() ===" . PHP_EOL;
        printf("ノード名 : %s\n", $root->name ?? '(名前なし)');
        printf("type     : %d\n", $root->type);
        printf("子数     : %d\n", $root->hasChildren() ? count($root->child) : 0);

        echo PHP_EOL . "=== tidy_get_html() ===" . PHP_EOL;
        printf("ノード名 : %s\n", $htmlNode->name);
        printf("type     : %d\n", $htmlNode->type);
        printf("子数     : %d\n", $htmlNode->hasChildren() ? count($htmlNode->child) : 0);
    }
}

$comp = new HtmlVsRootComparison();
$comp->compare('<!DOCTYPE html><html lang="ja"><head><title>T</title></head><body><p>本文</p></body></html>');

実行結果:

=== tidy_get_root() ===
ノード名 : (名前なし)
type     : 0
子数     : 2

=== tidy_get_html() ===
ノード名 : html
type     : 1
子数     : 2

tidy_get_root() のノードは名前を持たない最上位コンテナ(DOCTYPEと<html>の親)であるのに対し、tidy_get_html()htmlという名前を持つ具体的な要素ノードであることがわかります。

関連関数との比較

関数OOPスタイル返却するノード特徴
tidy_get_root()root()ドキュメント最上位ルートDOCTYPEを含む、名前なしノード
tidy_get_html()html()<html> 要素lang属性やhead/bodyへのアクセス起点
tidy_get_head()head()<head> 要素メタ情報・スタイル・スクリプト
tidy_get_body()body()<body> 要素ページのコンテンツ部分
tidy_get_output()value プロパティドキュメント全体(文字列)ノードではなく文字列で返す

よくある落とし穴・注意点

  1. $node->value<!DOCTYPE> は含まれない tidy_get_html()<html>...</html> 要素のみを返します。<!DOCTYPE html> 宣言はルートノードに属し、<html> ノードの value には含まれません。DOCTYPE も含めた全体が必要なら (string) $tidy(または tidy_get_output())を使いましょう。
  2. $node->childnull の場合がある <html> 要素が空の場合や特殊なHTMLでは childnull になることがあります。hasChildren() で確認してからアクセスしてください。
  3. tidy_get_root() との混同 「HTMLドキュメントの根」という意味で tidy_get_root() を使いたくなりますが、ツリーを <head> / <body> に分けてアクセスする起点としては tidy_get_html() のほうが適しています。用途に応じて使い分けましょう。
  4. tidy_clean_repair() を呼ばないとノードが不完全 他のノード取得関数と同様に、cleanRepair() を呼んだ後に tidy_get_html() を使うのが正しい手順です。
  5. Tidy が <html> タグを自動補完する <html> タグを省略したHTMLフラグメントを渡しても、Tidy が自動補完するため tidy_get_html() は常に有効なノードを返します。ただし補完された <html> には lang 属性がないことが多いため、lang チェックを行う際は注意してください。

まとめ

ポイント内容
役割HTMLの <html> 要素を tidyNode として取得する
引数tidyオブジェクト
戻り値tidyNode(html要素のノード)
OOPスタイル$tidy->html()
$node->value<html>...</html> 全体の文字列(DOCTYPE は含まない)
tidy_get_root() との違いhtml要素そのものを返す(rootはDOCTYPEも含む最上位コンテナ)
よく組み合わせる関数tidy_clean_repair(), tidy_get_head(), tidy_get_body()
主な用途lang属性の確認・CI検証、head/bodyへの同時アクセス、ツリー全体の走査
注意点value にDOCTYPEは含まれない、child は null チェックが必要

tidy_get_html() は Tidy の4つのノード取得関数(root / html / head / body)の中で、「HTMLドキュメントの構造全体を扱う起点」として最も活用場面が多い関数です。特に lang 属性の検証やアクセシビリティ要件のCI組み込み、<head><body> をまたぐ統合分析に適しています。

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