スポンサーリンク

コードが書ける!数式が書ける!AAが書ける!スタンプが貼れる!

無料の匿名掲示板型SNS「このはちゃんねる

新規会員募集中!

【Python】縁の下の力持ち、html.parserでHTMLをパースする

243, 2019-11-14

目次

html.parserってなんぞ

html.parserとはPythonで使えるHTMLをパースするためのライブラリです。
標準ライブラリなのでPythonの実行環境があればそれだけで使うことが出来ます。

昨今のスクレイピングなどで行うHTMLのパースなどでは外部ライブラリであるBeautifulSoup4を使うことが多いと思います。
html.parserはそのBeautifulSoup4内部で使っているパーサーです。
BeautifulSoup4などのライブラリを制作したい場合や、外部ライブラリに依存したくないケースなどで需要がありそうなパーサーです。

簡単な使い方

BeautifulSoup4などに慣れた人にはhtml.parserのパースは斬新に映るかもしれません。
html.parserではHTMLParserというパース用のクラスを利用します。
このHTMLParserをインポートするには↓のようにします。

from html.parser import HTMLParser

あとはこのHTMLParserを継承してパース用の独自クラスを作ります。
例えば↓のようにです(MyHTMLParserという独自クラスを定義)。

from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):
    pass

これでMyHTMLParserHTMLParserの機能を使うことが出来るようになりました。
HTMLParserのパースを実行するにはfeedメソッドを使います。

from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):
    pass

parser = MyHTMLParser()
parser.feed('<html><head><title>本日は晴天なり</title></head></html>')

feedメソッドの第1引数にHTMLの文字列を渡すとパーサーは解析を開始します。
HTMLParserは解析をはじめると、解析中のタグに対して対応したメソッドを呼び出します。
このメソッドをオーバーライドすることで独自のパースを実装することが出来ます。

たとえばhandle_starttagというメソッドはタグの解析が開始されたときに呼ばれます。
よって↓のようにMyHTMLParserを定義することでタグ名の解析をハンドリングすることが出来ます。

class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print(f'解析中のタグは"{tag}"です')

また、handle_endtagというメソッドをオーバーライドすればタグの解析の終了をハンドリングすることが出来ます。

class MyHTMLParser(HTMLParser):
    def handle_endtag(self, tag):
        print(f'解析したタグは"{tag}"です')

タグ内のテキストを取得したい場合はhandle_dataメソッドをハンドリングします。

class MyHTMLParser(HTMLParser):
    def handle_data(self, data):
        print(f'解析中のテキストは"{data}"です')

これらを含めたサンプルコードは↓のようになります。

from html.parser import HTMLParser

class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print(f'解析中のタグは"{tag}"です')

    def handle_endtag(self, tag):
        print(f'解析したタグは"{tag}"です')

    def handle_data(self, data):
        print(f'解析中のテキストは"{data}"です')

parser = MyHTMLParser()
parser.feed('<html><head><title>本日は晴天なり</title></head></html>')

↑のコードを実行すると↓のような結果になります。

解析中のタグは"html"です
解析中のタグは"head"です
解析中のタグは"title"です
解析中のテキストは"本日は晴天なり"です
解析したタグは"title"です
解析したタグは"head"です
解析したタグは"html"です

解析手法、その具体例

このhtml.parserは非常によく動作します。これがあればアイデア次第で何でもできそうです。
たとえば特定のタグのテキストを取得したいとなったとします。
今回は<h1><h2>などのタグのテキストを取得してみたいと思います。
そのためには以下のようにイベントドリブンで要素を取得する戦略が考えられます。

from html.parser import HTMLParser
import re

class GrepHeadlines(HTMLParser):
    """
    <h1>系のタグの内容を取得するパーサー
    """

    def __init__(self):
        super().__init__()
        self.found_headline = False # <h1>系のタグが見つかったらTrueになる
        self.headline_texts = [] # <h1>系タグの中身

    def handle_starttag(self, tag, attrs):
        if re.match('^h[1-9]$', tag):
            self.found_headline = True

    def handle_data(self, data):
        if self.found_headline:
            self.headline_texts.append(data)
            self.found_headline = False

parser = GrepHeadlines()
parser.feed('''
<h1>本日は</h1>
<h2>晴天なり</h2>
<h3>あっぱれ!</h3>
''')

print(parser.headline_texts)

↑のコードを実行すると↓のような結果になります。

['本日は', '晴天なり', 'あっぱれ!']

このGrepHeadlinesパーサーではフラグを管理して特定のタグのテキストを取得するようにしています。
self.found_headlineTrueのとき、現在パースしているタグは<h1> ~ <h9>のどれかです。
handle_dataではself.found_headlineを監視して、このフラグがTrueになっていたら引数のdataをリストに保存していきます。そして保存が完了したら律儀にフラグを元に戻すようにします。

属性のパース

先ほどのGrepTitlesパーサーを改造して、特定のクラス名を持つタグのテキストだけを保存するように変更してみたいと思います。
たとえば<h1> ~ <h9>などのタグがgrep-meというクラスを持っていたら、そのタグは保存対象です。
クラスの設計は色々考えられますが、今回はクラス名の指定はfeedメソッドに指定することにします。

from html.parser import HTMLParser
import re

class GrepHeadlines(HTMLParser):
    """
    <h1>系のタグの内容を取得するパーサー
    """

    def __init__(self):
        super().__init__()
        self.found_headline = False # <h1>系のタグが見つかったらTrueになる
        self.headline_texts = [] # <h1>系タグの中身
        self.target_class = '' # 解析対象のクラス名

    def handle_starttag(self, tag, attrs):
        d = dict(attrs)
        if re.match('^h[1-9]$', tag) and self.target_class in d.get('class', ''):
            self.found_headline = True

    def handle_data(self, data):
        if self.found_headline:
            self.headline_texts.append(data)
            self.found_headline = False

    def feed(self, content, class_=''):
        self.target_class = class_
        super().feed(content)

parser = GrepHeadlines()
parser.feed('''
<h1 class="hug-me">本日は</h1>
<h2 class="grep-me">晴天なり</h2>
<h3 class="grep-me">あっぱれ!</h3>
''', class_='grep-me')

print(parser.headline_texts)

↑のコードを実行すると↓のような結果になります。

['晴天なり', 'あっぱれ!']

今回の改造では、feedメソッドをオーバーライドして引数にclass_を加えています。
feedを呼び出すときにこのclass_に保存対象のクラス名を指定すると、GrepHeadlinesパーサーは指定の条件にあてはまる<h1> ~ <h9>などのタグのテキストを保存します。
条件を判断しているのはhandle_starttagメソッドです。

    def handle_starttag(self, tag, attrs):
        d = dict(attrs)
        if re.match('^h[1-9]$', tag) and self.target_class in d.get('class', ''):
            self.found_headline = True

このメソッドではまず引数のattrsを辞書に変換しています。そう、引数のattrsはタプルです。なんでやねん! と思ったあなた、気持ちはわかります。しかしよくよく考えると、タグのクラスというのは複数指定することが出来ます。よって属性のデータ型をタプルにしているのは理に叶っていると言えます。辞書だと重複したキーは上書きされますからね。
タグが<h1> ~ <h9>のどれかで、かつ辞書dに指定したクラス名が含まれていたら、フラグがTrueになります。

おわりに

意外と標準ライブラリだけで何とかなるものです。Pythonがパワフルだと言われるゆえんがよくわかります。
HTMLをパースしたくなったらこのhtml.parser, html.parserを思い出してみてください。

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク