Perl の HTML::Parser モジュールの使い方

# この記事を書いた経緯

Perl の HTML::Parser に関して使い方を紹介している日本語記事が上手く見つけられなかったので書き残す。
HTML をパースするなら他にもいろいろあるかもしれないが、著者が自作のツールを公開する際に使っているレンタルサーバはこれ以外入っていなかったので必要だったのである。

また、オプションについては書いてない。そこについては別の記事として書くかもしれない。
この記事の内容は [HTML::Parser - HTML parser class - metacpan.org](https://metacpan.org/pod/HTML::Parser) 記載の内容を確認しながら書いている。もっと正確な情報とか書いてないことを確認するにはこちらから。
書いた時点でのモジュールのバージョンは Release 3.72 である。

# 挙動の概要

`parse` メソッドが実行されると **HTML を上から順に読み込み、何らかの要素を見つけるたびに「イベント」が発生し、parser はこの「イベント」に対応する「ハンドラ」を実行する** という挙動をする。そのため、このモジュールを使う際はどのイベントに対してどのハンドラを呼ぶのかを定義すればよい。

# 使い方例

## 解析される HTML

```html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>サンプルページ</title>
  </head>
  <body>
  <h1>サンプルページ</h1>
  <h2>誰が書いたの</h2>
  <p><span class="name">@Chrysanthemum94</span> が書いてます</p>
  <h2>hogehoge</h2>
  <p>fugafuga</p>
  <!-- コメントだよ！ -->
  <hr/>
  <p><a href="../index.html">戻る</a></p>
</body>
</html>
```

## perl

```perl
use HTML::Parser ();
use LWP::UserAgent;

my $browser = LWP::UserAgent->new;

# 架空の場所から上述の HTML を取得している
my $response = $browser->get("http://chrysanthemum94.example.com/help/index.html");
my $parse_target_html = $response->content;

my $parser = HTML::Parser->new(
  api_version => 3,
  start_h     => [\&when_open_tag_found, "self, tagname, attr"],
  end_h       => [\&when_close_tag_found, "self, tagname"],
  text_h      => [\&when_text_found, "self, text"],
  comment_h   => [\&when_comment_found, "self, text"]
);
$parser->parse($parse_target_html);

sub when_open_tag_found {
  my ($self, $tagname, $attr) = @_;
  print "OPEN $tagname\n";
}

sub when_close_tag_found {
  my ($self, $tagname) = @_;
  print "CLOSE $tagname\n";
}

sub when_text_found {
  my ($self, $text) = @_;
  print "TEXT $text\n";
}

sub when_comment_found {
  my ($self, $text) = @_;
  print "COMMENT $text\n";
}
```

## 実行結果

```
TEXT

OPEN html
TEXT

OPEN head
TEXT

OPEN meta
TEXT

OPEN title
TEXT サンプルページ
CLOSE title
TEXT

CLOSE head
TEXT

OPEN body
TEXT

OPEN h1
TEXT サンプルページ
CLOSE h1
TEXT

OPEN h2
TEXT 誰が書いたの
CLOSE h2
TEXT

OPEN p
OPEN span
TEXT @Chrysanthemum94
CLOSE span
TEXT  が書いてます
CLOSE p
TEXT

OPEN h2
TEXT hogehoge
CLOSE h2
TEXT

OPEN p
TEXT fugafuga
CLOSE p
TEXT

COMMENT <!-- コメントだよ！ -->
TEXT

OPEN hr/
TEXT

OPEN p
OPEN a
TEXT 戻る
CLOSE a
CLOSE p
TEXT

CLOSE body
TEXT

CLOSE html
```

## 何が起こっているのか

先述の通り HTML を先頭から順番に解析していく。最初の数手について以下に書き起こしてみる。

1. `<!DOCTYPE html>`を見つけ、`declaration`イベントが発火する。しかし、ハンドラを設定していないため特に何も起こらない
1. 改行を読み込んだことで`text`イベントが発火する。`text`イベントには`when_text_found`がハンドラとして設定されているため、これが実行される
1. `<html lang="ja">`が読み込まれ、`start`イベントが発火する。同イベントには`when_open_tag_found`がハンドラとして設定されているため、これが実行される
1. 改行とスペース2つが読み込まれ、`text`イベントが発火して`when_text_found`が実行される
1. `<head>`が読み込まれ`start`イベントに対応して`when_open_tag_found`が実行される
1. 改行とスペース4つが読み込まれ`text`イベントに対応する`when_text_found`が実行される
1. 同様に HTML の構成要素ごとにイベントが発生、対応するハンドラが実行される、が末尾まで繰り返される

# ハンドラと引数

## イベントに対するハンドラの定義

```perl
my $parser = HTML::Parser->new(
  api_version => 3,
  start_h     => [\&when_open_tag_found, "self, tagname, attr"],
  end_h       => [\&when_close_tag_found, "self, tagname"],
  text_h      => [\&when_text_found, "self, text"],
  comment_h   => [\&when_comment_found, "self, text"]
);
```

このように `XXXX_h` に対してハンドラへの参照とハンドラに渡す引数を記載する。
上述の `start_h     => [\&when_open_tag_found, "self, tagname, attr"]` であれば次のようになる。

* `start` イベントに対するハンドラの宣言
* `start` イベントが起こったら `when_open_tag_found` を `self, tagname, attr` の引数を渡して実行する

## イベント

次のようなイベントがある。

|イベント名称|説明|
|:----------|:----|
|start|HTMLの開始タグを見つけた際に発生するイベント|
|end|HTMLの終了タグを見つけた際に発生するイベント。なお、self-closing な開始タグ（`<img/>` 等）の場合はこのイベントは発火しない|
|text|HTML中のプレーンなテキストを見つけた際に発生するイベント|
|comment|HTMLのコメントアウトされた部分を見つけた際に発生するイベント|
|start_document|HTMLを読み込み始める前に発生するイベント|
|end_document|HTMLの解析を途中で外部から停止した場合に発生するイベント|
|process|HTML中に埋め込まれた処理命令を見つけた際に発生するイベント|
|declaration|DOCTYPE 宣言を見つけた際に発生するイベント|
|default|ハンドラが特に設定されていないイベントが発生した際に発生するイベント|

## ハンドラに渡す引数

|引数名|説明|
|:---|:---|
|self|parserそのものが格納される|
|event|発生したイベントが格納される。start 等が入る|
|tagname|start や end のイベントのハンドラに対して与えることができる。タグの名称が格納される|
|tag|start や end のイベントのハンドラに対して与えることができる。タグの名称が格納されるが、end 等の / が含まれる|
|text|text のイベントのハンドラに対して与えることができる。タグとタグの間の文字列が格納される|
|dtext|text と同じだが、デコードされたものが格納される。例えば text の値が `&lt;&gt;` になった場合 `<>` が格納される|
|is_cdata|値が CDATA ないしリテラルである場合か否かが格納される。この値が False であるならば一般には text ではなく dtext を使って値を処理する方が良い|
|attr|start のイベントのハンドラに対して与えることができる。その要素に与えられた属性がハッシュで格納される|
|@attr|start のイベントのハンドラに対して与えることができる。その要素に与えられた属性の名称と、その属性の値が格納された配列が格納される。偶数番目の要素（0, 2, 4...）に名称、奇数番目の要素（1, 3, 5...）に値が格納される|
|attrseq|start のイベントのハンドラに対して与えることができる。その要素に与えられた属性の名前が配列で格納される|
|length|そのイベントで取得した HTML の構成要素の長さが格納される。単位は byte|
|line|そのイベントが HTML の何行目で発生したのかが格納される|
|column|そのイベントが行の何文字目で発生したのかが格納される|
|offset|そのイベントが HTML の先頭から何 byte 目で発生したのかが格納される|
|offset_end|そのイベントが HTML の先頭から何 byte 目で終了したのかが格納される|
|tokens|そのイベントの元になった HTML の構成要素を分解したものが格納される。各イベントごとに何が入るのかはまちまち。例えば start であれば要素名に続いて属性名、属性値、属性名、属性値……と続く|
|token0|tokens の先頭の要素が格納される|
|tokenpos|HTML の構成要素内において tokens が出現した位置とその token の長さが格納された配列が格納される。偶数番目の要素（0, 2, 4...）に位置、奇数番目の要素（1, 3, 5...）に長さが格納される|
|skipped_text|前回ハンドラが処理したイベントから現在ハンドラが処理しているイベントの間に処理されなかった HTML の構成要素が格納される|
|undef|未定義が格納される。複数のイベントに対して単一のハンドラを渡す際に便利|
|'任意の文字列'|シングルクオートないしダブルクオートで囲った文字列が格納される。複数のイベントに対して単一のハンドラを渡す際に便利|

# メソッド群

## HTML::Parser->new

新しい Parser を作るのに使う。引数としてイベントと対応するハンドラが格納されたハッシュを与える。
また、引数のハッシュに `api_version` に `3` の値が入ったものを与えない場合はバージョン2との互換性が有効になる。これを使いたくない場合は`api_version` に `3` の値を入れた状態で引数を渡してやる必要がある。

## parse

引数に渡した文字列を html として解析する。解析の際にはハンドラなどの設定を参照しながら実施される。
このモジュールを使うならまず呼び出すことになるであろうメソッド。

## parse_file

引数としてファイルを渡したい場合は `parse` ではなくこちらを利用する。引数にはファイルへのパスないしファイルハンドラを渡すことができる。
指定されたファイルが開けなかった場合はこのメソッドは未定義の値を返す。また、 `$!` にファイルを開けなかった理由が記載される。
問題なくファイルが開けていればこのメソッドは自分自身（parser）を返す。

## eof

`parse` ないし `parse_file` を中断させる。このメソッドは自分自身（parser）を返す。
ハンドラの外で呼んだ場合は `end_document` イベントが発火される。まだ未処理の html が残っている場合は残った html で text イベントが発火する。
このメソッドをハンドラの中で呼んだ場合は発火されない。