続・BeautifulSoup で HTML 文書からタグを取り除く(Python)
前回(BeautifulSoup で HTML 文書からタグを取り除く(Python) - やた@はてな日記)の内容でも,ある程度は問題なく処理できていました.しかし,大量の HTML 文書を渡してみると,新たに 2 つの問題が見つかりました.それぞれの内容と今回の対処は以下のとおりです.
- 深すぎる再帰呼び出しによる RuntimeError 例外
- <p> を改行(<br>)の代わりに使っている HTML 文書や,バグ入りの自動生成プログラムにより作成された HTML 文書などが原因だろうと思います(未確認).
- 再帰呼び出しを使わずに ParseTree を探索するようにしました.
- <p> を改行(<br>)の代わりに使っている HTML 文書や,バグ入りの自動生成プログラムにより作成された HTML 文書などが原因だろうと思います(未確認).
- 不正な数値文字参照による ValueError と OverflowError
- ValueError が送出される例:�
- OverflowError が送出される例:�
- BeautifulSoup に少しだけ手を加えることで対処しました.
また,PRE 要素の外部にある改行は空白に置き換えるようにしました.ただし,PRE 要素の入れ子には対処していません.
※ 私が普段使っているブラウザ(Firefox)では PRE 要素の入れ子が有効でした.
以上の修正を加えた結果,以下のようなコードになりました.元のコードと比べると,エレガントさに欠けます….
def getNavigableStrings(soup): # PRE 要素の階層を記録します. preCount = 0 # ループ 1 回で,葉ノードまでの探索をおこない,次の探索開始位置を求めます. while soup: # 葉ノードに到達するか,テキストノードに到達するまで探索します. while not isinstance(soup, BeautifulSoup.NavigableString): if soup.name in ("script", "style", "s"): break # 実際のところ,BeautifulSoup は PRE 要素の入れ子を許さないため, # True/False のフラグでも構わないかもしれません. if soup.name == "pre": preCount = preCount + 1 if soup.name in _blockTags: yield u"\n" if soup.contents: soup = soup.contents[0] else: break if isinstance(soup, BeautifulSoup.NavigableString): if type(soup) not in (BeautifulSoup.Comment, BeautifulSoup.Declaration): # PRE 要素の外側では改行を空白に置き換えます. if preCount: yield soup else: yield u" ".join(soup.splitlines()) if soup.nextSibling: soup = soup.nextSibling continue soup = soup.parent # 兄弟ノードを探しつつ,ルート方向へと移動します. while soup: if soup.name == "pre": preCount = preCount - 1 if soup.name in _blockTags: yield u"\n" if soup.nextSibling: soup = soup.nextSibling break soup = soup.parent ## BeautifulSoup.BeautifulStoneSoup の修正 def handle_charref(self, ref): "Handle character references as data." if self.convertEntities: # 修正内容は try, except の追加です. # 例外が送出された場合,10 進数の数値文字参照に戻します. try: data = unichr(int(ref)) except (ValueError, OverflowError): data = '&#%s;' % ref else: data = '&#%s;' % ref self.handle_data(data)
大量の HTML 文書からテキストを切り出してみて,「致命的な問題はなさそうな気がする」状態になりました.気のせいかもしれませんが….
現状で,Core2 Duo U9600 (1.6GHz) での処理速度は,8 文書/秒くらいです.2 並列にしても 16 文書/秒くらいですから,大量の HTML 文書からテキストを切り出すという用途では,かなりのリソースが必要になります.
追記(2010-06-27):BeautifulSoup の修正について,Tag._convertEntities() 内部の unichr() で例外(ValueError)が投げられる可能性を見逃していました.こちらも try, except を加えておいた方がよさそうです.
## 以下の部分を try, except で囲むと大丈夫なはず…. # Handle numeric entities if len(x) > 1 and x[1] == 'x': return unichr(int(x[2:], 16)) else: return unichr(int(x[1:]))