PHPのDOMDocumentの文字化けなど

PHPのDOMDocumentまわりには、文字化けと数値文字参照への強制変換という問題があるようです。

文字化けについては、DOMDocument->loadHTMLのマニュアルにコメントがありました。

Pay attention when loading html that has a different charset than iso-8859-1. Since this method does not actively try to figure out what the html you are trying to load is encoded in (like most browsers do), you have to specify it in the html head. If, for instance, your html is in utf-8, make sure you have a meta tag in the html's head section:

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>

If you do not specify the charset like this, all high-ascii bytes will be html-encoded. It is not enough to set the dom document you are loading the html in to UTF-8.
PHP: DOMDocument->loadHTML() - Manual

loadHTMLするHTMLについては、metaタグで文字コードを指定していれば正しいエンコーディングで処理してくれるようです。metaタグで文字コードを指定していないHTMLについて(一部を切り抜いたHTMLなど)は補完などの処理が必要になるのでしょうか?

次に、数値文字参照への変換ですが、これをどう防げば良いのかは不明です。ひとまず、数値文字参照を文字に戻す事はできるので、以前書いた(マニュアルからコピった)適当な関数に渡して対処してみました。元々数値文字参照だったものまで戻ってしまうのでどうにかしたいところです。
(libxml2のバージョンの問題と言う情報もありましたが、手元ではパッケージから入れているせいか駄目みたいです。)

ごまかしながら使う事は出来るようですが、どうも微妙な印象です。マニュアルから"(No version information available, might be only in CVS)"が消えるまでは使うな(知らんよ)、という事でしょうか?

テスト環境:

  • OSX, PHP5.2.0, libxml2.6.27(MacPorts)
  • etch, PHP5.2.0-8+etch1, 2.6.27.dfsg-1(aptitude)

以下、テストに利用したコードです。DOMXPathを試している中で遭遇した問題だったので、かなり意味不明なコードですが気にしないでください。

<?php
 
class HTML_XPath
{
 
    private $_context_list;
 
    public function __construct($source)
    {
        $this->_context_list = array();
        $source_list = array();
        if (!is_array($source)) {
            $source_list[] = $source;
        } else {
            $source_list = $source;
        }
        foreach ($source_list as $s) {
            $dom = new DOMDocument();
            if (is_string($s)) {
                $dom->loadHTML($s);
            } else if (get_class($s) == 'DOMDocument') {
                $dom = $s;
            }
            $this->_context_list[] = array(
                                           'dom'   => $dom,
                                           'xpath' => new DOMXPath($dom),
                                           );
        }
    }
 
    private function _nodeListToDomList($node_list)
    {
        $dom_list = array();
        foreach ($node_list as $node) {
            $dom = new DOMDocument();
            $dom->appendChild($dom->importNode($node, true));
            $dom_list[] = $dom;
        }
        return $dom_list;
    }
 
    public function find($query)
    {
        $new_context_list = array();
        foreach ($this->_context_list as $context) {
            $results = $context['xpath']->query($query);
            if ($results->length > 0) {
                $new_context_list = array_merge($new_context_list,
                                                $this->_nodeListToDomList($results));
            }
        }
        return new HTML_XPath($new_context_list);
    }
 
    public function length() {
        return count($this->_context_list);
    }
 
    private function _numentToChar($string)
    {
        $excluded_hex = $string;
        if (preg_match("/&#[xX][0-9a-zA-Z]{2,8};/", $string)) {
            // 16 進数表現は 10 進数に変換
            $excluded_hex = preg_replace("/&#[xX]([0-9a-zA-Z]{2,8});/e",
                                         "'&#'.hexdec('$1').';'", $string);
        }
        return mb_decode_numericentity($excluded_hex,
                                       array(0x0, 0x10000, 0, 0xfffff),
                                       "UTF-8");
    }
 
    public function item($index, $to_html = true) {
        if (0 <= $index and $index < count($this->_context_list)) {
            $dom = $this->_context_list[$index]['dom'];
            if ($to_html) {
                //return $dom->saveHTML();
                return $this->_numentToChar($dom->saveHTML());
            } else {
                return $dom;
            }
        } else {
            return null;
        }
    }
 
}
 
$html = '
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<title>タイトル</title>
</head>
<body>
<a name="anc1"></a>
<div class="div_1">
  <div class="div_div_1">
    <p class="p_1">anc1の1つめ</p>
  </div>
</div>
<a name="anc2"></a>
<div class="div_1">
  <div class="div_div_1">
    <p class="p_1">anc2の1つめ</p>
  </div>
</div>
<div class="div_1">
  <div class="div_div_1">
    <p class="p_1">anc2の2つめ</p>
  </div>
</div>
</body>
';
 
$scraper = new HTML_XPath($html);
$results = $scraper->find('//div[@class="div_1"]')->find('//div[@class="div_div_1"]');
echo "==> TEST01:\n";
for ($i = 0; $i < $results->length(); $i++) {
    echo "[No.$i]\n";
    echo $results->item($i) . "\n";
}
 
echo "==> TEST02:\n";
$scraper = new HTML_XPath($html);
$results = $scraper->find('//a[@name="anc2"]/following-sibling::div[@class="div_1"]')
    ->find('//div[@class="div_div_1"]');
for ($i = 0; $i < $results->length(); $i++) {
    echo "[No.$i]\n";
    echo $results->item($i) . "\n";
}
 
echo "==> TEST03:\n";
$scraper = new HTML_XPath($html);
$results = $scraper->find('//div[@class="div_1" and position()=last()]');
for ($i = 0; $i < $results->length(); $i++) {
    echo "[No.$i]\n";
    echo $results->item($i) . "\n";
}
 
?>
 

実行結果。

==> TEST01:
[No.0]
<div class="div_div_1">
    <p class="p_1">anc1の1つめ</p>
  </div>

[No.1]
<div class="div_div_1">
    <p class="p_1">anc2の1つめ</p>
  </div>

[No.2]
<div class="div_div_1">
    <p class="p_1">anc2の2つめ</p>
  </div>

==> TEST02:
[No.0]
<div class="div_div_1">
    <p class="p_1">anc2の1つめ</p>
  </div>

[No.1]
<div class="div_div_1">
    <p class="p_1">anc2の2つめ</p>
  </div>

==> TEST03:
[No.0]
<div class="div_1">
  <div class="div_div_1">
    <p class="p_1">anc2の2つめ</p>
  </div>
</div>

プロフィール

このブログ記事について

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

ひとつ前のブログ記事は「PHPでXPath」です。

次のブログ記事は「pythonutils.odict.OrederedDictいいかも」です。

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