HTML から『本文だけ』をキレイにスクレイピングする簡単な Python コード例を書きました。
※ 通常のテキスト取得方法はこちらに書きました。
【Python】HTML からテキストを抽出するコード例【lxml.html】
種々雑多な 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 がありました。
サイドバーが <aside></aside>
などのタグで囲まれている場合は簡単に除外できたので大丈夫だったんですが、そうでない場合は本文との区別が付きにくかったです。
どうしても抽出結果に不要なテキストが混ざってしまいました。
HTML には、本当にいろいろパターンがありました。
対応できないケースもあったけれどおおむね満足
『見出しタグと同じ階層にあるタグ』を取得していくアプローチは、Python コードが簡単で、抽出精度にもおおむね満足しています。
種々雑多な HTML から、本文だけを抽出する汎用的な方法として、役に立っています。
一方で、抽出方法に正解のない領域もありました。
本文にテーブルタグや CSS で組み立てたコンテンツがある場合ですね。
そういった『文章として扱えないコンテンツ』は、抽出するとどうしてもくずれてしまいました。
あとは、本文が div タグに入っていて、その中にさらに見出しタグが入っているようなケースですね。
まだ見かけたことがないので想像ですが、同じテキストを2重に取得してしまう結果になるかもしれません。
そういったところをどうするのかは、やはり個別対応になると思います。
ですが、そこまでこだわらなくても、有用なテキスト分析はできました。
なので、あまり深くこだわる必要はないと、自分はそう思うようにしています。