DOMDocumentを使い、取り込んだHTMLのaタグにrel="nofollow"
やtarget="_blank"
を付けたかったのですが、文字化けなどに苦戦したので残します。
想定対象者
PHPでHTMLのaタグにrel属性やtarget属性を付けたい人、またそれに似た操作をしたい人。
今回の実装コードは以下のように<html>
や<body>
を含まないHTMLを取得し、中にあるaタグを処理するコードになっています。
<html>
タグで囲まれているような完全なHTMLを想定していないので、注意してください。
<!-- <html>や<body>を含まないHTML -->
<div>
<p>
<a href="https://www.google.co.jp/">Google</a><br>
</p>
</div>
環境
- PHP8.2.8
実装
/**
* @param $html
* @return false|string
*/
function addAttributesToLinks($html)
{
$dom = new DOMDocument();
// ①HTMLを読み込む
$dom->loadHTML(
'<?xml encoding="UTF-8"><section>' . $html . '</section>',
LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED
);
// ②aタグを取得と処理
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$link->setAttribute("target", "_blank");
$link->setAttribute('rel', 'nofollow noopener noreferrer');
}
// ③加工したHTMLの保存
$newHtml = $dom->saveHTML($dom->documentElement);
return $newHtml;
}
詳しい説明
①HTMLを読み込む
$dom->loadHTML('<?xml encoding="UTF-8"><section>' . $html . '</section>', LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED);
この部分で文字列のHTMLを受け取り、HTMLとして読み込んでいます。
<?xml encoding="UTF-8">
loadHTMLはHTMLを読み込む際にXHTMLとして読み込むそうなので、xmlの形式で文字コードの宣言をしています。
<section> ... </section>
XMLを解析するLIBXMLの仕様で、一番最初にあるタグをルート要素として、その終了タグまでを処理するようになっています。
その為、全てのHTMLを処理してもらうために追加しています。
つまり、
<p>ごはん</p>
<p>おもち</p>
であれば、<p>ごはん</p>
内だけを処理し<p>おもち</p>
の部分は処理しません。
以下のように<div>
などのルート要素で囲めば、全て処理されます。
<div>
<p>ごはん</p>
<p>おもち</p>
</div>
完全なHTMLを処理する場合、この処理の後に追加したルート要素を削除する必要があります。
LIBXML_NOERROR
loadHTML()のオプション部分です。
DOMDocumentはHTML4を想定しており、HTML5からのタグなどが入っていると「HTMLの文法が違うよ!」とエラーを吐きまくります。
そこで、LIBXMLで定義されているエラー抑制の定数LIBXML_NOERROR
を利用します。
LIBXML_HTML_NOIMPLIED
こちらもLIBXMLで定義されているLIBXML_HTML_NOIMPLIED
です。
DOMDocumentでは、<html>
や<body>
が無い場合に自動でこれらのタグを追加してくれます。
今回は<html>
や<body>
を含まないHTMLを処理を想定しているので、LIBXML_HTML_NOIMPLIED
オプションを付け、<html>
や<body>
の追加を拒否しています。
②aタグの取得と処理
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$link->setAttribute("target", "_blank");
$link->setAttribute('rel', 'nofollow noopener noreferrer');
}
今回の実装では、aタグを取得し、それぞれのaタグにrel属性とtarget属性を追加しています。
PHPの公式ドキュメントを見れば詰まることは無いと思うので、説明は省略します。
③加工したHTMLの保存
$newHtml = $dom->saveHTML($dom->documentElement);
の部分です。
HTMLの保存は$dom->saveHTML()
でも問題なさそうですが、saveHTML()のみで取得すると日本語を数値文字参照として取得してしまいます。
つまり、<p>執筆中</p>
を<p>執筆中</p>
のように処理してしまいます。
数値文字参照でもブラウザは日本語に自動変換をしてくれますが、テスト時などにややこしいので、私は$dom->documentElement
で数値文字参照を回避して受け取るようにしています。
また、'$dom->documentElement'で受け取ることでDOCTYPE
や先に付けた<?xml encoding="UTF-8">
の部分を省略し、ルート要素の部分のみ受け取れます。
<!-- $dom->saveHTML($dom->documentElement)で受け取った場合 -->
<section>
<div>
<a href="https://www.google.co.jp/" target="_blank" rel="nofollow noopener noreferrer">Google</a>
</div>
</section>
<!-- $dom->saveHTML()で受け取った場合 -->
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<?xml encoding="UTF-8">
<section>
<div>
<a href="https://www.google.co.jp/" target="_blank" rel="nofollow noopener noreferrer">Google</a>
</div>
</section>
処理速度
処理速度が気になったので簡易的に検証しました。
検証した際のコードは以下のようになっており、サーバーでなくコマンドラインで実行しています。
<?php
// ファイルを読み込む
$html = file_get_contents("TestCase1.html");
// 10回処理する
for ($i = 1; $i <= 10; $i++){
// 計測開始
$startTime = microtime(true);
// HTML処理
$newHtml = addAttributesToLinks($html);
// 計測終了
$endTime = microtime(true);
$executionTime = $endTime - $startTime;
echo "実行時間: {$executionTime} 秒\n";
}
読み込むHTMLは以下のように、同じURLが2048個あるものです。
処理する関数は実装とほぼ同じものです
function addAttributesToLinks($html)
{
$dom = new DOMDocument();
$cnt = 0;
// ①HTMLを読み込む
$dom->loadHTML(
'<?xml encoding="UTF-8"><section>' . $html . '</section>',
LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED
);
// ②aタグを取得と処理
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$link->setAttribute("target", "_blank");
$link->setAttribute('rel', 'nofollow noopener noreferrer');
}
// ③HTMLの保存
$newHtml = $dom->saveHTML($dom->documentElement);
echo $cnt."回処理しました。";
return $newHtml;
}
結果
パソコンの性能や、状態にもよりますが10回計測したところ以下の通りでした。
2048個の場合、0.1秒前後といったところでしょうか。
そんな数の処理はしないと思いますが実用的なところを考えると、この方法の場合URL2000個ぐらいが限界そうです。