XPathはSeleniumかXSLTで極稀に使うだけだった事に気がついたので、PHPのXPathを使ってみました。
XPathの結果がDOMで返るようなので、併せてDOMも。今までは、PHPのDOMサポートがどのような状況なのかよく分かっておらず、HTMLをいじる際にはPEARのXML_HTMLSaxで済ませていました。DOMDocument->loadHTML()のドキュメントに"(No version information available, might be only in CVS)"とあるのはどういう事でしょうか?MacPortsから入れたPHP5.2.0では使えるんですが。
XPathについて半端な知識しかないまま過ごして来たので、XPath自体は改めてドキュメントを読もうと思っていますが、ひとまずPHPでどうつかえばよいかを簡単に試した結果をメモしておきます。
以下、テストコード。
<?php class HTML_Scraper { private $_html; private $_dom; private $_xpath; private $_cache; public function __construct($source = '') { $this->_html = $source; $this->_dom = new DOMDocument(); $this->_dom->loadHTML($source); $this->_xpath = new DOMXPath($this->_dom); $this->_cache = array(); } private function _setCache($exp, $result) { $this->_cache[$exp] = $result; } private function _getCache($exp) { if (isset($this->_cache[$exp])) { return $this->_cache[$exp]; } else { return null; } } public function find($name, $attributes = array()) { return $this->findAt($name, $attributes, 0); } public function findAt($name, $attributes = array(), $index) { $results = $this->findAll($name, $attributes); if ($results->length > 0) { return $results->item(0); } else { return null; } } public function findAll($name, $attributes = array()) { $exp = '//' . $name; if (!empty($attributes) and is_array($attributes)) { $attrs = array(); foreach ($attributes as $key => $value) { if (is_numeric($key)) { $attrs[] = '@' . $value; } else if (!empty($key) and !empty($value)) { $attrs[] = '@' . $key . '="' . addslashes($value) . '"'; } } $exp .= '[' . implode(' and ', $attrs) .']'; echo $exp . "\n"; } return $this->_xpath->query($exp); } public function execute($exp) { if (($result = $this->_getCache($exp)) == null) { $result = $this->_xpath->query($exp); $this->_setCache($exp, $result); } return $result; } } $source = "<html> <head> <meta http-equiv=\"content-type\" content=\"text/html; charset=<%CHARSET%>\" /> <title>たいと〜る</title> <script type=\"text/javascript\"> //<![CDATA[ document.write(\"<p>in cdata<\" + \"/p>\"); //]]> </script> <script type=\"text/javascript\" language=\"javascript\"> <!--// alert('in comment'); //--> </script> </head> <body> <ul> <li class=\"yes\">りすとの1<br /></li> <li class=\"non\">りすとの2</li> <li class=\"yes\">りすとの3</li> <li class=\"no\">りすとの4</li> <li class=\"yes\" hoge=\"foo\">りすとの5</li> </ul> <pre> a b \tc\t </pre> <!-- comment --> <!-- comment[2] --> <div> <!--// comment[3] //--> </div> </body> "; $codec = array( 'UTF-8', 'EUC-JP', 'Shift_JIS', 'ISO-2022-JP', ); foreach ($codec as $charset) { // -- 入力HTMLの文字コードに制限があるのか $html = mb_convert_encoding(str_replace('<%CHARSET%>', $charset, $source), $charset, 'UTF-8'); $scraper = new HTML_Scraper($html); // -- metaタグの文字コードとか echo "-- metaタグの文字コードとか\n"; $results = $scraper->execute('//meta[@http-equiv="content-type"]'); foreach ($results as $result) { var_dump($result->getAttribute('content')); } // -- titleタグ echo "-- titleタグ\n"; $results = $scraper->execute('//title'); foreach ($results as $result) { var_dump($result->firstChild->data); } // -- 親子指定 echo "-- 親子指定\n"; $results = $scraper->execute('//ul/li[@class="non"]'); var_dump($results->length); // -- ヒットしない式の結果 echo "-- ヒットしない式の結果\n"; $results = $scraper->execute('//ol/li[@class="non"]'); var_dump($results->length); // -- 複合条件で抽出 echo "-- 複合条件で抽出\n"; $result = $scraper->find('li', array('class' => 'yes', 'hoge' => 'foo')); var_dump($result->firstChild->data); // -- CDATAセクションがどうなるか echo "-- CDATAセクションがどうなるか\n"; $results = $scraper->execute('//script'); var_dump($results->item(0)->firstChild->data); // ---- javascriptについてコメント内に書くやつは echo "---- javascriptについてコメント内に書くやつは\n"; var_dump($results->item(1)->firstChild->data); var_dump($results->item(1)->getAttribute('language')); // -- 抽出したノードをHTMLとして表示 echo "-- 抽出したノードをHTMLとして表示\n"; var_dump($results->item(0)); $dom = new DOMDocument(); $node = $dom->importNode($results->item(0), true); $dom->appendChild($node); var_dump($dom->saveHTML()); // -- 整形済みテキスト echo "-- 整形済みテキスト\n"; $result = $scraper->find('pre'); $result = str_replace("\t", '^^^^', $result->firstChild->data); $result = str_replace(' ', '_', $result); $result = str_replace("\n", '/', $result); var_dump($result); // -- コメントノード echo "-- コメントノード\n"; foreach ($scraper->findAll('//comment()') as $result) { var_dump($result->data); } } ?>
以下、実行結果(入力のHTMLをUTF-8とした場合のみ抜粋)。
-- metaタグの文字コードとか
string(24) "text/html; charset=UTF-8"
-- titleタグ
string(15) "たいと〜る"
-- 親子指定
int(1)
-- ヒットしない式の結果
int(0)
-- 複合条件で抽出
//li[@class="yes" and @hoge="foo"]
string(15) "りすとの5"
-- CDATAセクションがどうなるか
string(67) "
//<![CDATA[
document.write("<p>in cdata<" + "/p>");
//]]>
"
---- javascriptについてコメント内に書くやつは
string(3) "
"
string(10) "javascript"
-- 抽出したノードをHTMLとして表示
object(DOMElement)#12 (0) {
}
string(108) "<script type="text/javascript">
//<![CDATA[
document.write("<p>in cdata<" + "/p>");
//]]>
</script>
"
-- 整形済みテキスト
string(27) "/__a__/____b____/^^^^c^^^^/"
-- コメントノード
string(30) "//
alert('in comment');
//"
string(9) " comment "
string(12) "
comment[2]
"
string(16) "//
comment[3]
//"


コメントする