2011年4月13日水曜日

トラブルシューティング

本セクションでは、人々が、Beautiful Soupを使う上で、共通の問題を取り上げます。


Beautiful Soupに渡した非ASCII文字が表示されないのはなぜか?


もし、次のようなエラーが表示されたら:

"'ascii' codec can't encode character 'x' in position y: ordinal not in range(128)",

おそらく、問題は、Beautiful Soup にあるよりも、むしろ、Pythonのインストールにあります。
Beautiful Soup 中以外で非ASCII文字の出力を試してみてください、同様の問題が起こるでしょう。
例えば、以下のようなコードを動かしてみてください:
latin1word = 'Sacr\xe9 bleu!'
unicodeword = unicode(latin1word, 'latin-1')
print unicodeword

もし、これがうまく動作して、Beautiful Soup ではうまくいかないなら、おそらくBeautiful Soup のバグでしょう。しかしながら、これがうまく動かないなら、問題はあなたのPythonのセットアップにあります。Python は、安全に動作するために、ターミナルに非ASCII文字を送出しません。
この動作を上書きする方法は2つあります。
  1. 簡単な方法は、標準出力を、ISO-Latin-1やUTF-8文字をターミナルに送出するコンバーターにリマップすることです。
    import codecs
    import sys
    streamWriter = codecs.lookup('utf-8')[-1]
    sys.stdout = streamWriter(sys.stdout)
    
    codecs.lookup は、多数のバウンドメソッドと、codecsに関連する他のオブジェクトを返します。
    最後の行は、ストリームの出力をラップ出来るStreamWriter オブジェクトです。

  2. 難しい方法は、デフォルトエンコーディングをISO-Latin-1や、UTF-8に設定するPython インストール中で、sitecustomize.py ファイルを作成することです。
    あなたの全てのPythonプログラムは、各プログラムに何もすることなく、標準出力に対し、そのエンコーディングを使用するでしょう。
    私のインストールの場合、/usr/lib/python/sitecustomize.py に、次のように書いています:
    import sys
    sys.setdefaultencoding("utf-8")
    

PythonのUnicodeサポートに関するさらなる情報は、 プログラマのためのUnicode や、 PythonにおけるUnicode web アプリケーションの全てをご覧ください。 Pythonクックブック中のレシピ 1.20 と 1.21 も非常に役に立ちます。
たとえ、ターミナルの表示がASCIIに制限されていたとしても、Beautiful Soup 使って、UTF-8や他のエンコーディングで、ドキュメントを解析、処理、記述することが出来るということは覚えておいてください。print 関数で、特定の文字を表示できないだけです。


Beautiful Soupに渡したデータが削除された!なぜ?どうして?????


Beautiful Soup は、悪い構造のSGMLを操作できますが、SGMLとは全く異なるものを渡されると、時々、データを失います。 これは、悪い構造のマークアップほど、よくあることではありませんが、webクローラーや、それに近いものを構築するなら、おそらくこの問題に出くわすでしょう。
この問題の唯一の解は、正規表現を用いて、早めにサニタイズを行うことです。 ここに、私とBeautiful Soup のユーザーが発見したコード例をいくつか示します:
  • Beautiful Soup は、データとして,邪悪な形式のXMLを扱います。しかしながら、これは、実際には存在しない、良い形式のXML定義を失います:
    from BeautifulSoup import BeautifulSoup
    BeautifulSoup("< ! FOO @=>")
    # < ! FOO @=>
    BeautifulSoup("<b><!FOO>!</b>")
    # <b>!</b>
    
  • もし、あなたのドキュメントが宣言で始まり、宣言を終了しなければ、Bautiful Soup はドキュメントの残りの部分は宣言の一部だとみなすでしょう。
    もし、ドキュメントが宣言の途中で終了したら、Beautiful Soup は宣言全体を無視します。
    いくつか例を示します:
    from BeautifulSoup import BeautifulSoup
    
    BeautifulSoup("foo<!bar") 
    # foo 
    
    soup = BeautifulSoup("<html>foo<!bar</html>") 
    print soup.prettify()
    # <html>
    #  foo<!bar</html>
    # </html>
    

    これを修正する方法がいくつかあります;1つはここに詳細が載っています。

    Beautiful Soup はまた、ドキュメントの最後で終了しないエンティティ参照を無視します:
    BeautifulSoup("&lt;foo&gt")
    # &lt;foo
    

    実際のwebページで、これは見たことありませんが、多分、そこらにあるでしょう。

  • 不正な形式のコメントがあると、Beautiful Soupはドキュメントの残りを無視します。
    正規表現を用いて不良データをサニタイズするの中で取り上げられています。


BeautifulSoup
クラスが構築した解析木が気に触る!

違った方法でマークアップの解析を行うためには、他のビルトインパーサーを調査するか、あるいは、カスタムパーサーを構築してください。


Beautiful Soupは遅すぎる!


Beautiful Soupは、要素木や、カスタムビルトの SGMLParser のサブクラスのように高速に動作しません。要素木はCで書かれており、SGMLParser を使えば、あなたが望む動作だけを行う、あなただけの小さな Beautiful Soup を書くことが出来ます。 Beautiful Soup のポイントはプログラマーの時間を減らすことであり、処理時間を減らすものではありません。
いわば、あなたが必要とするドキュメントの部分を解析することによってのみ大幅に、Beautiful Soupのスピードを上げることが出来ます、 そして、抽出や分解を使って、不要なオブジェクトをガベージコレクト出来ます。

2011年4月12日火曜日

解析木の修正

今や、あなたは解析木内を、どのように検索すれば良いか知っています。しかし、解析木を修正し、再出力したいことがあるかもしれません。
要素を親の contents の外側に切り取ることが出来ます、しかし、ドキュメントの残りの部分は、切り取った要素へのリファレンスを持ち続けます。
Beautiful Soup は、解析木の内部的な一貫性を維持する間に、解析木を修正するための様々な方法を提供します。

属性値を変更する


タグオブジェクトの属性値を修正するために、ディクショナリの代入を使用出来ます。
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<b id="2">Argh!</b>")
print soup
# <b id="2">Argh!</b>
b = soup.b

b['id'] = 10
print soup
# <b id="10">Argh!</b>

b['id'] = "ten"
print soup
# <b id="ten">Argh!</b>

b['id'] = 'one "million"'
print soup
# <b id='one "million"'>Argh!</b>

また、属性値を削除したり、新しい属性値を追加したり出来ます:
del(b['id'])
print soup
# <b>Argh!</b>

b['class'] = "extra bold and brassy!"
print soup
# <b class="extra bold and brassy!">Argh!</b>

要素の除去


要素への参照を持つと、 extract を使用して、要素を解析木の外側に切り取ることが出来ます。
このコードは、ドキュメントから、全てのコメントを除去します:
from BeautifulSoup import BeautifulSoup, Comment
soup = BeautifulSoup("""1<!--The loneliest number-->
                        <a>2<!--Can be as bad as one--><b>3""")
comments = soup.findAll(text=lambda text:isinstance(text, Comment))
[comment.extract() for comment in comments]
print soup
# 1
# <a>2<b>3</b></a>

このコードはドキュメントの部分木全体を刈り取ります:
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<a1></a1><a><b>Amazing content<c><d></a><a2></a2>")
soup.a1.nextSibling
# <a><b>Amazing content<c><d></d></c></b></a>
soup.a2.previousSibling
# <a><b>Amazing content<c><d></d></c></b></a>

subtree = soup.a
subtree.extract()

print soup
# <a1></a1><a2></a2>
soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>

extract メソッドは、1つの解析木を2つの互いに素な解析木に変換します。
航行メソッドが変更され、そのため、木は決して元に戻らないように見えます:
soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>
subtree.previousSibling == None
# True
subtree.parent == None
# True

1つの要素を別の要素に置換する


replaceWith メソッドは、1ページの要素を抽出し、異なる要素と置換します。
新しい要素は、タグや、可航オブジェクトで構いません。
The new element can be a Tag (possibly with a
whole parse tree beneath it) or a NavigableString.
もし、変更前のプレーン文字を replaceWith に渡せば、文字は、NavigableString に変わります。
航行メンバーは、ドキュメントが最初の場所で、上記方法で解析されたかように変更されます。

ここに、シンプルな例を示します:
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("<b>Argh!</b>")
soup.find(text="Argh!").replaceWith("Hooray!")
print soup
# <b>Hooray!</b>

newText = soup.find(text="Hooray!")
newText.previous
# <b>Hooray!</b>
newText.previous.next
# u'Hooray!'
newText.parent
# <b>Hooray!</b>
soup.b.contents
# [u'Hooray!']

あるタグを別のタグで置換する、より複雑な例を示します:
from BeautifulSoup import BeautifulSoup, Tag
soup = BeautifulSoup("<b>Argh!<a>Foo</a></b><i>Blah!</i>")
tag = Tag(soup, "newTag", [("id", 1)])
tag.insert(0, "Hooray!")
soup.a.replaceWith(tag)
print soup
# <b>Argh!<newTag id="1">Hooray!</newTag></b><i>Blah!</i>

ドキュメントの一部から要素を切り取り、他の場所に組み込むことさえ出来ます:
from BeautifulSoup import BeautifulSoup
text = "<html>There's <b>no</b> business like <b>show</b> business</html>"
soup = BeautifulSoup(text)

no, show = soup.findAll('b')
show.replaceWith(no)
print soup
# <html>There's  business like <b>no</b> business</html>

新しい要素の追加


タグクラスと、解析クラスは insert と呼ばれるメソッドをサポートします。
これは、ちょうど、Pythonのリストの insert メソッドのように動作します:
インデックスをタグの contents メンバーに渡し、新しい要素をその箇所に組み込みます。

これは、前のセクションで、ドキュメント中のタグを新しいタグで置換する時、実演済みです
スクラッチから、解析木全体を構築するために、 insert を使うことが出来ます:
from BeautifulSoup import BeautifulSoup, Tag, NavigableString
soup = BeautifulSoup()
tag1 = Tag(soup, "mytag")
tag2 = Tag(soup, "myOtherTag")
tag3 = Tag(soup, "myThirdTag")
soup.insert(0, tag1)
tag1.insert(0, tag2)
tag1.insert(1, tag3)
print soup
# <mytag><myOtherTag></myOtherTag><myThirdTag></myThirdTag></mytag>

text = NavigableString("Hello!")
tag3.insert(0, text)
print soup
# <mytag><myOtherTag></myOtherTag><myThirdTag>Hello!</myThirdTag></mytag>

1つの解析木中、一箇所だけで、要素が生じます。
もし、
If you
give insert an element that's already connected to a soup object, it
gets disconnected (with extract) before it gets connected
elsewhere.
この例の中で、 NavigableString をsoupの2番目の場所に挿入しようとしましたが、 NavigableString を再び挿入することは出来ませんでした。
NavigableString は移動してしました:
tag2.insert(0, text)
print soup
# <mytag><myOtherTag>Hello!</myOtherTag><myThirdTag></myThirdTag></mytag>

この現象は、要素が前もって、全く異なるスープオブジェクトに属していたとしても起きます。
要素は1つの parent や、1つの nextSibling などしか持っておらず、要素は同時に1箇所にしか存在出来ないためです。

解析木の内部を検索する

先に取り上げたメソッド、 findAllfind は、解析木内のある地点からスタートし、木を下っていきます。
それらは底につくまで、再帰的に contents オブジェクトを反復処理します。

これは、オブジェクト上ではそれらのメソッドを呼び出せないという意味です、なぜなら、これらのオブジェクトは、 contents を持たないからです:これらのオブジェクトは常に、解析木の葉なのです。

しかし、下ることだけが、ドキュメントを反復処理するための唯一の方法ではありません。
解析木を航行する に戻ってみると、他にも多くの方法を示しています: parentnextSibling、などなど。
これらの反復処理の技術は、2つの対応するメソッドを持っています: 1つは、 findAll のように動作するもので、もう一つは、find のように動作するものです。
そして、航行可能オブジェクトはこれらの操作をサポートしているため、タグオブジェクト上や、主な解析オブジェクト上同様、航行可能オブジェクト上で、これらのメソッドを呼び出すことが出来ます。

なぜ、これが役に立つのでしょうか?
時々、欲しいタグや航行可能文字オブジェクトを取得するために、 findAll
find を使うことができないことがあります。
例えば、次のようなHTMLを考えてみてください:
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup('''<ul>
 <li>An unrelated list
</ul>

<h1>Heading</h1>
<p>This is <b>the list you want</b>:</p>
<ul><li>The data you want</ul>''')

欲しいデータを含む<LI>タグを航行するための方法が多数あります。
最も明らかな方法はこれです:
soup('li', limit=2)[1]
# <li>The data you want</li>

この <LI> タグを取得するための非常に安定した方法ではないということは、同様に明らかです。

もし、あなたがこのページを1度スクレイピングするだけなら、問題はありません、しかし、もし、長期間に渡って、何度も繰り返しスクレイピングするつもりなら、このような考慮が重要になります。
もし、不適合リスト(訳注:マッチしない部分のリスト)が別の <LI> タグを増やしたら、欲しいタグの代わりに、そのタグを取得することになるでしょう、そして、あなたのスクリプトは処理を中断するか、間違ったデータを渡すでしょう。
soup('ul', limit=2)[1].li
# <li>The data you want</li>

これは、不適合リストの変更を生き残ることが出来るので、少しマシです。
しかし、もし、ドキュメントが先頭で別の不適合リストを増やした場合、 <LI> は、あなたの欲しいタグの代わりに、そのリストの最初のタグを取得するでしょう。
あなたの欲しいulタグを参照するためのより信頼性の高い方法は、ドキュメントの構造中で、そのタグの場所を熟慮することです。

このHTMLを見ると、あなたは欲しいリストが 「 <H1> タグ直下の <UL> タグ」 だと考えるかもしれません。
そう考える問題は、タグがH1タグの内部に含まれないということです;問題は、この後で起こります。
<H1> タグを得るには十分ですが、そこから、firstfetch を使って <UL> タグを取得する方法が全くありません、なぜなら、これらのメソッドは <H1> タグの
contents しか検索できないからです。
nextnextSibling のメンバーを使って <UL> を航行する必要があります。
s = soup.h1
while getattr(s, 'name', None) != 'ul':
    s = s.nextSibling
s.li
# <li>The data you want</li>

あるいは、こちらの方が、より安定的だと考えるかもしれません:
s = soup.find(text='Heading')
while getattr(s, 'name', None) != 'ul':
    s = s.next
s.li
# <li>The data you want</li>

しかし、これは、通過する際よりも、より多くの問題があります。
本セクション中のメソッドは、役に立つ速記法を提供しています。
それらは、ナビゲーションのメンバーの1つ以上をwhileループを書きたい時にいつでも、使うことが出来ます。

木の中の開始地点を考えると、それらはいくつかの方法で木を航行し、タグの経路と、あなたが指定した基準にマッチする可航文字オブジェクトを保持します。

上記コード例中の最初のループの代わりに、こう書くことが出来ます:
soup.h1.findNextSibling('ul').li
# <li>The data you want</li>

2番目のループの代わりに、こう書く事が出来ます:
soup.find(text='Heading').findNext('ul').li
# <li>The data you want</li>

ループは、findNextSiblingfindNext の呼び出しで置換されます。
本章の残りは、この種の全てのメソッドへのリファレンスとなっています。
再掲、各航行メンバーに対して2つのメソッドがあります:
1つはリストを返すメソッドは findAll で、スカラーを返すメソッドは find です。

最後に一度、例として、おなじみのドキュメントに詰め込んでみましょう:
from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))
print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

findNextSiblings(name, attrs, text, limit, **kwargs)findNextSibling(name, attrs, text, **kwargs)


これらのメソッドは、オブジェクトのタグを収集する nextSibling メンバー、あるいは、あなたが指定した基準にマッチする NavigableText オブジェクトを繰り返し辿ります。
上記ドキュメントにおいて:
paraText = soup.find(text='This is paragraph ')
paraText.findNextSiblings('b')
# [<b>one</b>]

paraText.findNextSibling(text = lambda(text): len(text) == 1)
# u'.'

findPreviousSiblings(name, attrs, text, limit, **kwargs)findPreviousSibling(name, attrs, text, **kwargs)


これらのメソッドは、オブジェクトのタグを収集する previousSibling メンバー、あるいは、あなたが指定した基準にマッチする NavigableText オブジェクトを繰り返し辿ります。
上記ドキュメントにおいて:
paraText = soup.find(text='.')
paraText.findPreviousSiblings('b')
# [<b>one</b>]

paraText.findPreviousSibling(text = True)
# u'This is paragraph '

findAllNext(name, attrs, text, limit, **kwargs)findNext(name, attrs, text, **kwargs)


これらのメソッドは、オブジェクトのタグを収集する next メンバー、あるいは、あなたが指定した基準にマッチする NavigableText オブジェクトを繰り返し辿ります。
上記ドキュメントにおいて:
pTag = soup.find('p')
pTag.findAllNext(text=True)
# [u'This is paragraph ', u'one', u'.', u'This is paragraph ', u'two', u'.']

pTag.findNext('p')
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

pTag.findNext('b')
# <b>one</b>

findAllPrevious(name, attrs, text, limit, **kwargs)findPrevious(name, attrs, text, **kwargs)


これらのメソッドは、オブジェクトのタグを収集する previous メンバー、あるいは、あなたが指定した基準にマッチする NavigableText オブジェクトを繰り返し辿ります。
上記ドキュメントにおいて:
lastPTag = soup('p')[-1]
lastPTag.findAllPrevious(text=True)
# [u'.', u'one', u'This is paragraph ', u'Page title']
# 逆順に注意!

lastPTag.findPrevious('p')
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

lastPTag.findPrevious('b')
# <b>one</b>

findParents(name, attrs, limit, **kwargs) and
findParent(name, attrs, **kwargs)


これらのメソッドは、オブジェクトのタグを収集する parent メンバー、あるいは、あなたが指定した基準にマッチする NavigableText オブジェクトを繰り返し辿ります。
全てのオブジェクトが親に NavigableString を持つ方法が無いので、これらは、text 引数を取りません。
上記ドキュメントにおいて:
bTag = soup.find('b')

[tag.name for tag in bTag.findParents()]
# [u'p', u'body', u'html', '[document]']
# 注記: "u'[document]'" は、解析オブジェクトが自身とマッチしたという意味です。

bTag.findParent('body').name
# u'body'

解析木を検索する

Beautiful Soup は、指定した基準にマッチするタグと可航文字を集めながら、解析木を航行する多くのメソッドを提供します。

Beautiful Soup オブジェクトにマッチする基準を定義する方法は、いくつかあります。
Beautiful Soup の全ての検索メソッドの中で、最も基本となるfindAllを徹底的に分析し、実演してみましょう。
これまでと同じく、以下のドキュメントで実演します。
from BeautifulSoup import BeautifulSoup
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))
print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

ちなみに、本セクションで取り上げる2つのメソッド (findAllfind)は、タグオブジェクトと、トップレベルの解析オブジェクトのみで使用でき、可航文字オブジェクトでは使用できません。「解析木の内部を検索する」で定義されているメソッドは、可航文字オブジェクトでも利用できます。


基本検索メソッド: findAll(name, attrs, recursive, text, limit, **kwargs)



findAll メソッドは解析木を航行し、与えられた地点から開始し、あなたが渡した基準にマッチする、あらゆるタグと可航文字オブジェクトを検索します。

findall メソッドの書き方はこうです:

findAll(name=None, attrs={}, recursive=True, text=None,
limit=None, **kwargs)


これらの引数は、Beautiful Soup API中で、繰り返し現れます。
最も重要な引数は、 name と、キーワード引数です。

  • name 引数は name で、タグの集合を制限します。
    name を制限する方法がいくつかあり、これらは Beautiful Soup API 全体で、何度も繰り返し現れます。


    1. 最も簡単な使用方法は、タグ名を渡すことです。このコードは、ドキュメント中の全てのタグを検索します:
      soup.findAll('b')
      # [<b>one</b>, <b>two</b>]
      

    2. また、正規表現を渡すことも出来ます。このコードは、Bから始まるタグ名を全て検索します:
      import re
      tagsStartingWithB = soup.findAll(re.compile('^b'))
      [tag.name for tag in tagsStartingWithB]
      # [u'body', u'b', u'b']
      

    3. リストやディクショナリで渡すことも出来ます。これら2つの呼び出しは、全ての<TITLE>と、<P>タグを見つけます。
      それらは同じように動作しますが、2番目の呼び出しの方がより高速です。
      soup.findAll(['title', 'p'])
      # [<title>Page title</title>, 
      #  <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>, 
      #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
      
      soup.findAll({'title' : True, 'p' : True})
      # [<title>Page title</title>, 
      #  <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>, 
      #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
      

    4. 名前付きのタグにマッチする、特別な値 True を渡すことが出来ます:
      これは、いわば、あらゆるタグにマッチします。
      allTags = soup.findAll(True)
      [tag.name for tag in allTags]
      [u'html', u'head', u'title', u'body', u'p', u'b', u'p', u'b']
      
      これは、役に立つように見えませんが、 True は属性値を制限する際に、非常に役に立ちます。

    5. Tag オブジェクトを唯一の引き数として取る、呼び出し可能オブジェクトを渡すことが出来ます、これはブール値を返します。
      findAll が出くわす、各タグオブジェクトは、このオブジェクト中に渡され、もし、呼び出しがTrue を返せば、タグはマッチしたとみなされます。

      このコードは属性値をちょうど2つしか持っていないタグを見つけることが出来ます:
      soup.findAll(lambda tag: len(tag.attrs) == 2)
      # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>, 
      #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
      

      このコードは1文字の名前を持ち、属性を持たないタグを検索します:
      soup.findAll(lambda tag: len(tag.name) == 1 and not tag.attrs)
      # [<b>one</b>, <b>two</b>] 

  • キーワード引数は、タグの属性を制限します。
    このシンプルな例は "align" 属性に "center" 値を持つ全てのタグを検索します:
    soup.findAll(align="center")
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>]
    

    name引数と同様に、属性ごとに異なる制限を行うため、キーワード引数に、異なる種類のオブジェクトを渡すことが出来ます。
    上で見たように、属性を単一の値に制限するために、文字を渡すことが出来ます。
    また、正規表現や、リスト、ハッシュ、特別な値として、TrueNone 、あるいは、(値は None かもしれませんが)引数として属性値を持つ呼び出しを渡すことも出来ます。例として:
    soup.findAll(id=re.compile("para$"))
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>,
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    
    soup.findAll(align=["center", "blah"])
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>,
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    
    soup.findAll(align=lambda(value): value and len(value) < 5)
    # [<p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    

    特別値TrueNone は特に興味深いです。
    True は、渡された属性に何らかの値を持つタグにマッチし、None は、渡された属性に値がないタグにマッチします。
    例として:
    soup.findAll(align=True)
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>,
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    
    [tag.name for tag in soup.findAll(align=None)]
    # [u'html', u'head', u'title', u'body', u'b', u'b']
    

    もし、タグ属性に複雑な制限や、連結した制限を課す必要がある場合、上にあるよう、nameに呼び出し可能なオブジェクトを渡し、タグオブジェクトを処理します。

    ここで、あなたは問題に気がつくかもしれません。
    もし、name と言う名前の属性を定義しているタグ付きのドキュメントでは、どうすればよいでしょうか?
    Beautiful Soup のsearchメソッドは、すでに name 引数を定義しているため、 name という名前のキーワード引数を使うことができません。
    また、キーワード引数に for のようなPythonの予約語を使うこともできません。

    Beautiful Soup は、これらのシチュエーションで使用できる attrs と呼ばれる特別な引数を提供します。
    attrs は、ちょうど、キーワード引数のように動作するディクショナリです。
    soup.findAll(id=re.compile("para$"))
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>,
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    
    soup.findAll(attrs={'id' : re.compile("para$")})
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>,
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]
    

    名前がclassforimportのような、Pythonの予約語の属性や、namerecursivelimittextattrsなど、Beautiful Soup 検索メソッドのキーワード引数以外の属性に対して、制限を課す必要がある場合、attrs を使うことが出来ます。
    from BeautifulSoup import BeautifulStoneSoup
    xml = '<person name="Bob"><parent rel="mother" name="Alice">'
    xmlSoup = BeautifulStoneSoup(xml)
    
    xmlSoup.findAll(name="Alice")
    # []
    
    xmlSoup.findAll(attrs={"name" : "Alice"})
    # [parent rel="mother" name="Alice"></parent>]
    

    CSSによる検索

    attrs 引数はあるものがなければ、かなり曖昧な機能だったでしょう:それはCSSです。
    特定のCSSクラスを持つタグを探すのに、とても役に立ちます、しかし、CSS属性の class もPythonの予約語なのです。

    soup.find("tagName", { "class" : "cssClass" })
    として検索出来るでしょうが、このような一般的な操作には、たくさんのコードが必要です。
    実際、ディクショナリの代わりに、文字を attrs に渡すことが出来ます。
    渡す文字は、CSSクラスを制限するのに使われます。
    from BeautifulSoup import BeautifulSoup
    soup = BeautifulSoup("""Bob's <b>Bold</b> Barbeque Sauce now available in 
                            <b class="hickory">Hickory</b> and <b class="lime">Lime</a>""")
    
    soup.find("b", { "class" : "lime" })
    # <b class="lime">Lime</b>
    
    soup.find("b", "hickory")
    # <b class="hickory">Hickory</b>
    
  • text は、タグの代わりに、可航文字オブジェクトを検索出来るようにする引数です。
    その値は、文字列、正規表現、リスト、ディクショナリ、True
    None 、引数に可航文字オブジェクトを持つ呼び出し、を取れます:
    soup.findAll(text="one")
    # [u'one']
    soup.findAll(text=u'one')
    # [u'one']
    
    soup.findAll(text=["one", "two"])
    # [u'one', u'two']
    
    soup.findAll(text=re.compile("paragraph"))
    # [u'This is paragraph ', u'This is paragraph ']
    
    soup.findAll(text=True)
    # [u'Page title', u'This is paragraph ', u'one', u'.', u'This is paragraph ', 
    #  u'two', u'.']
    
    soup.findAll(text=lambda(x): len(x) < 12)
    # [u'Page title', u'one', u'.', u'two', u'.']
    

    text を使う場合、 name に渡すあらゆる値と、キーワード引数は無視されます。
  • recursive は、Beautiful Soupに、解析木の全ての経路を下るかどうか、あるいは、タグの直下の子や解析木を見るだけかどうかを伝える(デフォルトではTrueの)ブール値引数です。
    ここに違いを示します:
    [tag.name for tag in soup.html.findAll()]
    # [u'head', u'title', u'body', u'p', u'b', u'p', u'b']
    
    [tag.name for tag in soup.html.findAll(recursive=False)]
    # [u'head', u'body']
    

    recursive が false の場合、<HTML> タグ直下の子だけが検索されます。
    もし、それが検索する必要のあるもの全てだと分かっているなら、この方法である程度時間を省略できます。
  • limit 引数を設定すると、argument lets you
    stop the search once Beautiful Soup finds a certain number of matches.

    もし、ドキュメント中に千テーブルあるけれども、最初の4つしか必要無いなら、 limit に4を渡せば時間を省略出来ます。デフォルトではlimitは無制限です。
    soup.findAll('p', limit=1)
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>]
    
    soup.findAll('p', limit=100)
    # [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>, 
    #  <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>]

タグの呼び出しは findall を呼び出すのに似ている

小さなショートカットがあります。解析オブジェクトや、タグを関数のように呼び出す場合、 あらゆるfindallの引き数を渡すことが出来ます、これは findall を呼び出すのと同じです。 上記のドキュメントに関しては:
soup(text=lambda(x): len(x) < 12)
# [u'Page title', u'one', u'.', u'two', u'.']

soup.body('p', limit=1)
# [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>] 

find(name, attrs, recursive, text, **kwargs)

O.K. では、他の検索メソッドを見ていきましょう。それらは findAll とほぼ同様の引数を取ります。 find メソッドは、あらゆるマッチングオブジェクトを検索するというを除き、findAll と同じです、こちらは、最初のオブジェクトだけを検索します。 結果の集合に、 limit 1を強制したり、結果の配列から1つの結果を抜き出すようなものです。 上記のドキュメントに関しては:
soup.findAll('p', limit=1)
# [<p id="firstpara" align="center">This is paragraph <b>one</b>.</p>]

soup.find('p', limit=1)
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

soup.find('nosuchtag', limit=1) == None
# True
一般的に、( findAllfindNextSiblings のような)複数形の名前の検索メソッドを見ると、そのメソッドは limit 引数を取り、結果リストを返します。 ( findfindNextSibling のような)複数形ではない名前の検索メソッドを見ると、そのメソッドは limit 引数を取らず、単一の結果を返します。

firstに何が起こるか?

以前のバージョンの Beautiful Soup は、 firstfetchfetchPrevious などのメソッドを持っていました。 これらのメソッドはまだありますが、それらは推奨されておらず、まもなく削除されるかもしれません。 これらの全ての名前の効果はとてもややこしいです。 新たな名前は一貫性を持っています: 上記で述べたように、名前が複数形や、Allを含んでいたら、複数のオブジェクトを返し、そうでなければ、1つのオブジェクトを返します。

解析木を航行する

全てのタグオブジェクトは、(実際のメンバーの値はNoneかもしれませんが)以下で示されているメンバーを全て持っています。
可航文字オブジェクトは、 contentsstring以外、全て持っています。

parent (親)


先の例では、 <HEAD> タグの parent<HTML> タグです。
<HTML> タグの parent は、 BeautifulSoup パーサーオブジェクト自身です。
パーサーオブジェクトの parent はNoneです。
このように、parent を追うことで、解析木を遡ることが出来ます:
soup.head.parent.name
# u'html'
soup.head.parent.parent.__class__.__name__
# 'BeautifulSoup'
soup.parent == None
# True

contents (内容)


parent を使うと、解析木を遡ります。 contents を使うと、解析木を下ります。
contents は、ページ要素中に含まれているタグと可航文字オブジェクトの順序付きリストです。
最上位のパーサーオブジェクトとタグオブジェクトのみ、 contents を持っています。可航文字オブジェクトは単なる文字であり、サブ要素を含むことが出来ません、そのため、contentsを持っていません。

先の例では、最初の <P> タグは、可航文字("This is paragraph ")と、<B> タグと、別の可航文字(".")を含むリストです。
<B> タグの contents:可航文字("one")を含むリストです。
pTag = soup.p
pTag.contents
# [u'This is paragraph ', <b>one</b>, u'.']
pTag.contents[1].contents
# [u'one']
pTag.contents[0].contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

string (文字)


参考までに、タグが子ノードを1つしか持っておらず、そのノードが文字であるとき、子ノードは tag.contents[0] 及び tag.string とすれば利用できるようになります。

先の例の中で、soup.b.string は、Unicode文字 "one" を表す可航文字です。
これは、解析木中の最初の <B> タグの中に含まれる文字です。
soup.b.string
# u'one'
soup.b.contents[0]
# u'one'

しかし、解析木中の<P>タグは、子を1つ以上持っているため、soup.p.stringはNoneです。
soup.head.string もNoneです。
たとえ、 <HEAD> タグが子を1つしか持っていなくても、この子は( <TITLE> )タグであり、可航文字ではありません。
soup.p.string == None
# True
soup.head.string == None
# True

nextSibling (次の兄弟) と previousSibling (前の兄弟)


これらのメンバーを使うと、解析木の同階層の、次のノードや前のノードに移動できます。
先のドキュメントでは、<HEAD> タグの nextSibling は、 <BODY> タグです、これは、 <BODY> タグが <html> タグの直下にある次のノードだからです。
<BODY>nextSibling はNoneです、これは、 <HTML> タグの直下に、他のノードがないためです。
soup.head.nextSibling.name
# u'body'
soup.html.nextSibling == None
# True

逆に、<BODY>タグの previousSiblingは、<HEAD> タグで、<HEAD> タグの previousSiblingはNoneです。
soup.body.previousSibling.name
# u'head'
soup.head.previousSibling == None
# True

さらに例をいくつか:
最初の <P> タグの nextSibling は、2番目の <P> タグです。
2番目の <P> タグ中にある <B> タグの previousSiblingは、可航文字 "This is paragraph" です。
この可航文字の previousSiblingNone であり、最初の <P> タグ中の何かではありません。
soup.p.nextSibling
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

secondBTag = soup.findAlll('b')[1]
secondBTag.previousSibling
# u'This is paragraph'
secondBTag.previousSibling.previousSibling == None
# True

next (次) と previous (前)


これらのメンバーを使えば、解析木中に現れる順番ではなく、パーサーによって処理される順番で、ドキュメントの要素中を移動できます。

例えば、 <HEAD> タグの next<TITLE> タグであり、<BODY> タグではありません。
これは、元のドキュメント中で、 <TITLE> タグが <HEAD> タグの直後に来るためです。

soup.head.next
# u'title'
soup.head.nextSibling.name
# u'body'
soup.head.previous.name
# u'html'
ここで、 nextprevious は関係しており、タグのcontents は、タグのnextSiblingの前に来ます。

あなたは通常、このメンバーを使う必要はありませんが、この方法は、時々、解析木中で何らかの変化するものを取得する、最も簡単な方法になります。


タグの繰り返し処理


タグをリストのように扱うことで、タグのcontents を繰り返し処理できます。
これは、便利なショートカットです。
同様に、タグが子ノードをいくつ持っているかを見るために、 len(tag.contents)の代わりに、len(tag)を呼ぶことも出来ます。
先のドキュメントにおいて:
for i in soup.body:
    print i
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

len(soup.body)
# 2
len(soup.body.contents)
# 2

tag名をメンバーとして使用する


欲しいタグ名を、解析木のメンバーやタグオブジェクトのように扱うことで、解析木を航行するのは簡単です。
ここまでの例でもこのことを行なってきました。
先のドキュメントにおいて、 soup.head は、ドキュメント中の(たまたま1つですが)最初の <HEAD> タグを返します。
soup.head
# <head><title>Page title</title></head>

一般的に、 mytag.foo の呼び出しは、たまたま <FOO> であるmytag の最初の子を返します。
もし、mytagの下に <FOO>タグが全くなければ、mytag.foo は、Noneを返します。

これを使えば、解析木を高速で航行出来ます:
soup.head.title
# <title>Page title</title>

soup.body.p.b.string
# u'one'

また、これを使うと、解析木の特定箇所に速やかにジャンプ出来ます。
例えば、 <HEAD>タグの外側の変な場所にある <TITLE> タグについて、心配しなくて良いなら、HTMLドキュメントのタイトルを入手するには、単に、 soup.title を使えば良いでしょう。
soup.head.title を使う必要はありません。
soup.title.string
# u'Page title'

soup.p は、タグがどの場所にあっても、ドキュメント中の最初の <P> タグにジャンプします。
soup.table.tr.td は、ドキュメント中の最初のテーブルの最初の行の最初の列にジャンプします。

最初のメソッドに対する、これらのメンバーの正確なエイリアスは、以下で取り上げられています。
エイリアスを使えば、周知の解析木の関心ある部分をクローズアップするのが非常に簡単になるため、ここで触れています。


この慣用句の代替形式を使えば、.foo の代わりに、 .fooTag として、最初の <FOO> タグにアクセス出来ます。

例えば、 soup.table.tr.td は、soup.tableTag.trTag.tdTag や、soup.tableTag.tr.tdTag としても表現できます、
あなたが自分の行っていることについて、もっと明示したかったり、競合するBeautiful Soupのメソッド名やメンバー名と競合するタグ名を持つXMLを解析しているなら、この方法は役に立ちます。
from BeautifulSoup import BeautifulStoneSoup
xml = '<person name="Bob"><parent rel="mother" name="Alice">'
xmlSoup = BeautifulStoneSoup(xml)

xmlSoup.person.parent                      # A Beautiful Soup member
# <person name="Bob"><parent rel="mother" name="Alice"></parent></person>
xmlSoup.person.parentTag                   # A tag name
# <parent rel="mother" name="Alice"></parent>

もし、あなたが(ハイフン名のように)正式なPython識別子ではないタグ名を探しているなら、 findを使う必要があります。

2011年4月10日日曜日

解析木

ほとんどの場合、あなたは解析木ーBeautiful Soupがドキュメントを解析する際に構築するデータ構造ーに興味を持っているでしょうが、これまでは、ドキュメントの読み込みと書き出しに焦点を当ててきました。

( BeautifulSoup BeautifulStoneSoupの)パーサーオブジェクトは、 XML や HTMLドキュメントの構造に対応するよう、深くネストされ、適切に連結されたデータ構造です。
パーサーオブジェクトは2つの異なるオブジェクト型を持っています:
<TITLE> タグや、 <B>タグのようにタグに対応する タグオブジェクトと、"Page title" や "This is paragraph"のような文字に対応する、可航文字オブジェクトです。

また、特別なXML構造に対応する、 (CData、コメント、宣言、処理命令などの)可航文字のサブクラスもあります。
可航文字のサブクラスは、それらを出力する時に、付随する余分なデータを持つという点以外は、可航文字のように動作します。
ここに、コメントを含んだドキュメントがあります:
from BeautifulSoup import BeautifulSoup
import re
hello = "Hello! <!--I've got to be nice to get what I want.-->"
commentSoup = BeautifulSoup(hello)
comment = commentSoup.find(text=re.compile("nice"))

comment.__class__
# <class 'BeautifulSoup.Comment'>
comment
# u"I've got to be nice to get what I want."
comment.previousSibling
# u'Hello! '

str(comment)
# "<!--I've got to be nice to get what I want.-->"
print commentSoup
# Hello! <!--I've got to be nice to get what I want.-->

さて、本ドキュメントの冒頭で使われている文章をより詳しく見てみましょう。
from BeautifulSoup import BeautifulSoup 
doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))

print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

タグの属性


タグオブジェクト可航文字オブジェクトは、多数の役立つメンバーを持っており、それらのほとんどについては、「解析木を航行する」と、「解析木を検索する」の章で取り上げています。

ですが、ここでは、タグオブジェクトの一面を取り上げます:それは属性です。

SGML タグは属性を持っています:. 例えば、先の例のHTML中のそれぞれの <P> タグは、"id"属性と、"align" 属性を持っています。
あなたは、タグオブジェクトをディクショナリのように扱うことで、タグ属性にアクセスできます:
firstPTag, secondPTag = soup.findAll('p')

firstPTag['id']
# u'firstPara'

secondPTag['id']
# u'secondPara'

可航文字オブジェクトには属性がありません; タグオブジェクトのみ、属性があります。

ドキュメントを表示する

あなたは、str関数や、prettifyメソッド、renderContentsメソッドを使って、Beautiful Soupドキュメント(や、その一部)を文字列に変換できます。
また、ドキュメント全体をUnicode文字列として取得するために、unicode関数を使う
ことも出来ます。

prettify メソッドは、ドキュメントの構造を明白にするための、戦略的な改行とスペースを追加します。
また、ホワイトスペースだけを含んだテキストノードを除去します、これによって、XMLドキュメントの意味が変わってしまうかもしれません。
str 関数とunicode 関数はホワイトスペースだけを含んだテキストノードを除去せず、また、それぞれのノード間にホワイトスペースを追加したりしません。

ここに例があります。
from BeautifulSoup import BeautifulSoup
doc = "<html><h1>Heading</h1><p>Text"
soup = BeautifulSoup(doc)

str(soup)
# '<html><h1>Heading</h1><p>Text</p></html>'
soup.renderContents()
# '<html><h1>Heading</h1><p>Text</p></html>'
soup.__str__()
# '<html><h1>Heading</h1><p>Text</p></html>'
unicode(soup)
# u'<html><h1>Heading</h1><p>Text</p></html>'

soup.prettify()
# '<html>\n <h1>\n  Heading\n </h1>\n <p>\n  Text\n </p>\n</html>'

print soup.prettify()
# <html>
#  <h1>
#   Heading
#  </h1>
#  <p>
#   Text
#  </p>
# </html>

注記: strrenderContents はドキュメント中のタグで使用された際、異なる結果を返します。 str はタグとその内容を表示し、renderContents は、内容のみを表示します。
heading = soup.h1
str(heading)
# '<h1>Heading</h1>'
heading.renderContents()
# 'Heading'

__str__ や、 prettify や、 renderContents を呼び出す時、出力エンコーディングを指定できます。デフォルトエンコーディング(strが使用するエンコーディング) は、UTF-8です。
ここでは、ISO-8851-1 文字列を解析し、そして、同じ文字列を異なるエンコーディングで出力する例を示します:
from BeautifulSoup import BeautifulSoup
doc = "Sacr\xe9 bleu!"
soup = BeautifulSoup(doc)
str(soup)
# 'Sacr\xc3\xa9 bleu!'                          # UTF-8
soup.__str__("ISO-8859-1")
# 'Sacr\xe9 bleu!'
soup.__str__("UTF-16")
# '\xff\xfeS\x00a\x00c\x00r\x00\xe9\x00 \x00b\x00l\x00e\x00u\x00!\x00'
soup.__str__("EUC-JP")
# 'Sacr\x8f\xab\xb1 bleu!'

元のドキュメントがエンコーディング宣言を含んでいた場合、Beautiful Soupはドキュメントを文字列に戻す際、新しいエンコーディングを宣言するために、宣言を上書きします。
これは、あなたがHTMLドキュメントを読み込み、それを出力する場合、HTMLがクリーンアップされるだけではなく、透過的にUTF-8に変換されることを意味します。

ここに、HTMLの例を示します:
from BeautifulSoup import BeautifulSoup
doc = """<html>
<meta http-equiv="Content-type" content="text/html; charset=ISO-Latin-1" >
Sacr\xe9 bleu!
</html>"""

print BeautifulSoup(doc).prettify()
# <html>
#  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
#  Sacré bleu!
# </html>

ここに、XMLの例を示します:
from BeautifulSoup import BeautifulStoneSoup
doc = """<?xml version="1.0" encoding="ISO-Latin-1">Sacr\xe9 bleu!"""

print BeautifulStoneSoup(doc).prettify()
# <?xml version='1.0' encoding='utf-8'>
# Sacré bleu!

Beautiful Soupは、あなたにUnicodeを渡す、Dammit

あなたのドキュメントは、解析される時までにUnicodeに変換されます。Beautiful Soupは自身のデータ構造中にUnicode文字列だけを保存します。
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("Hello")
soup.contents[0]
# u'Hello'
soup.originalEncoding
# 'ascii'

ここに、UTF-8でエンコードされた日本語ドキュメントを用いた例を示します:
from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf")
soup.contents[0]
# u'\u3053\u308c\u306f'
soup.originalEncoding
# 'utf-8'
str(soup)
# '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf'
# 注記: このビットはEUC-JPを使用します、そのため、cjkcodecsをインストールしているか、
# Python 2.4を使用している場合のみ動作します。
soup.__str__('euc-jp')
# '\xa4\xb3\xa4\xec\xa4\xcf'

Beautiful Soup は、あなたがBeautiful Soupに渡したドキュメントのエンコーディングを判定し、Unicodeに変換するために、UnicodeDammit と呼ばれるクラスを使います
もし、(ドキュメントの解析に Beautiful Soup を使わないで)他のドキュメントでエンコーディングの判定とUnicodeへの変換を行う必要がある場合、UnicodeDammit単独で使うことも出来ます。
 
UnicodeDammitは、Universal Feed Parser由来のコードにかなり依拠しています。
Pythonのバージョン2.4よりも古いものを使っている時は、より多くのコーデック、特にCJKコーデックをサポート出来るために、必ず cjkcodecsiconvcodec をダウンロードし、インストールしてください。
また、 より良い自動判定を行うには、chardet ライブラリをインストールしてください。

Beautiful Soupは、優先度の高い順に、以下のエンコーディングを試します。
ドキュメントをUnicodeに変換するには:

  • スープコンストラクタに、fromEncoding 引数として渡したエンコーディング
  • ドキュメント中で見つかったエンコーディング: 例えば、XML 宣言や(HTML ドキュメントでは) http-equiv META タグ。もし、Beautiful Soup がドキュメント中にこの種のエンコーディングを見つけたら、Beautiful Soup は再度、先頭からドキュメントを解析し、新しいエンコード化を試します。もし、あなたがエンコーディングを明白に指定し、それが正しく動作した場合だけが 唯一の例外です:この場合、ドキュメント中に見つけるあらゆるエンコーディングを無視します。
  • ファイル先頭の数バイトを見ることで、探ったエンコーディング。
    もし、この段階でエンコーディングが判定されたら、UTF-*か、EBCEICか、ASCIIのうちのいずれかでしょう。
  • chardetライブラリインストールしていた場合、同ライブラリによって探ったエンコーディング
  • UTF-8
  • Windows-1252

Beautiful Soup は、推測出来る場合は、ほとんどいつも正しく推測します。
しかし、宣言がなく、エンコーディングがおかしいドキュメントの場合は、しばしば推測することが出来ません。
エンコーディングは、Windows-1252になりますが、これはおそらく間違っています。
ここで、Beautiful Soupがエンコーディングを間違って推測する、EUC-JP の例を示します。
(再掲:この例はEUC-JPを使っているので、 Python 2.4を使用しているか、cjkcodecsをインストールしている場合のみ動作します。)
from BeautifulSoup import BeautifulSoup
euc_jp = '\xa4\xb3\xa4\xec\xa4\xcf'
soup = BeautifulSoup(euc_jp)
soup.originalEncoding # 'windows-1252'
str(soup) # '\xc2\xa4\xc2\xb3\xc2\xa4\xc3\xac\xc2\xa4\xc3\x8f'     # 間違い!

しかし、もし、あなたが fromEncodingを伴ってエンコーディングを指定したら、Beautiful Soupはドキュメントを正確に解析し、UTF-8に変換したり、EUC-JPに戻すことができます。
soup = BeautifulSoup(euc_jp, fromEncoding="euc-jp")
soup.originalEncoding
# 'windows-1252'

str(soup)
# '\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf'                 # 正しい!

soup.__str__(self, 'euc-jp') == euc_jp
# True

もし、Windows-1252エンコーディング(やISO-8859-1、ISO-8859-2など、同様のエン コーディング)のドキュメントをBeautiful Soupに渡した場合、Beautiful SoupはスマートクォートとWindows特殊文字を見つけ、破壊するでしょう。
Beautiful Soupはそれらの文字をUnicodeの対応する文字に変換するのではなく、むしろ、それらを(BeautifulSoupだと)HTMLエンティティや(BeautifulStoneSoupだと)XMLエンティティに変換します。
これを回避するためには、スープコンストラクターにsmartQuotesTo=None を渡すことも出来ます:そして、スマートクォートは他のあらゆるネイティブエンコーディング文字のように、Unicodeに変換されるでしょう。
また、 BeautifulSoupBeautifulStoneSoupのデフォルトの動作を変更するために、 smartQuotesToに"xml" か "html"を渡すことが出来ます。
from BeautifulSoup import BeautifulSoup, BeautifulStoneSoup
text = "Deploy the \x91SMART QUOTES\x92!"

str(BeautifulSoup(text))
# 'Deploy the &lsquo;SMART QUOTES&rsquo;!'

str(BeautifulStoneSoup(text))
# 'Deploy the &#x2018;SMART QUOTES&#x2019;!'

str(BeautifulSoup(text, smartQuotesTo="xml"))
# 'Deploy the &#x2018;SMART QUOTES&#x2019;!'

BeautifulSoup(text, smartQuotesTo=None).contents[0]
# u'Deploy the \u2018SMART QUOTES\u2019!'

2011年4月9日土曜日

ドキュメントの解析

Beautiful SoupコンストラクタはXMLやHTMLドキュメントを文字形式(やファイルのようなオブジェクトを開いて)で引数に取ります。また、Beautiful Soupコンストラクタはドキュメントを解析し、対応するデータ構造をメモリー中に作成します。

Beautiful Soupに完全な形式のドキュメントを渡したら、解析されたデータ構造は、元のドキュメントと全く同じに見えます。しかし、ドキュメントに何か問題がある場合、Beautiful Soupはデータ構造に対して、合理的な構造を見つけ出すために、ヒューリスティックを使用します。


HTMLの解析


HTMLドキュメントを解析するために、Beautiful Soupクラスを使用します。ここでは、BeautifulSoupが知っていることをいくつか示します:
  • ネスト出来るタグ(<BLOCKQUOTE>)があり 、出来ないタグ(<P>)がある。
  • Table タグと list タグには、自然なネストの順序がある。例えば、
    <TD> タグは <TR> タグ中にあり、それ以外の箇所では使われない。
  • <SCRIPT> タグの中身はHTMLとして解析されるべきではない。
  • <META> タグはドキュメントのエンコーディングを指定するかもしれない。
ここで、実際の動作をご覧ください:
from BeautifulSoup import BeautifulSoup
html = "<html><p>Para 1<p>Para 2<blockquote>Quote 1<blockquote>Quote 2"
soup = BeautifulSoup(html)
print soup.prettify()
# <html>
#  <p>
#   Para 1
#  </p>
#  <p>
#   Para 2
#   <blockquote>
#    Quote 1
#    <blockquote>
#     Quote 2
#    </blockquote>
#   </blockquote>
#  </p>
# </html>
BeautifulSoupは、たとえ元のドキュメントに終了タグがなくても、終了タグを置くのにふさわしい場所を見つけることに注意してください。
元のドキュメントは正しいHTMLではありませんが、それほど悪いHTMLでもありません。

ここに、本当にひどいドキュメントがあります。
終了タグの欠落以外の問題の中でも、 <FORM>タグが <TABLE>タグの外側で始まり、 <TABLE>タグの中で終了しています(このようなHTMLは主なweb企業によって運営されているウェブサイトで見つけられます。)
from BeautifulSoup import BeautifulSoup
html = """
<html>
<form>
 <table>
 <td><input name="input1">Row 1 cell 1
 <tr><td>Row 2 cell 1
 </form> 
 <td>Row 2 cell 2<br>This</br> sure is a long cell
</body> 
</html>"""
Beautiful Soupは、このドキュメントも同様に処理します:
print BeautifulSoup(html).prettify()
# <html>
#  <form>
#   <table>
#    <td>
#     <input name="input1" />
#     Row 1 cell 1
#    </td>
#    <tr>
#     <td>
#      Row 2 cell 1
#     </td>
#    </tr>
#   </table>
#  </form>
#  <td>
#   Row 2 cell 2
#   <br />
#   This 
#   sure is a long cell
#  </td>
# </html>
テーブルの最後のセルが <TABLE> タグの外側にあります;
Beautiful Soupは、 <FORM> タグを閉じる時、 <TABLE> タグを閉じることを決定します。
おそらく、元のドキュメントの著者は、 <FORM> タグがテーブルの終わりまで拡張されると考えたのでしょう、しかし、Beautiful Soupはそのことを知る由もありません。
このような変な場合でも、Beautiful Soupは不良ドキュメントを解析し、全てのデータにアクセスできるようにしてくれます。


XMLの解析


BeautifulSoup クラスは、HTML作者の意図を推測するため、webブラウザのようにヒューリスティックがたくさんあります。
しかし、 XML は修正タグの集合を持っておらず、そのため、これらのヒューリスティックは適用されません。したがって、 BeautifulSoup は、 XML をうまく扱えません.
XML ドキュメントを解析するためには、BeautifulStoneSoup クラスを使ってください。
BeautifulStoneSoup は、あらゆるXML方言についての特別な知識がいらず、タグのネストについての極めて単純なルールを持つ、一般的なクラスです:
ここで、実際の動作をご覧ください:
from BeautifulSoup import BeautifulStoneSoup
xml = "<doc><tag1>Contents 1<tag2>Contents 2<tag1>Contents 3"
soup = BeautifulStoneSoup(xml)
print soup.prettify()
# <doc>
#  <tag1>
#   Contents 1
#   <tag2>
#    Contents 2
#   </tag2>
#  </tag1>
#  <tag1>
#   Contents 3
#  </tag1>
# </doc> 
BeautifulStoneSoup の最も一般的な欠点は、自己終了タグを知らないことです。
HTML は、自己終了タグの修正集合を持っていますが、 XML では、DTD が何と言っているかに依ります。
自己終了タグ名を、selfClosingTags引数としてコンストラクタに渡すことで、BeautifulStoneSoup に、特定のタグが自己終了タグだと教えることが出来ます:
from BeautifulSoup import BeautifulStoneSoup
xml = "<tag>Text 1<selfclosing>Text 2"
print BeautifulStoneSoup(xml).prettify()
# <tag>
#  Text 1
#  <selfclosing>
#   Text 2
#  </selfclosing>
# </tag>

print BeautifulStoneSoup(xml, selfClosingTags=['selfclosing']).prettify()
# <tag>
#  Text 1
#  <selfclosing />
#  Text 2
# </tag>

動作しない場合には


これら2つとは異なるヒューリスティックを持つ他の解析クラスもあります。また、解析木をサブクラス化、カスタマイズ出来、解析木に自分のヒューリスティックを渡すことも出来ます。

クイックスタート

Beautiful Soupを ここ から入手します。
更新履歴には、ver3.0と、それ以降のバージョン間の違いが載っています。


次のうち、いずれか1行を使い、あなたのアプリケーションに、Beautiful Soupをインクルードします:
from BeautifulSoup import BeautifulSoup      # HTMLの処理用
from BeautifulSoup import BeautifulStoneSoup # XMLの処理用
import BeautifulSoup                         # いずれも使う場合

Beautiful Soupの基本機能を実演するためのコードを、いくつかここに示します。自分でコードを動かすなら、Pythonセッションに、コピー&ペーストできます:
from BeautifulSoup import BeautifulSoup
import re

doc = ['<html><head><title>Page title</title></head>',
       '<body><p id="firstpara" align="center">This is paragraph <b>one</b>.',
       '<p id="secondpara" align="blah">This is paragraph <b>two</b>.',
       '</html>']
soup = BeautifulSoup(''.join(doc))

print soup.prettify()
# <html>
#  <head>
#   <title>
#    Page title
#   </title>
#  </head>
#  <body>
#   <p id="firstpara" align="center">
#    This is paragraph
#    <b>
#     one
#    </b>
#    .
#   </p>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

スープを航行する方法をいくつかここに示します:
soup.contents[0].name
# u'html'

soup.contents[0].contents[0].name
# u'head'

head = soup.contents[0].contents[0]
head.parent.name
# u'html'

head.next
# <title>Page title</title>

head.nextSibling.name
# u'body'

head.nextSibling.contents[0]
# <p id="firstpara" align="center">This is paragraph <b>one</b>.</p>

head.nextSibling.contents[0].nextSibling
# <p id="secondpara" align="blah">This is paragraph <b>two</b>.</p>

スープ中で特定のタグや、特定のプロパティを持ったタグを検索する方法を、いくつかここに示します:
titleTag = soup.html.head.title
titleTag
# <title>Page title</title>

titleTag.string
# u'Page title'

len(soup('p'))
# 2

soup.findAll('p', align="center")
# [<p id="firstpara" align="center">This is paragraph <b>one</b>. </p>]

soup.find('p', align="center")
# <p id="firstpara" align="center">This is paragraph <b>one</b>. </p>

soup('p', align="center")[0]['id']
# u'firstpara'

soup.find('p', align=re.compile('^b.*'))['id']
# u'secondpara'

soup.find('p').b.string
# u'one'

soup('p')[1].b.string
# u'two'

スープを修正するのは簡単です:
titleTag['id'] = 'theTitle'
titleTag.contents[0].replaceWith("New title")
soup.html.head
# <head><title id="theTitle">New title</title></head>

soup.p.extract()
soup.prettify()
# <html>
#  <head>
#   <title id="theTitle">
#    New title
#   </title>
#  </head>
#  <body>
#   <p id="secondpara" align="blah">
#    This is paragraph
#    <b>
#     two
#    </b>
#    .
#   </p>
#  </body>
# </html>

soup.p.replaceWith(soup.b)
# <html>
#  <head>
#   <title id="theTitle">
#    New title
#   </title>
#  </head>
#  <body>
#   <b>
#    two
#   </b>
#  </body>
# </html>

soup.body.insert(0, "This page used to have ")
soup.body.insert(2, " &lt;p&gt; tags!")
soup.body
# <body>This page used to have <b>two</b> &lt;p&gt; tags!</body>

(※訳注:ICCサイトが変更されたため、以下の例は動作しません。)
実世界の例をここに示します。ICC Commercial Crime Services weekly piracy reportのサイトを取得し、Beautiful Soupを用いて解析し、海賊事件を引き出します:
import urllib2
from BeautifulSoup import BeautifulSoup

page = urllib2.urlopen("http://www.icc-ccs.org/prc/piracyreport.php")
soup = BeautifulSoup(page)
for incident in soup('td', width="90%"):
    where, linebreak, what = incident.contents[:3]
    print where.strip()
    print what.strip()
    print

Beautiful Soup ドキュメント 日本語訳

原著:Leonard Richardson
(leonardr@segfault.org)

和訳:Hirotaka Shioji

这份文档也有中文版了 (本ドキュメントは、中国語訳版もあります。)


Beautiful Soupは、Python用のHTML/XMLパーサーで、不正なマークアップでさえ、解析木に変換できます。
Beautiful Soupは、解析木を航行、検索、修正するための、シンプルで慣用的な方法を提供します。
Beautiful Soupは、一般的に、プログラマーの労働時間や労働日数を減らします。
Rubyful Soupと呼ばれるRuby用のものもあります。


本ドキュメントは、Beautiful Soup version 3.0の主な特徴を、例を混じえて、全て示しています。
本ドキュメントは、ライブラリがどういうことに役立つのか、どのように動作するのか、どのように使うのか、どうすればしたいことを出来るのか、思惑がはずれた時にはどのように動作するのかを示します。

コンテンツ一覧

  • クイックスタート
  • ドキュメントの解析
    • HTMLの解析
    • XMLの解析
    • 動作しない場合には
  • Beautiful SoupはあなたにUnicodeを渡す、 Dammit
  • ドキュメントを表示する
  • 解析木
    • タグの属性
  • 解析木を航行する
    • parent
    • contents
    • string
    • nextSiblingpreviousSibling
    • nextprevious
    • Tagを反復処理する
    • タグ名をメンバーとして使う
  • 解析木を検索する
    • 基本の検索メソッド: findAll(name, attrs, recursive, text, limit, **kwargs)
      • CSSのクラスで検索する
      • findallを呼び出すように、タグを呼び出す
    • find(name, attrs, recursive, text, **kwargs)
    • firstに何が起こるか?
  • 解析木の内部をする
    • findNextSiblings(name, attrs, text, limit, **kwargs)findNextSibling(name, attrs, text, **kwargs)
    • findPreviousSiblings(name, attrs, text, limit, **kwargs)findPreviousSibling(name, attrs, text, **kwargs)
    • findAllNext(name, attrs, text, limit, **kwargs)findNext(name, attrs, text, **kwargs)
    • findAllPrevious(name, attrs, text, limit, **kwargs)findPrevious(name, attrs, text, **kwargs)
  • 解析木を修正する
    • 属性値を変更する
    • 要素を除去する
    • 要素を別の要素に置換する
    • 新しい要素を追加する
  • トラブルシューティング
    • Beautiful Soupに渡した非ASCII文字が表示されないのはなぜか?
    • Beautiful Soupに渡したデータが削除された!なぜ?どうして?????
    • Beautiful Soupは遅すぎる!
  • 高度なテーマ
    • Generators
    • 他のビルトインパーサー
    • パーサーをカスタマイズする
    • エンティティ変換
    • 正規表現を用いた不良データのサニタイズ
    • SoupStrainersを使って楽しむ
    • ドキュメントの一部を解析することでパフォーマンスを改善する
    • extractを使ったメモリー使用量の改善
  • その他の参考
    • Beautiful Soupを使ったアプリケーション
    • 類似のライブラリ
  • まとめ

【和訳】Django Rest Framework 目次

目次 【和訳】Django Rest Framework クイックスタート 【和訳】Django Rest Framework チュートリアル1:シリアル化 【和訳】Django Rest Framework チュートリアル2:リクエストとレスポンス 【和訳】Django Res...