BeautifulSoup で HTML 文書からタグを取り除く(Python)
はじめに
HTML の解析に便利な BeautifulSoup(Python ライブラリ)を使って HTML 文書のテキスト部分の切り出しを試みましたというお話です.「間違えているところがある」とか「もっと良い方法がある」という場合,コメントをいただけると幸いです.
※ HTML 文書の焦点抽出(ニュースやブログからの記事抽出など)については考慮していません.
追記(2010-06-21):このお話には続き(続・BeautifulSoup で HTML 文書からタグを取り除く(Python) - やた@はてな日記)があり,追加の問題とその対処について説明しています.
BeautifulSoup とは
BeautifulSoup は,以下のサイトでダウンロードできる Python 用のライブラリです.インストールをしなくても,アーカイブの中にある BeautifulSoup.py をコピーするだけで簡単に使えます.
※ Ubuntu 10.4 には Python 2.6 がインストールされているので,BeautifulSoup 3.0.8.1 を選択しました.
Beautiful Soup: We called him Tortoise because he taught us.
例えば,BeautifulSoup の基本機能に含まれる「HTML 文書の整形(prettify)」であれば,コンストラクタに HTML 文書を渡して,それから prettify() を呼び出すという 2 ステップで終了します.
import BeautifulSoup html = '<html><head><title>Title</title></head><body><p>Paragraph</p></body></html>' soup = BeautifulSoup.BeautifulSoup(html) print soup.prettify() # <html> # <head> # <title> # Title # </title> # </head> # <body> # <p> # Paragraph # </p> # </body> # </html>
日本語の文字コードについては,自動的に判別してくれるようになっています.しかし,nkf のようなツールが失敗するのと同様に,BeautifulSoup も文字コードの判別に失敗することがあります.そのような場合,コンストラクタに fromEncoding を指定してやる必要があります.
※ HTML 文書に不正な文字が含まれている場合,どうやってもエラーが発生します.さっさと諦めてしまいましょう.
BeautifulSoup とタグ除去
ウェブ検索ですぐに先達(BeautifulSoupでタグを除去してテキストだけ抜き出す - yanbe.py - pythonグループ)が見つかりました.ここは先人に感謝し,とりあえず真似させていただくことにしましょう.
def getNavigableStrings(soup): if isinstance(soup, BeautifulSoup.NavigableString): if type(soup) not in (BeautifulSoup.Comment, BeautifulSoup.Declaration) and soup.strip(): yield soup elif soup.name not in ('script', 'style'): for c in soup.contents: for g in getNavigableStrings(c): yield g soup = BeautifulSoup.BeautifulSoup(html) text = '\n'.join(getNavigableStrings(soup))
試してみると,大体うまくいきます.でも,いくつか気になることがあり,以下のように対処しました.
- 文字参照を解決したい.
- BeautifulSoup のコンストラクタで convertEntities を指定する.
- 正規化おこないつつ,不要な空白・改行を取り除きたい.
- unicodedata.normalize() で正規化する.
- splitlines(), split(), join() で不要な空白・改行を取り除く.
- インライン要素の前後に改行を入れたくない.
- 改行で連結するのをやめて,ブロック要素の前後のみに改行を入れる.
## 1. 文字参照について
soup = BeautifulSoup.BeautifulSoup(html,
convertEntities = BeautifulSoup.BeautifulStoneSoup.HTML_ENTITIES)
## 2. 正規化と空白について import unicodedata # 不要な空白を取り除き,空行以外を返す. def nonEmptyLines(text): for line in text.splitlines(): line = u' '.join(line.split()) if line: yield line # 正規化の後で不要な空白・改行を取り除く. def normalizeText(text): text = unicodedata.normalize('NFKC', text) return u'\n'.join(nonEmptyLines(text)) text = u'\n'.join(getNavigableStrings(soup)) text = normalizeText(text)
## 3. インライン要素前後の改行について # ブロック要素の集合です. blockTags = frozenset(['p', 'div', 'table', 'dl', 'ul', 'ol', 'form', 'address', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'fieldset', 'hr', 'pre' 'article', 'aside', 'dialog', 'figure', 'footer', 'header', 'legend', 'nav', 'section']) def getNavigableStrings(soup): if isinstance(soup, BeautifulSoup.NavigableString): # 空白だけでも返すように,``and soup.strip()'' を消しました. # 英数字前後の空白を誤って取り除くことを防ぎます. if type(soup) not in (BeautifulSoup.Comment, BeautifulSoup.Declaration): yield soup elif soup.name not in ('script', 'style'): # ブロック要素の前後に改行を入れるようにしました. isBlock = soup.name in blockTags if isBlock: yield u'\n' for c in soup.contents: for g in getNavigableStrings(c): yield g if isBlock: yield u'\n' # 連結時に改行を挟むのをやめました. text = u''.join(getNavigableStrings(soup))
このくらいで,ひとまず満足しました.でも,テーブルのセル周りはどうするのか(改行で区切るか?),取り消しされた部分(<s> と </s> で挟まれている部分)はどうするのか('script' や 'style' と同じように扱うか?),などの問題についても考慮する必要があるかもしれません.
BeautifulSoup と日本語文字コード
BeautifulSoup に日本語の HTML 文書を渡すと,10 〜 20% くらいの割合で TypeError や UnicodeError が送出されます.そして,どうやらエラーの原因は文字コードにあるようです.
さすがに 10% を超えてエラーになるのは困るので,上手くいくことを願いつつ,適当な文字コードを指定してみることにしました.
# お試し文字コードのリストです. knownEncodings = ['CP932', 'SHIFT_JISX0213', 'EUC-JP-MS', 'EUC-JISX0213', 'ISO-2022-JP-2', 'ISO-2022-JP-3', 'UTF-8'] # 有名どころの文字コードで BeautifulSoup を試します. def testKnownEncodings(html): for e in knownEncodings: try: # 長くなるので convertEntities は省略しています. soup = BeautifulSoup.BeautifulSoup(html, fromEncoding = e) return soup except (TypeError, UnicodeError): pass try: # 長くなるので convertEntities は省略しています. soup = BeautifulSoup.BeautifulSoup(html) except (TypeError, UnicodeError): soup = testKnownEncodings(html)
以上の強引(手抜き)な方法により,25% くらい長く時間はかかるものの,エラーの割合を 1 〜 2% くらいに抑えることができました.めでたしめでたし….
追記(2010-06-21):大量の HTML 文書を処理してみたところ,エラーの割合は 4% 前後に落ち着きました.
追記(2010-06-27):testKnownEncodings() について,いくつか修正すべき箇所があります.
Python は "euc-jp-ms" をサポートしていないので,fromEncoding に "euc-jp-ms" を指定すると,必ず失敗します.iconv などを利用して変換する必要があります.ついでに,標準エンコーディングを見た感じ,文字コードのリストは以下のように修正した方がよさそうです.
※ cp932 と shift_jis_2004 や euc-jp-ms と euc_jis_2004 では,同じ符号に異なる文字が割り当てられています.そのため,文字化けの割合が少なくなるように,使われることの多い cp932 と euc-jp-ms を前に配置しています.また,絵文字
KNOWN_ENCODINGS = ["cp932", "shift_jis_2004", "euc-jp-ms", "euc_jis_2004", "iso2022-jp-2", "iso2022-jp-3", "iso2022-jp-2004"]
また,BeautifulSoup のコンストラクタが成功しても,文字化けしている可能性があります.これまでに確認した誤りとしては,euc-jp-ms らしき文書が windows-1252 扱いされて,記号だらけになるという事例がありました.原因は,meta タグで指定されている文字コードでの処理に失敗したとき,自動的に utf-8 と windows-1252 での処理を試みるためです.
windows-1252 ではないと断定できるのであれば,originalEncoding が windows-1252 になっている場合に失敗と判断すればよいでしょう.
soup = BeautifulSoup.BeautifulSoup(html) if soup.originalEncoding == "windows-1252": # エラー処理