HTMLからテキストを抽出するコード例【Python, lxml.html】

スポンサーリンク

HTMLファイルからテキストを抜き出すコード例です(テキスト抽出のコード例)。

高速なlxmlライブラリを使ったコード例を紹介します。

lxmlでテキスト抽出するのは簡単です。

HTMLをfromstring()で解析して、text_content()で抽出します。

文字のエンコーディングが判定できないときは、cchardet.detect()で判定します。

テキスト抽出前に不要なタグを削除するときは、findall()でリストアップして、drop_tree()で消していきます。

そのあとにtext_content()を実行します。

さて、lxmlには『HTML用のパッケージ(lxml.html)』と『XML用のモジュール(lxml.etree)』がありますが、HTMLからテキストを抽出するときは、lxml.htmlを使います。

最初にコード例を載せています。そのあとで、ほかの書き方やパフォーマンスの比較などを紹介しています。

テキスト分析に取り組まれている方の参考になれば幸いです。

テキスト抽出

Pythonでタグを除去して、テキストを抽出する方法です。

コード例

読み込んだHTMLからそのままテキスト抽出する例と、不要なタグを削除してから抽出する例です。

"""html_main1.py"""
from lxml import html

html_file = r'*****\sample.html'


# HTML読み込み f.read()
# 解析 html.fromstring()
with open(html_file, mode='rb') as f:
    t = html.fromstring(f.read())
    # エンコーディングはlxmlが自動で判定します。


# そのままテキスト抽出
# 先頭と末尾からスペースや改行を除去 strip()
text1 = t.text_content().strip()
print('-----[ text1 ]--------------------')
print(text1)
print('----------------------------------')


# 不要なタグを削除する例
remove_tags = ('.//style', './/script', './/noscript')
for remove_tag in remove_tags:
    for tag in t.findall(remove_tag):
        tag.drop_tree()
        # ここでの削除は元の変数tに反映されます。


# 不要タグ削除後のテキスト抽出
# 先頭と末尾からスペースや改行を除去 strip()
text2 = t.text_content().strip()
print('-----[ text2 ]--------------------')
print(text2)
print('----------------------------------')

抽出結果の先頭と末尾には余分な空白や改行が入っているので、Pythonのstrip()メソッドで取り除いています。

HTMLソースには見やすさのために改行を入れたりしますが、それらが抽出結果に現れているんですね。先頭と末尾だけならstrip()で簡単に取り除けます。

XPathの中の'.//'は、現在のタグより下にあるものといった意味です。ピリオドが現在のタグの位置を表していて、スラッシュ2つが途中の階層をまとめて表しています。

styleタグ、scriptタグ、noscriptタグについては、たいていのケースで削除していいと思います。タグの用途からして、重要な文章が入っていることはほとんどありません。

サンプルHTML

テスト用に作ったサンプルHTMLファイルです(sample.html)。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>タイトル - サンプルサイト</title>
<style type="text/css"><!-- h1 {color:blue;} --></style>
</head><body><h1>H1見出し</h1>
<p>本文。あいうえお<strong>アイウエオ</strong>11111。</p>
<script>document.write("スクリプト");</script>
<noscript>JavaScriptがOFFです。</noscript>
</body>
</html>

抽出結果

サンプルからテキストを抽出した結果です。

後半は不要タグを削除してから抽出した結果で、意図した通り、styleタグ、scriptタグ、noscriptタグの内容が消えています。

-----[ text1 ]--------------------
タイトル - サンプルサイト

<!-- h1 {color:blue;} -->

H1見出し

本文。あいうえおアイウエオ11111。

document.write("スクリプト");

JavaScriptがOFFです。
----------------------------------
-----[ text2 ]--------------------
タイトル - サンプルサイト



H1見出し

本文。あいうえおアイウエオ11111。
----------------------------------

解説

『文字エンコーディングの判定方法』や『エラー対策』の紹介と、『テキスト抽出の速さ』、『タグ検索の速さ』の測定結果です。

エンコーディングはlxmlが自動判定します

HTMLのencoding(文字コード)は、特に指定しなくてもlxmlが自動で判定してくれます。

バイナリデータをhtml.fromstring()に渡せば、ほとんどの場合でうまく解析してくれます。

エンコーディングを指定して解析することもできます。

エンコーディングを指定する方法

HTMLParser()でエンコーディングが指定できます。

そのパーサーをfromstring()関数に渡すと、指定したエンコーディングで解析できます。

# エンコーディングを指定してパーサーを作成
html_parser = html.HTMLParser(encoding='utf-8')

with open(html_file, mode='rb') as f:
    # パーサーを指定して解析
    html_data = html.fromstring(f.read(), parser=html_parser)

エラー回復モードなど

HTMLParser()には、ほかにも色々な機能があります。

エンコーディングのほかに、エラー回復モード(recover:デフォルトでTrue)や、巨大なデータに対応するモード(huge_tree:デフォルトでFalse)などがあります。

html_parser = html.HTMLParser(encoding='utf-8', recover=True, huge_tree=False)

壊れたHTMLを検出したいときはrecover=Falseにすると良いでしょう。

huge_treeについてはMemory allocation failed を回避する【lxml.etree】で解説しています。こちらはXBRLで遭遇したエラーです(XBRLはXMLの一種)。

HTMLで遭遇した経験はまだないのですが、エラー回復モードがTrueなのにエラーが発生する場合は、huge_tree=Trueに設定すると改善するかもしれません。

cchardetを使ったエンコーディング判定

シー・チャー・デットって読めばいいのかな?

HTMLのバイナリデータからエンコーディングを推定してくれます。

cchardetのdetect()関数を使って判定します。

この判定結果でパーサーを作れば、より精度の高い解析ができます。

lxmlが勘違いしてしまったHTMLでも、cchardetなら適切に判定できた場合がたくさんありました。

事例としては、metaタグのエンコーディング指定が間違ってた場合が多かったですね。

結論ですが、一般のウェブサイトを扱うならcchardet.detect()を使うのが最良だと思います。

スポンサーリンク

一方で、仕様が分かっているHTMLだけを扱うなら、lxmlの自動判定に任せたり、パーサーで指定するのが良いと思います。

以下、cchardetのコード例と解説です。cchardetは事前にインストールしておきます。

"""html_detect_encoding.py"""
from lxml import html
from cchardet import detect

# HTML読み込み
with open(html_file, mode='rb') as f:
    html_data = f.read()

# エンコーディングを判定
det_enc = detect(html_data)['encoding']

# HTMLパーサーはUTF-8-SIGを受け付けないので変更する。
if det_enc == 'UTF-8-SIG':
    det_enc = 'utf-8'

# 判定したエンコーディングでパーサーを作成
html_parser = html.HTMLParser(encoding=det_enc)

# 解析
t = html.fromstring(html_data, parser=html_parser)

# テキスト抽出
text = t.text_content()

以下、解説です。

detect()の戻り値は辞書になっています。

{"encoding": encoding, "confidence": confidence}

カンフィデンス(confidence)は、判定の確かさを表す数値です。

通常はencodingだけあれば十分ですので、detect(html_data)['encoding']のように辞書から直接取得しています。

あと、HTMLパーサーは『UTF-8-SIG(BOM付きのUTF-8)』を受け付けないので、そのときは普通の『UTF-8』に変更します。

このようにしても、テキスト抽出の結果に不都合はありませんでした。

先頭にBOMのデータが残っている様子もありませんでした。

2種類のテキスト抽出方法

個々のタグから使えるtext_content()メソッドを使う方法と、lxml.htmlから使えるtostring()関数を使う方法です。

結論です。

tostring()でunicodeを指定した場合が一番高速でした。

いままではtext_content()を使っていたのですが、これからはtostring()に変えてもいいかなって思いました。

それぞれ以下のように使います。

text = t.text_content()

text = html.tostring(t, method='text', encoding='unicode')

text = html.tostring(t, method="text", encoding="utf-8").decode("utf-8")

サンプルHTMLでの抽出結果は同じでした。すべてのHTMLで同じになるかは調べていません。

なお、tostring()のencoding指定は必須のようでした。指定しないと以下のようなエラーが発生しました。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 6-9: ordinal not in range(128)

あと、text_content()はlxml.htmlだけの機能です。XML用のlxml.etreeには存在しません。

テキスト抽出の速さ比較

速さに関しては、tostring()でunicodeを指定した場合が一番高速でした。

以下、計測コードです。

"""html_text_timeit.py"""
from lxml.html import fromstring, tostring
from timeit import timeit

html_file = r'*****\sample.html'

with open(html_file, mode='rb') as f:
    t = fromstring(f.read())

n_max = 100000 # 10万回ずつ実行する
g = globals()

t1 = timeit(
    't.text_content()',
    globals=g, number=n_max,
    )
print('%f t.text_content()' % t1)

t2 = timeit(
    'tostring(t, method="text", encoding="unicode")',
    globals=g, number=n_max,
    )
print('%f tostring(t, method="text", encoding="unicode")' % t2)

t3 = timeit(
    'tostring(t, method="text", encoding="utf-8").decode("utf-8")',
    globals=g, number=n_max,
    )
print('%f tostring(t, method="text", encoding="utf-8").decode("utf-8")' % t3)

実行結果です。

それぞれ10万回の実行にかかった時間(秒)です。

tostring()でunicodeを指定した場合が最短で完了しています。

1.050410 t.text_content()
0.475821 tostring(t, method="text", encoding="unicode")
0.601755 tostring(t, method="text", encoding="utf-8").decode("utf-8")

2種類のタグ検索方法

findall()メソッドとxpath()メソッドを紹介します。

たとえば、不要タグをまとめて検索する時に使います。

結論です。

findall()のほうが高速だけど、タグの属性は『名前』しか指定できない。

xpath()は少し遅いけど、タグの属性は『名前と値』を指定できる。

以下はスタイルタグを取得する例です。2つとも結果は同じでした。

tags = t.findall('.//style')

tags = t.xpath('.//style')

2つの違いですが、たとえばfindall()はタグ属性の『値』を指定できません。

指定できるのは属性の『名前』だけです。属性の存在だけですね。

findall()はid属性の『名前』だけ指定できる。

tags = t.findall('.//div[@id]')

xpath()はid属性の『名前と値』まで指定できる。

tags = t.xpath('.//div[@id="header"]')

tags = t.xpath('.//div[contains(@id,"header")]')

タグ検索の速さ比較

速さに関しては、findall()のほうが高速でした。

以下、計測コードです。

"""html_search_timeit.py"""
from lxml import html
from timeit import timeit

html_file = r'*****\sample.html'

with open(html_file, mode='rb') as f:
    t = html.fromstring(f.read())

n_max = 100000 # 10万回ずつ実行する
g = globals()

remove_tag = './/script'

t1 = timeit(
    't.findall(remove_tag)',
    globals=g, number=n_max,
    )
print('%f t.findall(remove_tag)' % t1)

t2 = timeit(
    't.xpath(remove_tag)',
    globals=g, number=n_max,
    )
print('%f t.xpath(remove_tag)' % t2)

実行結果です。

それぞれ10万回の実行にかかった時間(秒)です。

findall()のほうが早く完了しています。

1.451534 t.findall(remove_tag)
4.434657 t.xpath(remove_tag)

lxmlは速さで圧倒的なメリットがある

おわりに、速さの面では、BeautifulSoupでlxmlを使う方法もありますが、それと比較しても、素のlxmlのほうが8倍くらい高速でした。これは、BeautifulSoupをCPU8コアで動かすのに匹敵する速さといえます。

開発の面では、BeautifulSoupのほうが圧倒的に分かりやすかったのですが、素のlxmlでも調べたら使えるようになりました。大規模な分析で速さが必要なときに、とてもおすすめです。

大量のHTMLからテキストを取り出すときは、lxml モジュールがおすすめ。

テキスト分析に取り組まれている方の参考になれば幸いです。

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