utility
指定のページの指定のclass内を別のサイトのページに取り込むPHPコードVer2
最終更新日: 2025年2月9日
以前作ったものはその当時正しく動いたが、あるLP制作(マーケット無いし当然ニーズもない=無駄なもの)でコンテンツもろくでもなければってので最短で作ることを条件に既存ページからのスクレイピングをブロックごとに使ってやることにした際に、パースの精度に問題が出たので作り直した。
<?php
// スクレイピング対象のURL
$url = "URL";
// HTMLを取得
$context = stream_context_create([
'http' => [
'timeout' => 10,
'header' => "User-Agent: PHP\r\n",
]
]);
$html = @file_get_contents($url, false, $context);
if ($html === false) {
die("Failed to fetch content from the URL.");
}
// DOMDocumentでHTMLをパース
libxml_use_internal_errors(true); // パースエラーを無視
$dom = new DOMDocument();
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
libxml_clear_errors();
// DOMXPathで特定のクラスを持つ要素を取得(順序指定)
$xpath = new DOMXPath($dom);
// 最初のクラス
$firstNode = $xpath->query("(//div[contains(@class, 'child_pages')])[1]"); // 1つ目の要素
if ($firstNode->length > 0) {
echo "最初の要素:\n" . $dom->saveHTML($firstNode->item(0)) . "\n\n";
}
// 2つ目のクラス
$secondNode = $xpath->query("(//div[contains(@class, 'child_pages')])[2]"); // 2つ目の要素
if ($secondNode->length > 0) {
echo "2つ目の要素:\n" . $dom->saveHTML($secondNode->item(0)) . "\n\n";
}
// 3つ目のクラス(必要なら追加)
$thirdNode = $xpath->query("(//div[contains(@class, 'child_pages')])[3]"); // 3つ目の要素
if ($thirdNode->length > 0) {
echo "3つ目の要素:\n" . $dom->saveHTML($thirdNode->item(0)) . "\n\n";
}
?>
同じクラスが複数あった場合配列として1度で持ってこれるが、引用元で間に別のクラスがある場合やその逆で何か挟みたいときは一つ目・二つ目などうまく使いまわせば可能。
まぁこちらの方が篤実かな。
要件的にはあくまでも引用元と先が同じドメイン所有者といったことが最低限。
商売上添え物的だったコンテンツをLPでちょっと出したいというようなレアケースで、運用管理の手間を減らすのに良いかなというところです。
追記
後日ACFカスタムフィールドでURLとクラス指定し投稿ループで呼び出す形を作った。
自社の異なるブランド=別サイト内の同じカテゴリー商品をまとめるサイト作成だが、コピーコンテンツリスクはドメイン所有が同じでリスク低い、経験上リスクゼロに等しい、管理のしやすさ(価格変更多い企業。。。)、結局シンプルに商品概要書くとオリジナルとほぼ同じコンテンツになる。また元ページの入れ子がdivとsectionがあるので、それぞれに対応させ、この部分仕組化するために修正コード作った。
<?php
// スクレイピング対象のURL
$url = get_field('scrurl');
if (!$url) {
die("URLが設定されていません。");
}
// HTMLを取得
$context = stream_context_create([
'http' => [
'timeout' => 10,
'header' => "User-Agent: PHP\r\n",
]
]);
$html = @file_get_contents($url, false, $context);
if ($html === false) {
die("Failed to fetch content from the URL.");
}
// DOMDocumentでHTMLをパース
libxml_use_internal_errors(true); // パースエラーを無視
$dom = new DOMDocument();
@$dom->loadHTML($html); // 修正: 直接 HTML を渡す
libxml_clear_errors();
// DOMXPathで特定のクラスを持つ要素を取得(div または section)
$xpath = new DOMXPath($dom);
// クラスを取得
$c1 = get_field('scrclass1');
$c2 = get_field('scrclass2');
$c3 = get_field('scrclass3');
if ($c1) {
$firstNode = $xpath->query("(//div[contains(@class, '$c1')] | //section[contains(@class, '$c1')])[1]");
if ($firstNode->length > 0) {
echo $dom->saveHTML($firstNode->item(0)) . "\n\n";
}
}
if ($c2) {
$secondNode = $xpath->query("(//div[contains(@class, '$c2')] | //section[contains(@class, '$c2')])[1]");
if ($secondNode->length > 0) {
echo $dom->saveHTML($secondNode->item(0)) . "\n\n";
}
}
if ($c3) {
$thirdNode = $xpath->query("(//div[contains(@class, '$c3')] | //section[contains(@class, '$c3')])[1]");
if ($thirdNode->length > 0) {
echo $dom->saveHTML($thirdNode->item(0)) . "\n\n";
}
}
?>
複数ブランドの同カテゴリー商品をまたぐサイト、制作者や制作日時により一様でないが、入れ子を複数とすることで柔軟性を増した。
ACFがphpを書いても文字列しか吐かないし、javaは<script>を呼び出しの外に書けば動くものの、javaでは挙動遅いしセキュリティリスクもあるので、このような形にした。
※mb_convert_encoding($html, ‘HTML-ENTITIES’, ‘UTF-8’)が非推奨になったので
$dom = new DOMDocument();
@$dom->loadHTML($html); // 修正: 直接 HTML を渡す
こう変更もした。
このようにブランドサイトが複数あり、横断するサイトが似た構造なら役に立つかも。
画像を使いまわす際に、元サイトが相対パスだった場合のドメイン補完+指定のタグ(属性指定可)削除ver。
<?php
// 既存のコード部分はそのまま...
// スクレイピング対象のURL
$url = get_field('scrurl');
if (!$url) {
die("URLが設定されていません。");
}
// ベースURLの取得
$parsedUrl = parse_url($url);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$basePath = rtrim(dirname($parsedUrl['path']), '/');
// HTMLの取得
$context = stream_context_create([
'http' => [
'timeout' => 10,
'header' => "User-Agent: PHP\r\n",
]
]);
$html = @file_get_contents($url, false, $context);
if ($html === false) {
die("Failed to fetch content from the URL.");
}
// DOMDocumentでHTMLをパース
libxml_use_internal_errors(true);
$dom = new DOMDocument();
@$dom->loadHTML($html);
libxml_clear_errors();
// --- ここから削除処理の追加 ---
// 属性指定で削除する関数
function remove_elements_by_xpath($dom, $xpath_query) {
$xpath = new DOMXPath($dom);
$nodes = $xpath->query($xpath_query);
for ($i = $nodes->length - 1; $i >= 0; $i--) {
$node = $nodes->item($i);
if ($node && $node->parentNode) {
$node->parentNode->removeChild($node);
}
}
}
// 例:h2 要素のうち class="abc" を含むもの、または id="bcd" のものを削除
remove_elements_by_xpath($dom, "//h2[contains(concat(' ', normalize-space(@class), ' '), '')]");/*タグにclass指定する場合は最後の''にclass名を記入*/
remove_elements_by_xpath($dom, "//h2[@id='']");/*タグにid指定する場合は最後の''にid名を記入*/
// --- ここまで削除処理の追加 ---
// 既存の画像パス修正関数などもそのまま使用
function fix_image_paths($dom, $baseUrl, $basePath) {
$images = $dom->getElementsByTagName('img');
foreach ($images as $img) {
$src = $img->getAttribute('src');
if ($src && !preg_match('/^https?:\/\//', $src)) {
if (strpos($src, '/') === 0) {
$img->setAttribute('src', $baseUrl . $src);
} else {
$img->setAttribute('src', $baseUrl . $basePath . '/' . ltrim($src, '/'));
}
}
}
}
// DOMXPathで特定のクラスを持つ要素を取得して画像パス修正する関数
function get_and_fix_element($xpath, $dom, $query, $baseUrl, $basePath) {
$nodes = $xpath->query($query);
if ($nodes->length > 0) {
fix_image_paths($dom, $baseUrl, $basePath);
return $dom->saveHTML($nodes->item(0)) . "\n\n";
}
return '';
}
// クラス指定を取得
$c1 = get_field('scrclass1');
$c2 = get_field('scrclass2');
$c3 = get_field('scrclass3');
$xpath = new DOMXPath($dom);
$output = '';
if ($c1) {
$output .= get_and_fix_element($xpath, $dom, "(//div[contains(@class, '$c1')] | //section[contains(@class, '$c1')])[1]", $baseUrl, $basePath);
}
if ($c2) {
$output .= get_and_fix_element($xpath, $dom, "(//div[contains(@class, '$c2')] | //section[contains(@class, '$c2')])[1]", $baseUrl, $basePath);
}
if ($c3) {
$output .= get_and_fix_element($xpath, $dom, "(//div[contains(@class, '$c3')] | //section[contains(@class, '$c3')])[1]", $baseUrl, $basePath);
}
echo $output;
?>