PHPでXPath

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]
//"
プロフィール

このブログ記事について

このページは、koshigoeが2007年4月18日 01:37に書いたブログ記事です。

ひとつ前のブログ記事は「PythonPerlを触ってみた」です。

次のブログ記事は「PHPのDOMDocumentの文字化けなど」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。