図解 HTMLParser

# はじめに
HTMLParserを使う機会があったのですが、どれも使ってみただけの紹介で、概念を解説したような記事がなかったのでここで改めて紹介しようと思います。（優秀な方ならドキュメント等読めば理解できるのでしょうが、最初はそれなりに理解に時間がかかったのでここでまとめてみます。）
参考：[20.2 html.parser](https://docs.python.jp/3.6/library/html.parser.html)

※ソースを読んでいるわけではなく勝手なイメージで説明しています。もし挙動が間違っているなどがあればご指摘お願いいたします。

# HTMLParserクラス

このクラスを俯瞰してみるために次のコードを張り付けておきます。

```Python:MyParser.py
class MyParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        pass  # タグが始まった時に呼ばれる
    def handle_endtag(self, tag):
        pass  # タグが終わった時に呼ばれる
    def handle_data(self, data):
        pass  # データを呼び出すときに呼ばれる
```

基本的な構造の理解としてはこれだけで十分です。HTMLはタグと要素で構成されているのはご存じてあると想定して話を進めていきます。

```Python
parser = MyParser()  # HTMLParserを改良して作った自分用のパーサー
parser.feed(html)  # 何らかのhtml形式のデータを引数としてパーサーを実行。
```
このようにfeedメソッドを呼び出すことでパースが始まります。

feedメソッドが呼び出されたときにどのような呼び出しがされるのかを図示しながら説明します。

```HTML
<HTML>
  <HEAD>
    <TITLE>Greeting</TITLE>
  </HEAD>
  <BODY class="mybody">
    <H1>Hello</H1>
    <P>World</P>
  </BODY>
</HTML>
```

![開始タグの発見](https://qiita-image-store.s3.amazonaws.com/0/202772/7df6a0be-7811-fc99-0326-1cf3e41c83ac.png)
まず、上から順に捜査していき、開始タグがあることに気づきます。

![handle_starttagの呼び出し](https://qiita-image-store.s3.amazonaws.com/0/202772/93542466-bd65-e4fa-c539-a6b37a5c2d99.png)
次に、HTMLParserのhandle_starttagメソッドが呼び出されます。

![tag引数が渡される](https://qiita-image-store.s3.amazonaws.com/0/202772/6a1a8a26-b0c4-7354-4ebb-f0dfd4e8c8eb.png)
このとき、タグ名を小文字に変換したものが引数tagに渡されます。

![attr引数が渡される](https://qiita-image-store.s3.amazonaws.com/0/202772/f1b3b755-2580-930c-11cc-9ebcf2760376.png)
同時に、属性についても渡されます。

![head開始タグでのやりとり](https://qiita-image-store.s3.amazonaws.com/0/202772/dc1d2737-a7f2-d96d-b7bf-3b2faed08b18.png)
同じように、順番にこのような操作が繰り返されます。

![handle_data](https://qiita-image-store.s3.amazonaws.com/0/202772/223907f4-80d4-a161-2840-329e54fef255.png)
handle_dataメソッドはこのように要素に出会ったときに必ず呼び出されます。

![handle_endtag](https://qiita-image-store.s3.amazonaws.com/0/202772/a1c31fc2-46cd-e0dc-c970-e15ed3686c51.png)
handle_endtagメソッドは終了タグに出会ったときに呼び出されます。このときはattr引数は必要ない（なぜなら開始タグにそれが書かれているから）ので用意されていないことに注意してください。

![属性があるときの値の内容](https://qiita-image-store.s3.amazonaws.com/0/202772/88c01a14-132f-7778-e973-5d1427b85570.png)
属性があるときは空リストではなく、このように属性名,属性値,属性名,属性値,...となるようなリストとして渡されます。

#我々は何をすればいいのか
HTMLParserの挙動がわかったところで、早速このパーサーを使ってスクレイピングをしてみましょう。（継承、オーバーライドが分かっている前提で説明します。）

handle_starttagメソッドは開始タグがあったときに呼ばれますから、そのタグの中にこれから入っていくことを覚えておくための処理を書くことができます。例えば、

```HTML
<p>
  <b>hutomoji</b>
</p>
```
このように太文字になっている部分だけを取り出したいときに、handle_starttagメソッド中に

```Python
def handle_starttag(self, tag, attr):
  if tag == "p":
    self.p = True
```
と書いてやることで、今`<p>`の後にいますよということを覚えておくことができます。

```Python
def handle_starttag(self, tag, attr):
  if tag == "p":
    self.p = True
  if self.p and tag == "b":
    self.pb = True    
```
さらに、こう加えると、pタグの中のbを発見したときに`self.pb`がTrueになります。このような状態で要素を読み込みましょう。

```Python
def handle_data(self, data):
  if self.pb:
    self.pb_data = data
```

こうすることで、目標のデータを取得することができました。
めでたしめでたし、ではなくて、

```Python
def handle_end(self, tag):
  if self.pb and tag == "b":
    self.pb = False
  if self.p and tag == "p":
    self.p = False
```

としてあげることで、タグから抜けたときにちゃんと抜けているよということを覚えさせてあげることが必要です。そうでないと、`self.pb`フラグがこの後ずっとTrueになり、HTMLファイルの終端までずっと`self.data`を書き換えられてしまいます。

実際に作るときのコツとしては、まずスクレイピングしたいページのHTMLコードを眺めながらどうやって目標のデータのところまで侵入していけばいいのかを考えます。

注意として、例のように同じような構造がページにいくつかあることがあります。

```HTML:example.html
<h1 class="a1">Article1</h1>
<p>It's a fine day</p>

<h1 class="a2">Article2</h1>
<p>People open the window</p>
```

このときは、属性名などを使ってうまく判別するかカウンタを使って同じ構造が何回出現したときに必要なデータが出てくるかを判別すればよいです。

他にもいろいろと工夫できるところがあると思うので、いろいろ試して面白いやり方があったら教えてください。

少しばかりですが機能がまだまだありますので公式docを参照してみください。
https://docs.python.jp/3.6/library/html.parser.html