ziguzagu.org

XML::LibXML で HTML の一部をパースする

XML::LibXML::parse_html_string で HTML の一部、たとえばブログ記事の本文のみとか、をパースしようとして無駄に苦戦してしまったのでメモ。

以下のような、DOCTYPE 宣言もなければ html/body 要素もないけど、ちゃんと(?)HTMLの一部ではあるものを XML::LibXML でパース、ごにょったあと出力したい。

#!/usr/bin/perl
use strict;
use warnings;
use XML::LibXML;

my $html =<<HTML;
<div class="entry">
<p>test!!</p>
</div>
HTML

my $parser = XML::LibXML->new;
my $doc = $parser->parse_html_string($html);
print $doc->toStringHTML;

結果は、以下のとおり。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div class="entry">
<p>test!!</p>
</div></body></html>

読み込んで toStringHTML すると DOCTYPE宣言、/html/body 要素がくっつけられる。toString の場合はさらに先頭に xml 宣言がくっついてくる。DOCTYPE が HTML 4.0 なのは libxml の HTML パーサーが 4.0 対応がベース(?)になっているとかいないとかの影響かな。

んで、この HTML はあくまでも全体の「一部」なので、このままだと埋め込むときに困る。なので削除する。

#!/usr/bin/perl
use strict;
use warnings;
use XML::LibXML;

my $html =<<HTML;
<div class="entry">
<p>test!!</p>
</div>
HTML

my $parser = XML::LibXML->new;
my $doc = $parser->parse_html_string($html);
my $out = $doc->toStringHTML;
$out =~ s{^[\S\s]*?<body>\s*}{};
$out =~ s{\s*</body>\s*</html>\s*$}{};
print $out;

結果。

<div class="entry">
<p>test!!</p>
</div>

めでたしめでたし。とおもったらそうではなかった。 日本語(とかマルチバイトななにか)がはいってると、それらが数値参照になってしまう。

my $html =<<HTML;
<div class="entry">
<p>test!!</p>
<p>てすと</p>
</div>
HTML

結果。

<div class="entry">
<p>test!!</p>
<p>&#x3066;&#x3059;&#x3068;</p>
</div>

ぬーん。うれしくない。

ぐぐったら、富田さんの HTML::MobileJp::Filter の perldoc が見つかる。

CONFIG AND DEFAULT VALUES

base_dir                => '',
xml_declaration_replace => 1,
xml_declaration         => <<'END'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/1.0) 1.0//EN" "i-xhtml_4ja_10.dtd">
END

XML 宣言や DTD がないと文字が全部実体参照になったりうまく parse できないので ヘッダを付け替えることで HTML::DoCoMoCSS の中の XML::libXML に指示をしています。

ほうほう。やってみる。

my $html =<<HTML;
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/1.0) 1.0//EN" "i-xhtm\
l_4ja_10.dtd">
<html><body>
<div class="entry">
<p>test!!</p>
<p>てすと</p>
</div>
</body></html>
HTML

結果。

<div class="entry">
<p>test!!</p>
<p>&#x3066;&#x3059;&#x3068;</p>
</div>

ずーん。まだだめ。。。それで、あーだこーだやること数十分。meta つけてみた。

my $html =<<HTML;
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/1.0) 1.0//EN" "i-xhtm\
l_4ja_10.dtd">
<html>
<head><meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /></head>
<body>
<div class="entry">
<p>test!!</p>
<p>てすと</p>
</div>
</body></html>
HTML

結果。

<div class="entry">
<p>test!!</p>
<p>てすと</p>
</div>

きたーーー。

で、もすこしためしてみると、どうも Content-Type で charset がわかればいいぽい。DOCTYPE なしでもいけた(でも toStringHTML で DOCTYPE くっついてくる)。

けつのろん。

XML::LibXML::parse_html_string (に限らず parse_html_fh, parse_html_file もかな?調べてない)で、マルチバイトな文字を含むHTML の一部のみを無傷でパースするには、できるにはできるけど、

  • charset 指定ありの Content-Type が指定された meta をつけてあげる。
  • toStringHTML, toString に余計なものくっついてくるので削る。

とかする。無駄に長いことかきましたがそんだけ :) あ、あと、HTML::Entities::Numbered を事前にやっとくと吉。← やんなくても大丈夫。

まとめ。

#!/usr/bin/perl
use strict;
use warnings;
use XML::LibXML;

my $HEADER = q{<html><head><meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /></head><body>}
my $FOOTER = q{</body></html>}

my $html = <<HTML;
<div class="entry">
<p>test!!</p>
<p>テスト</p>
</div>
HTML

my $parser = XML::LibXML->new;
my $doc = $parser->parse_html_string($HEADER . $html . $FOOTER);
my $out = $doc->toStringHTML;
$out =~ s{^[\S\s]*?<body>\s*}{};
$out =~ s{\s*</body>\s*</html>\s*$}{};
print $out;

結果。

<div class="entry">
<p>test!!</p>
<p>テスト</p>
</div>

(これを勝手にやってくれる、XML::LibXML::parse_html_XXX の wrapper あるとよいかな)

*追記*:はてぶでフォローいただいた。http://b.hatena.ne.jp/kits/20090405#bookmark-12818252 parse_html_string に encoding オプションを渡せる。なるほどー(perldoc みてたけど完全に見落としていたなぁ…)

追記2: encoding オプション渡すと某環境でえらったのでしらべてみると、parse_html_string に encoding の option とか渡せるようになったのは、XML::LibXML 1.62 からだそう。以下 Changes より。

1.62

  • if compiled against libxml2 >= 2.6.27, new parse_html_* implementation is used allowing encoding and other options to be passed to the parser

追記3: libxml 2.6.31 + XML::LibXML 1.69 の組み合わせで encoding 渡しても、meta ないと実体参照になるなぁ。謎…。

追記4: HTML::Entities::Numbered::name2decimal は、やらなくても ♥ とかで parse エラーになったりはしなかった(libxml 2.6.31 / XML::LibXML 1.69)。