HTML から本文のテキストだけを抽出する Python コード例(見出しタグと見出しに属するテキストを取得)

HTML から『本文だけ』をキレイにスクレイピングする簡単な Python コード例を書きました。

種々雑多しゅじゅざったな HTML から、本文だけをねらって抽出する、汎用的はんようてきなコード例です。

for 文と while 文を使用して、『見出しタグと同じ階層にあるタグ』を取得していくアプローチになります。

『見出しタグ』と『見出しタグに属するテキスト』を対応づけて抽出していきます。

このアプローチで本文抽出を行った結果、本文以外のノイズを含まない、キレイなテキストを取得することができました。

サイドバーやフッターなど、本文とは関係ないテキストを、キレイに除去することができました。

本文に『見出しタグ (h1-h6)』を使用している普通の HTML であれば、そういった高い精度で抽出することができました。

その Python コード例です。

本文だけをキレイに抽出

コード例

HTML から本文だけを抽出する Python コード例です。

ローカルに置いたサンプル HTML から抽出しています。

print 文は、すべてデバッグ用です。

テキストの抽出過程を動作ログとして出力するために書きました(削除しても大丈夫です)。

抽出したテキストは、見出しタグと一緒にリストに入れました。

抽出したテキストを保持するやり方は、ほかにもいろいろあると思います。

このコードを実行すると、『見出しタグ』と『タグに属するテキスト』を、関連付けて取得することができます。

抽出結果は、とりあえず CSV として保存しました。

"""extract_text.py"""

import re
import csv
from pathlib import Path
from lxml import html


# 不要なタグを検索する xpath 表現のタプル
REMOVE_TAGS = ('.//style', './/script', './/noscript')

# 見出しタグを検索する xpath 表現
XPATH_H_TAGS = './/h1|.//h2|.//h3|.//h4|.//h5|.//h6'

# 見出しタグを検出するための正規表現
RE_H_MATCH = re.compile('^h[1-6]$').match


def main():
    """メイン関数"""

    # (1/8) HTML ファイルを指定します
    src_file = Path(r'***\source.html')

    # (2/8) HTML データを取得します
    with src_file.open('rb') as f:
        html_data = f.read()

    # (3/8) HTML を解析します
    root = html.fromstring(html_data)

    # (4/8) HTML から不要なタグを削除します
    for remove_tag in REMOVE_TAGS:
        for tag in root.findall(remove_tag):
            tag.drop_tree()

    # (5/8) テキストの入れ物を用意します
    #      (デバッグ用にラベル行も追加)
    texts = []
    texts.append(
        ['タグ名', 'タグテキスト', 'タグに属するテキスト'])

    # (6/8) タイトルタグを取得します
    t = root.find('.//title')
    if t is not None:
        text = t.text_content()

        # 空でなければリストに追加
        if text:
            texts.append([t.tag, text, ''])

        print(f'(デバッグ) {t.tag}: {text}\n')

    # (7/8) 見出しタグを検索します
    for h_tag in root.xpath(XPATH_H_TAGS):
        # 見出しタグのテキストを取得
        h_text = h_tag.text_content()

        print(f'(デバッグ) {h_tag.tag}: {h_text}')

        # 見出しタグと同じ階層にあったテキストを入れるリスト
        contents = []

        # 見出しの次のタグを取得
        next_tag = h_tag.getnext()

        # 次のタグがなくなるまでループ
        while next_tag is not None:
            # タグが見出しだったらブレーク
            if RE_H_MATCH(next_tag.tag):
                print(f'(デバッグ) 次の見出しタグ {next_tag.tag} が見つかった。')
                print(f'(デバッグ) while ブレーク\n')
                break

            # タグのテキストを取得
            text = next_tag.text_content()

            # 空でなければリストに追加
            if text:
                contents.append(text)

            print(f'(デバッグ) {next_tag.tag}: {text}')

            # さらに次のタグを取得してループする
            next_tag = next_tag.getnext()
        else:
            # 同じ階層のタグをたどり尽くして、次のタグが無かった場合。
            print(f'(デバッグ) 次のタグが無かった。 {next_tag}')

        # リストを連結してひとつの文字列にします
        contents = '|'.join(contents)

        # リストに追加
        texts.append([h_tag.tag, h_text, contents])

    # (8/8) テキストを CSV に保存します
    csv_file = Path(r'***\texts.csv')
    with csv_file.open('w', encoding='utf-8', newline='') as f:
        w = csv.writer(f)
        w.writerows(texts)

    # 以上です
    return


if __name__ == "__main__":
    main()

サンプルHTML

ローカルに置いたサンプル HTML です。

本文のテキストが、h1 タグや h2 タグと同じ階層に並んでいます。

本文のテキスト抽出は、ちょうど CSV ファイルをカンマで区切って読み込んでいく感じに似ています。

次の見出しタグ (h1-h6) が出てくるまで、同じ階層のタグを、次々に取得して読み込んでいきます。

(ファイル名:source.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>タイトル</title>
</head>
<body>
    <div>
        <h1>あああ</h1>
        <p>本文テキスト1。</p>
        <p>本文テキスト2。</p>
        <p>本文テキスト3。</p>

        <h2>いいい</h2>
        <p>本文テキスト4。</p>
        <p>本文テキスト5。</p>

        <h3>ううう</h3>
        <p>本文テキスト6。</p>

        <h4>えええ</h4>
        <p>本文テキスト7。</p>

        <h5>おおお</h5>
        <p>本文テキスト8。</p>
        <p>本文テキスト9。</p>

        <h6>かかか</h6>
        <p>本文テキスト10。</p>

        <h2>ききき</h2>
        <p>本文テキスト11。</p>
    </div>
</body>
</html>

実行ログ

Python コードの実行ログです。

『次の見出しタグ』に遭遇するたびに、while 文をブレークしているのがわかります。

(コンソールの表示)

(デバッグ) title: タイトル

(デバッグ) h1: あああ
(デバッグ) p: 本文テキスト1。
(デバッグ) p: 本文テキスト2。
(デバッグ) p: 本文テキスト3。
(デバッグ) 次の見出しタグ h2 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h2: いいい
(デバッグ) p: 本文テキスト4。
(デバッグ) p: 本文テキスト5。
(デバッグ) 次の見出しタグ h3 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h3: ううう
(デバッグ) p: 本文テキスト6。
(デバッグ) 次の見出しタグ h4 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h4: えええ
(デバッグ) p: 本文テキスト7。
(デバッグ) 次の見出しタグ h5 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h5: おおお
(デバッグ) p: 本文テキスト8。
(デバッグ) p: 本文テキスト9。
(デバッグ) 次の見出しタグ h6 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h6: かかか
(デバッグ) p: 本文テキスト10。
(デバッグ) 次の見出しタグ h2 が見つかった。
(デバッグ) while ブレーク

(デバッグ) h2: ききき
(デバッグ) p: 本文テキスト11。
(デバッグ) 次のタグが無かった。 None

抽出結果

HTML からテキストを抽出した結果です。

CSV に保存したので、LibreOfficeリブレオフィス で開いたところのスクリーンショットを撮りました。

ねらい通り、『見出しタグ』と『タグに属するテキスト』を関連付けて取得することができました。

タグに属するテキストを縦棒 '|' で区切っていますが、やり方はいろいろあると思います。

json 形式や pickle 形式などを使用するなら、連結せずにリストのまま持っていてもいいと思います。

(ファイル名:texts.csv)スクリーンショット

(ファイル名:texts.csv)ファイルの中身

タグ名,タグテキスト,タグに属するテキスト
title,タイトル,
h1,あああ,本文テキスト1。|本文テキスト2。|本文テキスト3。
h2,いいい,本文テキスト4。|本文テキスト5。
h3,ううう,本文テキスト6。
h4,えええ,本文テキスト7。
h5,おおお,本文テキスト8。|本文テキスト9。
h6,かかか,本文テキスト10。
h2,ききき,本文テキスト11。

見出しごとに本文を抽出できて便利

見出しタグと同じ階層にあるタグ』を取得していくアプローチは、コードが簡単でノイズも少なく、テキスト分析に有効でした。

記事本体のテキストを的確に取得することができたので、見出しとテキストの関係を研究するのに、とても便利でした。

以前は、HTML 全体をまるごとテキストに変換していましたが、本文が区別できないという問題がありました。それが、おおむね解決しました。

テキスト分析の研究が、さらにはかどっています。

これまでは、サイドバーやフッターでよく使われていたタグをリストアップして、できるだけ本文が残るように除外していました。

ですが、そういったアプローチでは、対応できる HTML に限界がありました。

そこで、『見出しタグと同じ階層にあるタグ』を取得していくアプローチを考えて試してみました。

もちろん、万能ではありませんでしたが、自分としては多くの HTML で的確に本文を読み込むことができたので、良かったです。

テキスト分析や検索エンジンの開発に使用しています。

自分は lxml.htmlエルエックスエムエル エイチティーエムエル で実装しましたが、おそらく BeautifulSoupビューティフルスープ でもできるんじゃないかと思います。

BeautifulSoup にも『次のタグ』を取得するメソッドがあるようでしたので、たぶんできるんじゃないかと。

テキストデータを削減できた

サイドバーやフッターなどのナビゲーションには、本文と関係のないテキストがたくさんありました。

そういった余分なテキストが、ストレージのデータ容量を圧迫していました。

それが、HTML の記事本文をねらって抽出できるようになったことで、抽出したテキストのサイズを削減することができました。

見出しが無い HTML には不向き

見出しタグと同じ階層にあるタグ』を取得していくアプローチですが、見出しが無い HTML には不向きでした。

そういった HTML は、少ないですが、少しありました。

具体的には、『エラーページ』とか『メンテナンス中の表示』とかです。

そういった簡素かんそなページで、h タグの見出しがない HTML に遭遇しました。

構造的には、body タグの直下ちょっかにテキストだけが載っていました。

そういったケースもありましたので、自分の場合は、テキスト抽出関数をいくつか用意して、切りかえながら使っています。

もし見出しタグがなかったら HTML 全体をテキストに変換する、といった感じです。

見出しタグが入れ子になっている HTML には不向き

ごくまれにですが、H タグの中に H タグを入れている HTML に遭遇しました。

たぶん、単なるコーディングミスだと思うのですが、そういった場合は、抽出結果が少し変になりました。

しかたがないので、そういったケースはあきらめました。

HTML が少し変でもブラウザでは表示可能という現実があるので、ある程度の精度で良しとしています。

サイドバーなどに見出しタグが使われている HTML には不向き

たまにですが、本文に加えて、サイドバーにまで見出しタグを使用している HTML がありました。

HTML のコーディングミスなのか、意図的にそうしているのかはわかりませんが、そういったケースでは、抽出結果に不要なテキストが混ざってしまいました。

HTML には、本当にいろいろパターンがありました。

対応できないケースもあったけれどおおむね満足

見出しタグと同じ階層にあるタグ』を取得していくアプローチは、Python コードが簡単で、抽出精度にもおおむね満足しています。

種々雑多しゅじゅざったな HTML から、本文だけを抽出する汎用的な方法として、役に立っています。

一方で、抽出方法に正解のない領域もありました。

本文にテーブルタグや CSS で組み立てたコンテンツがある場合ですね。

そういった『文章として扱えないコンテンツ』は、抽出するとどうしてもくずれてしまいました。

あとは、本文が div タグに入っていて、その中にさらに見出しタグが入っているようなケースですね。

まだ見かけたことがないので想像ですが、同じテキストを2重に取得してしまう結果になるかもしれません。

そういったところをどうするのかは、やはり個別対応になると思います。

ですが、そこまでこだわらなくても、有用なテキスト分析はできました。

なので、あまり深くこだわる必要はないと、自分はそう思うようにしています。

タイトルとURLをコピーしました