先日BeautifulSoupを使おうとして挫折した訳ですが、そうは言ってもGAEではlxmlが使えない以上、GAE上でスクレイピングする場合は使うしかないです。htmlのパーサーなんて書く気にならないのであります。書く力が無いのであります。
しかし先日も書いたように使ってみると、割と頻繁にパースエラーが発生して処理できなくなります。例えば(http://mlb.mlb.com/stats/historical/player_stats.jsp)を実際にパースしてみると以下の様なエラーが発生します。
>>> import urllib >>> from BeautifulSoup import BeautifulSoup >>> source = urllib.urlopen('http://mlb.mlb.com/stats/historical/player_stats.jsp').read() >>> soup = BeautifulSoup(source) Traceback (most recent call last): File "< stdin >", line 1, in < module > File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/BeautifulSoup.py", line 1499, in __init__ BeautifulStoneSoup.__init__(self, *args, **kwargs) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/BeautifulSoup.py", line 1230, in __init__ self._feed(isHTML=isHTML) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/BeautifulSoup.py", line 1263, in _feed self.builder.feed(markup) File "/System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/HTMLParser.py", line 108, in feed self.goahead(0) File "/System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/HTMLParser.py", line 150, in goahead k = self.parse_endtag(i) File "/System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/HTMLParser.py", line 314, in parse_endtag self.error("bad end tag: %r" % (rawdata[i:j],)) File "/System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/HTMLParser.py", line 115, in error raise HTMLParseError(message, self.getpos()) HTMLParser.HTMLParseError: bad end tag: u'< / scr "+" ipt > ', at line 104, column 114
原因は< / scr "+" ipt > の様な形式のscriptタグをパースできない事が理由で、対策としてはhtml5libを使うと良いようです。確かに先ほどのURLをhtml5libを利用してパースすると上手くパースできます。
>>> parser = HTMLParser(tree=treebuilders.getTreeBuilder("beautifulsoup")) >>> soup = parser.parse(source) >>> soup.find('title') <title>Historical Player Stats | MLB.com: Stats</title>
ところがこれで万事解決ではありません。何故かというとhtml5libを使うと、逆にパースできなくなるページがあるからです。例えば(‘http://blog.livedoor.jp/goldennews/archives/51338054.html’)をhtml5libでパースしてみると以下の様なエラーが発生します。(ただしこちらは普通のBeautifulSoupでパースしてもエラーは発生しません)
>>> source = urllib.urlopen('http://blog.livedoor.jp/goldennews/archives/51338054.html').read() >>> parser.parse(source) Traceback (most recent call last): File "< stdin >", line 1, in < module > File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/html5lib/html5parser.py", line 155, in parse self._parse(stream, innerHTML=False, encoding=encoding) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/html5lib/html5parser.py", line 132, in _parse method(token["name"]) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/html5lib/html5parser.py", line 329, in processEndTag self.endTagHandler[name](name) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/html5lib/html5parser.py", line 1160, in endTagFormatting furthestBlock.parent.removeChild(furthestBlock) File "/Users/matsumototaichi/Documents/test/python_test/beautifulsoap_test/html5lib/treebuilders/soup.py", line 92, in removeChild del node.parent.element.contents[index] TypeError: list indices must be integers
バグトラッカーへの記述によると、内部でhtmlのノードツリーを書き換える際に、ノード間のリンクが壊れているらしいです。解ってるなら直してくれと思いながらも、何とかうまく解決する方法は無いのかと、色々と実験してみましたが上手く解決する方法は見つかりませんでした。
仕方がないので、その場しのぎですがパースエラーをキャッチして、両方のパーサを併用する事にしました。
def get_soup(url): source = urllib.urlopen(url).read() try: # まずBeautifulSoupでパースして、 soup = BeautifulSoup(source) except: # エラーが発生したらhtml5libでパースする parser = HTMLParser(tree=treebuilders.getTreeBuilder("beautifulsoup")) soup = parser.parse(source) return soup
更にエラーが発生する場合は、また別のパーサでパースする訳ですが、汚いですよね。もっといい方法があったら教えて下さい。
関連する記事
タグ: beautifulsoup, python

ずいぶんと前の投稿にコメントしてよいものやらわかりませんが、
参考にさせていただきました。
source = urllib.urlopen(url).read()
ここで<script~を削除してから
soup = BeautifulSoup(source)
とするとうまくいくことが多いように思います。
te2rojp 様
コメントありがとうございます。返信が遅くなってしまいました。
確かにがパースできないので削除すると動くのですが、
一々削除するのは抵抗がありますし、他のパースエラーが出た際に
都度対応するのも面倒です。そこで記事のような方策を取っていたのですが、
先日リリースされたBS3.2系列でうまくパースできるようになっているようです。
別のパースエラーがでない限りは特にhtml5libを使う必要はなさそうですね。