Perl で HTML をパースするモジュール ― 2021年12月19日 23時40分
この記事は HTML アドベントカレンダーの 19 日目の分、兼 Perl アドベントカレンダーの 19 日目の分です。
Perl で HTML をパースするモジュールはいくつもあります。
HTML::Parser
そのままの名前ですね。HTML コードをパースしていき、開始タグ、終了タグ、テキストなどを認識するとそれをイベントとして知らせてくれる、プッシュ型のパーサーです。
HTML の要素の内容モデルや、ある要素のタグが省略可能かといった知識は持っていません。あくまでもタグやテキストなどの出現を知らせるだけで、文書木を構築するわけではないからです。
逐次的なパースに対応しています。HTML 文書全体を表すコードを一気に入力として与えなくてもよく、HTTP 通信中に受け取った分からパースしていくといったことが可能です。
Web 製作者の意図を汲み取ろうと努めており、「壊れた」HTML コードでもパースできます。ただし、現在の HTML 標準 (いわゆる HTML5) のパース規則には対応しておらず、現在の主要 Web ブラウザのパース結果と異なる結果になることがあります。
HTML::PullParser
HTML::Parser
をプル型のパーサーとして利用できるようにしたものです。HTML::Parser
と同じ HTML-Parser ディストリビューションに含まれています (HTML::Parser
をインストールしようとすると HTML::PullParser
もインストールされます)。
HTML::TokeParser
HTML::PullParser
を拡張し、開始タグだけを取り出す、テキストだけを取り出すといったことを簡単にできるようにしたものです。HTML::Parser
と同じ HTML-Parser ディストリビューションに含まれています。
HTML::TreeBuilder
HTML をパースし、文書木を構築してくれます。DOM にアクセスするのと似た感覚で HTML 文書を扱えます。内部的なパースには HTML::Parser
を使っています。
HTML の要素や内容モデルに関しては HTML 4 相当の知識しかないため、現在の HTML 標準に従った HTML コードを期待通りパースできないことがあります。ignore_unknown
オプションを無効にしないと HTML5 で追加された要素が無視される (パース結果の文書木に現れない) 点は、特に注意が必要です。
HTML::TreeBuilder::XML
HTML をパースし、文書木を構築してくれます。内部的なパースには libxml2 ライブラリを使っています。
一部 HTML::TreeBuilder
と同名のメソッドを持ちますが、完全に互換性があるわけではありません。
HTML::HTML5::Parser
HTML をパースし、文書木を構築してくれます。XS を使わず Pure-Perl で書かれています。
2013 年で更新が止まっているため、現在の HTML 標準には一部追従していない……と思っていたのですが、2021 年 9 月に更新されていました (この記事を書いている途中で気づきました)。現在の HTML 標準にどこまで適合しているかは未確認です。
HTML5::DOM
HTML をパースし、文書木を構築してくれます。内部的なパースには MyHTML ライブラリを使っています。
MyHTML ライブラリは開発が停止しており、後継の Lexbor ライブラリに引き継がれています。
終わりに
現在の HTML 標準に最も近いのは HTML5::DOM
だと思っていたのですが、HTML::HTML5::Parser
が更新されていくのなら今後はどうなるかわかりません。
Perl の wantarray 関数で返り値の扱いを確認する ― 2020年12月22日 22時28分
こんにちは、nanto_vi です。この記事は Perl Advent Calendar の 22 日目です。
Perl の特徴のひとつに関数が多値を返せるというのがあります。Go 言語と同様に、処理の結果とエラーの値を同時に返すことができます。
sub do_something {
...
return (undef, $error) if $something_wrong;
return ($result, undef);
}
my ($result, $error) = do_something();
上述の do_something
関数はリストを返し、その第 1 要素に処理結果を、第 2 要素にエラーの値を含んでいます。ところが、ここでうっかり、
my $result = do_something();
とリストを使わずに返り値を受け取ってしまうと、変数 $result
にエラーの値が代入されてしまいます。(リストをスカラコンテキストで評価した場合、そのリストの末尾要素が返ります。)
このような事態を防ぐために、wantarray
関数を使って呼び出し側がリストコンテキストで返り値を受け取っているか確かめられます。
use Carp qw(croak);
sub do_something {
croak 'Must handle error' unless wantarray;
...
return (undef, $error) if $something_wrong;
return ($result, undef);
}
my $result = do_something();
# => 'Must handle error' 例外が投げられる。
ここでエラーの値だけを受け取りたいときは、次のようにリストの要素として undef
を書けます。
my (undef, $error) = do_something();
空リストに代入することで、返り値をすべて無視することもできます。
() = do_something();
また、defined
関数と組み合わせて呼び出し側が返り値を受け取っているか確かめられます。
sub do_another_thing {
croak 'Must handle error' unless defined wantarray;
return $error if $something_wrong;
return undef;
}
do_another_thing();
# => 'Must handle error' 例外が投げられる。
my $error = do_another_thing();
# => 例外が投げられず、エラーの値が返る。
この場合、返り値を無視するには scalar
関数が使えます。
scalar do_another_thing();
Perl の Test2::V0 でオブジェクトの基底クラスを確認する ― 2020年12月18日 00時37分
こんにちは、nanto_vi です。この記事は Perl Advent Calendar の 18 日目です。
近年、Perl でテストの記述によく使われているモジュールが Test2::V0
です。Test2::V0
について詳しくは「第51回 Test2で変わるモダンなテスト―拡張性を持ったテスティングフレームワークとTest2::V0の使い方(1):Perl Hackers Hub|gihyo.jp … 技術評論社」を参照してください。
Test2::V0
を使い、あるオブジェクトがあるクラスのインスタンスであることを確かめたいときはどうすればよいでしょうか? 例えば、オブジェクトが URI
クラスのインスタンスであることを確かめようとして、次のように書くと……
# Bad
use Test2::V0;
use URI;
my $uri = URI->new('https://www.example.com/');
is $uri, object {
prop blessed => 'URI';
};
このテストは失敗します! prop blessed
の値 (Scalar::Util::blessed($uri)
の返り値) は URI::https
であり、URI
ではないからです。(URI::https
クラスは URI
クラスを継承しています。)
Test2::V0
0.000138 までは、次のように書く必要がありました。
# Good
is $uri, object {
call [ isa => 'URI' ] => T;
};
$uri->isa('URI')
の返り値が真ならよいというわけです。でもパッと見ただけでは何をやっているのかわかりづらいですよね。
Test2::V0
0.000139 (日本時間で 2020 年 12 月 16 日、つまりこれを書いている 2 日前のリリースです) では、次のように書けるようになりました!
# Good
is $uri, object {
prop isa => 'URI';
};
prop isa
を使うことで、オブジェクトの基底クラスの確認をわかりやすく書けます。また、check_isa
関数も追加されました。
これらの機能は Introduce object inehritance chekcs by nanto · Pull Request #230 · Test-More/Test2-Suite で導入されました。変更をレビューし取り込んでくださった Chad Granum 氏に感謝します。
DBIx::Handler と mysql_enable_utf8 と utf8mb4 ― 2013年06月10日 21時52分
Perl で MySQL サーバーに接続するとき、DBIx::Handler
を使っています。
my $handler = DBIx::Handler->new(
$dsn, $username, $password,
{
RaiseError => 1,
RootClass => 'My::DBI',
mysql_enable_utf8 => 1,
},
);
しかし、これだと "𠮷" (U+20BB7
、いわゆる「つちよし」) など BMP 外の文字を保存しようとしたとき、テーブルの文字コードが utf8mb4
であっても "????" (疑問符 4 文字) に化けてしまいます。
ググったところ、mysql_enable_utf8 => 1
を指定した時点で DBD::mysql
が接続時の文字コードを utf8
にしてしまうのが問題だそうです。(cf. おそらくはそれさえも平凡な日々: DBD::mysqlでmysql_enable_utf8しつつutf8mb4使いたいとき)
接続時に文字コードを utf8mb4
に指定しなおせばいいとのことですが、DBIx::Handler
ではどうするのだろうとソースコードを眺めていたところ、new
メソッドの第 5 引数の存在を知りました。
my $handler = DBIx::Handler->new(
$dsn, $username, $password,
{
RaiseError => 1,
RootClass => 'My::DBI',
mysql_enable_utf8 => 1,
},
{
on_connect_do => ['SET NAMES utf8mb4'],
},
);
これで BMP 外の文字も文字化けせず保存・取得できるようになったのですが、この第 5 引数は (DBIx::Handler
0.07 時点では) 文書化されていないので、使っていいものか心配です。
Callbacks
を使っても期待通り動作するようなので、そちらのほうがよかったりするのでしょうか。
my $handler = DBIx::Handler->new(
$dsn, $username, $password,
{
RaiseError => 1,
RootClass => 'My::DBI',
mysql_enable_utf8 => 1,
Callbacks => {
connected => sub {
$_[0]->do('SET NAMES utf8mb4');
return;
},
},
},
);
(SET NAMES
をつかってはいけないという話もありますが、追いきれていません。)
Template::Plugin::JSON::Escape 公開 ― 2011年06月15日 20時57分
Template::Plugin::JSON::Escape
という Perl モジュールを公開しました。
Template Toolkit を使って HTML を出力するとき、HTML 中に JSON を埋め込みたいことがあります。そのためのモジュールとして Template::Plugin::JSON
が存在するのですが、これは小なり記号 (<
) などの文字を素通しするため、JSON の内容によっては不正な HTML が出力されてしまいます。
Template::Plugin::JSON::Escape
は一部記号を \uxxxx
の形式にエスケープするので、JSON の内容にかかわらず出力する HTML を妥当に保てます (もちろん、JSON 出力部以外が妥当な HTML を出力するのであれば、です。なお、SGML のコメント内に JSON を埋め込む場合はこの限りではありません)。副次作用として、XSS の防止にも一定の効果を期待できます。
また、Template::Plugin::JSON
0.06 は Moose
を使っていますが、私が普段 Perl を使っている環境には Moose
が入っていません。そのためだけに Moose
を導入するのは難しいので、Moose
を使用しないモジュールが求められていたというのもあります。
最近のコメント