BeautifulSoup を使ったコード片のメモ
昔作ったはずなんだけど、コードをなくしたらしい。なので作り直した。はてな上にメモしておけば、今度こそなくさない?(いや、ちゃんとバックアップ取れよオレ)
BeautifulSoup とは?
HTML, XML パーサ。メモリにツリー状のデータを構築するタイプ。標準ライブラリ xml.sax のようなイベント駆動型ではない。BeautifulSoup に近い標準ライブラリは xml.etree や xml.dom 。
サイトの URL は http://www.crummy.com/software/BeautifulSoup/ 。
テキスト抽出
BeautifulSoup のドキュメントにもあった例を関数にしたもの。
def collect_string(tag): return u''.join(tag.findAll(text=True))
もうすこし凝ったことがしたくなったら find* とか *Generator を使って中身を順に取り出しつつ処理。たとえば recursiveChildGenerator 。これを使って似たものを実装するとこんな感じ。
def collect_string(tag): for next_tag in tag.recursiveChildGenerator(): if isinstance(next_tag, unicode): yield next_tag
使用例。
>>> from BeautifulSoup import BeautifulSoup >>> source = u'<html><body><p>foo</p><p>bar<br />baz</p></body></html>' >>> soup = BeautifulSoup(source) >>> print u''.join(collect_string(soup.body)) foobarbaz
これをベースに必要に応じて改良する。たとえば br タグを改行にしたかったらこう。
def collect_string(tag): for next_tag in tag.recursiveChildGenerator(): if isinstance(next_tag, unicode): yield next_tag elif getattr(next_tag, 'name', None) == u'br': yield u'\n'
>>> from BeautifulSoup import BeautifulSoup >>> source = u'<html><body><p>foo</p><p>bar<br />baz</p></body></html>' >>> soup = BeautifulSoup(source) >>> print u''.join(collect_string(soup.body)) foobar baz
recursiveChildGenerator の代わりに nextGenerator を使い、抽象度を上げていけばイベント駆動型のパーサになりそうだが、作りこみすぎは無駄におわりそう。本格的なイベント駆動型のパーサが要るならば sgmllib (Python 3 で削除) や HTMLParser (Python 3 では html.parser) 、 xml.sax (XML 限定) 、 xml.parser.expat (XML 限定) でよいから。 BeautifulSoup もこれらを使って作られているのだし*1。
table 解析
まずは rowspan, colspan 無視版。
def parse_table(tbody): u"""table を行列に分割する rowspan, colspan は無視される。""" table_cols = len(tbody.find('tr')) for tr in tbody.findAll('tr', recursive=False): yield (td for td in tr.findAll(['td', 'th'], recursive=False))
引数は tr タグを直接子として持っているタグ。 tbody, thead, tfoot タグなど。 tbody が省略されている表なら table タグそのもの。
>>> from BeautifulSoup import BeautifulSoup >>> source = u""" ... <html><body> ... <table> ... <thead> ... <tr><td>a</td><td>b</td><td>c</td></tr> ... </thead> ... <tbody> ... <tr><td>d</td><td>e</td><td>f</td></tr> ... <tr><td colspan="2">g</td><td rowspan="2">h</td></tr> ... <tr><td>i</td><td>j</td></tr> ... </tbody> ... </table> ... </body></html> ... """ >>> for cols in parse_table(soup.tbody): ... for col in cols: ... print col, ... print ... <td>d</td> <td>e</td> <td>f</td> <td colspan="2">g</td> <td rowspan="2">h</td> <td>i</td> <td>j</td>
parse_table は tbody が複数ある、省略されている、 thead, tfoot も読みたい、といった各表のこまかい差異には対応しない。呼び出し側でなんとかして、という方針。たとえば thead も読む場合はこう。
>>> from itertools import chain, imap >>> for cols in chain.from_iterable( ... imap(parse_table, (soup.thead, soup.tbody))): ... for col in cols: ... print col, ... print ... <td>a</td> <td>b</td> <td>c</td> <td>d</td> <td>e</td> <td>f</td> <td colspan="2">g</td> <td rowspan="2">h</td> <td>i</td> <td>j</td>
rowspan, colspan を考慮に入れた parse_table はこう。
def parse_table(tbody): u"""table を行列に分割する""" # はじめに列数をカウントする table_cols = 0 for td in tbody.find('tr').findAll(['td', 'th'], recursive=False): table_cols += int(td.get('colspan', 1)) # [繰り返し回数, 対象 column] を記録するリストを作成 old = [[0, None] for i in xrange(table_cols)] # 行を分解 for tr in tbody.findAll('tr', recursive=False): # 列を分解 cols = [] col = 0 tds = tr.findAll(['td', 'th'], recursive=False) while col < table_cols: if old[col][0]: td = old[col][1] old[col][0] -= 1 else: td = tds.pop(0) rowspan = int(td.get('rowspan', 1)) rowspan_m1 = rowspan - 1 if rowspan_m1: old[col] = [rowspan_m1, td] colspan = int(td.get('colspan', 1)) for i in xrange(colspan): cols.append(td) col += 1 yield iter(cols)
>|python| >>> from BeautifulSoup import BeautifulSoup >>> source = u""" ... <html><body> ... <table> ... <thead> ... <tr><td>a</td><td>b</td><td>c</td></tr> ... </thead> ... <tbody> ... <tr><td>d</td><td>e</td><td>f</td></tr> ... <tr><td colspan="2">g</td><td rowspan="2">h</td></tr> ... <tr><td>i</td><td>j</td></tr> ... </tbody> ... </table> ... </body></html> ... """ >>> for cols in parse_table(soup.tbody): ... for col in cols: ... print col, ... print ... <td>d</td> <td>e</td> <td>f</td> <td colspan="2">g</td> <td colspan="2">g</td> <td rowspan="2">h</td> <td>i</td> <td>j</td> <td rowspan="2">h</td>
前述のテキスト抽出と組み合わせるとこうなる。
>>> for cols in parse_table(soup.tbody): ... for col in cols: ... print collect_string(col), ... print ... d e f g g h i j h
*1:BeautifulSoup 3.0.8 までは sgmllib, 以後は HTMLParser で作られている。