Python: BeautifulSoupで、HTML/XMLをらくらくパージング

2010年2月14日

BeautifulSoupとは、HTML/SGML/XMLをパージングするPythonライブラリです。 ネーミングにセンスを感じさせるこのライブラリは、実用として考えても大変有用なライブラリです。 これを使い始めると、ありとあらゆるHTML/XMLをいじり倒したくなります。

ダウンロード
BeautifulSoupは、Pythonの標準ライブラリではありませんので、 使用するためには以下サイトからDL&インストールする必要があります。

BeautifulSoupのHP


インストール方法
BeautifulSoup.pyを上記サイトからDLしてきて、 pythonのライブラリ置場である「site-packages/」に配置するだけでOKです。 

その上で、

1
2
3
#!/usr/bin/python 
from BeautifulSoup 
import BeautifulSoup, BeautifulStoneSoup

などとすることで、使用可能になります。



使い方
使い方については、 BeautifulSoupのHPのTutorial が最も分かりやすいと思いますのでそちらをご参照ください。 

でもちょっとだけノウハウをば。。。

HTMLファイルからhrefの右辺の値(すなわちURL)だけをとってきたい場合って良くありますよね?
BeautifulSoupでは Attribute属性にアクセスする際に連想配列によるアクセスを用いるのですが、 安直にやると「KeyError: 'href'」というエラーが出力され、 はまります。なので以下のように書きましょう。
1
2
3
4
soup = BeautifulSoup(htmlUtf8) 
for link in soup.findAll("a", href=re.compile("http://")):
    # 「for link in soup.findAll("a"):」とするとエラー
    print link['href']

 エラーの原因は、aタグにhref属性が無いケースもあるためです。なので、上記のように書かずとも、以下のように対応することも可能です。

1
2
3
4
soup = BeautifulSoup(htmlUtf8) 
for link in soup.findAll("a"):
    if ( link.has_key('href') ):
        print link['href']

BeautifulSoupは、内部で文字コードをUTF-8として扱いますので、 コンストラクタにHTMLなどの文字列を渡す際は、以下のようにしてUTF-8に変換してから渡してあげましょう。

1
2
html = htmlFP.read().decode("utf-8", "replace") 
soup = BeautifulSoup(html)

HTMLをパースする際は上記のようにすることでOKですが、 XMLをパースする際はサブクラスのコンストラクタ「BeautifulStoneSoup」をコールしてあげたほうが良いです。 理由は、BeautifulSoupは、HTMLを一度XHTMLに変換してからXML木として各エレメントタグなどへのアクセスを 可能にしているのですが、XHTMLに変換する際に独自のヒューリスティックを用いて変換しているからです。 


XMLをパースする際は、もともとタグによって完全なXML木が構成されていますので そのようなヒューリスティックが適用されると、オリジナルの木構造が変わってしまう恐れがあるわけです。


例えば、以下のようなシングルタグは、BeautifulSoup内ではどう扱われているかというと。。。 


<a>

    <b class="bochibochi-denna" /> 

    <c> here is inside of c-tag. </c> 

</a> 


 これが、以下のように変換されてしまいます。 


 <a> 

     <b class="bochibochi-denna" > 

         <c> here is inside of c-tag. </c> 

     </b> 

</a>


こうなると、オリジナルのXML木ではなくなっていますよね。
このようなヒューリスティック処理が内部的に勝手に行われてしまいますから、 XMLをパースする際はBeautifulStoneSoupを用いるようにしましょう。 

XMLをパースする際にBerutifulStoneSoupを使用しても、 malformedなXML文書にも対応する機能を持っているために、 ここでもいくつかヒューリスティックに基づく変換が自動的になされてしまいます。 

例えば、上記のようなシングルタグのケースもしかりです。 そのような場合は、以下のようにコンストラクタにこのタグは「シングルタグですよ」 と教えてあげる必要があります。そうすれば、シングルタグはシングルタグとして扱われます。

1
2
3
4
#!/usr/bin/python 
from BeautifulSoup import BeautifulStoneSoup 
html = htmlfp.read().decode("utf-8", "replace") 
soup = BeautifulStoneSoup(html, selfClosingTags=['ellipse', 'path'])

これで、「ellipse」タグと「path」タグがシングルタグの場合は、 そのままシングルタグとして扱われることになります。 


BeautifulSoupでタグ除去関数を作成する方法


BeautifulSoupでは、そのオブジェクトbsに対して、 bs.__class__.__name__とすることで現在 BeautifulSoupがパースしている要素の型を返してくれます。 型はタグ型とテキスト型で、それぞれ、"Tag"、"NavigableString"という文字列で表されます。 また、bsがタグ要素(i.e. bs.__class__.__name__ == "Tag")のときは、 bs.nameとすることでタグ名を得ることができます。 テキスト型のときは、bs.stringでそのテキスト自身を取得できます。

この仕組みを応用すれば、以下のようにHTMLからPlaneテキストに変換する関数を作成することができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def stripTags(bso): 
    """ 
    strips html tags in bso. The bso is BeautifulSoup Object. 
    """ 
    global Log_fp 

    if ( (bso.__class__.__name__ != "NavigableString") 
        and (bso.__class__.__name__ != "Tag") ): 
        logger(Log_fp, "(Error) invalid variable in stripTags()...\n") 
        sys.exit(1) 

    rtn = None 
    elem = bso 

    while ( elem ): 
        if ( elem.__class__.__name__ == "NavigableString" ): 
            rtn = rtn + elem.string 
        elif ( elem.__class__.__name__ == "Tag" ): 
            if ( elem.name == "br" ): 
                rtn = rtn + "\n" 
            else: 
                pass elem = elem.next 
            return rtn

注意事項


ここでは、BeautifulSoupを使う際のハマリどころを記載していきます。これが血となり肉となる。。。

BeautifulSoupのコンストラクタに文字列を渡す際は、必ずUTF-8に変換してから行わなければなりません。 そうしなければ、パースできません。 また、PythonのデフォルトエンコーティングもUTF-8にしてあげておく必要がある、、、といううわさもあります。


Pythonのデフォルトエンコーディングは、「/usr/lib/python2.5/site-packages」配下に 「sitecustomize.py」を作成し、以下のように記述してあげることで設定できます。


#!/usr/bin/python

import sys

sys.setdefaultencoding('utf-8')


タグの中のにタグ表現「[<...>]」があると、BeautifulSoupが動作しなくなる場合があります。 例えば以下のようなXML宣言部があると、BeautifulSoupが全くパースしなくなります。

1
2
<?xml version="1.0" encoding="UTF-8" standalone="no"?> 
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [ <!ATTLIST svg xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink">] >