Beautiful Soupドキュメント

Beautiful Soupはpythonで動作するHTMLとXMLのパーサーです。Beautiful Soupはパースしたツリーの操作、検索、変更を簡単に、かつ、今までと同じ方法でできます。これにより、プログラマーの仕事時間を節約します。また、Rubyful SoupというRuby版もあります。

このドキュメントはBeautiful Soupのバージョン3.0における主要な機能をサンプル付きで説明します。このドキュメントを読めばこのライブラリがどんなに良いか、どうやって動いているか、どうやって使うか、やりたいことをどうやって実現するか、予想と異なる動作をした場合になにをすればいいのかが分かります。

クイックスタート

Beautiful Soupをここから手に入れます。ChangeLogにはバージョン3.0とそれ以前のバージョンとの違いが書かれています。

以下のうちのどれか一行を書き、Beautiful Soupを読み込みます。:

from BeautifulSoup import BeautifulSoup          # For processing HTML
from BeautifulSoup import BeautifulStoneSoup     # For processing XML
import BeautifulSoup                             # To get everything

これは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を使って操作します。

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>

あるタグ、あるいは、あるプロパティを持っているタグを探すsoupを使います。

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'

soupを変更するのは簡単です。

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 Commercial Crime Services の週間海賊報告から取得、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コンストラクタはXMLかHTMLのドキュメントを文字列(あるいはファイル形式のオブジェクト)を引数にトリます。このコンストラクタはドキュメントをパースし、メモリ内に対応するデータ構造を作成します。

Beautiful Soupに完璧な形式のドキュメントを渡すと、パースされたデータ構造は元のドキュメントとほとんど同じようになります。しかし、もしドキュメントに間違いがあれば、Beautiful Soupは経験則的な方法を使って構造を理解し、データ構造を作成します。

HTMLをパースする

HTMLドキュメントをパースするにはBeautifulSoupクラスを使います。以下に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ではありませんが、そこまで悪くはありませんでした。こちらはとんでもないドキュメントです。他の問題もありますが、<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クラスは経験則によって、Webブラウザと同じぐらいのHTMLを書いた人の意図を読み取る機能を備えています。しかし、XMLはタグの種類が固定されていないため、経験則は適用できません。つまり、BeautifulSoupはXMLに対してはそれほど良くありません。

XMLドキュメントをパースするには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の欠点は空要素タグ(例: <EXAMPLE />)について理解出来ないことです。HTMLは空要素ができるタグは固定されていますが、XMLはDTDになんと書かれているかに依存します。 自分で閉じるタグをselfClosingTagsとしてBeautifulStoneSoupのコンストラクタに渡すと、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はユニコード対応だぜこんちくしょう

ドキュメントがパースされたら、ユニコードに変換されます。Beautiful Soupのデータ構造の中にはユニコードだけが保存されます。

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'

# Note: this bit uses EUC-JP, so it only works if you have cjkcodecs
# installed, or are running Python 2.4.
soup.__str__('euc-jp')
# '\xa4\xb3\xa4\xec\xa4\xcf'

Beautiful SoupはUnicodeDammitと呼ばれるクラスを使って与えられたドキュメントのエンコーディングを調べ、元のエンコーディングがなにであろうともUnicodeに変換します。もし、(Beautiful Soupがパースしない)他のドキュメントが必要な場合、UnicodeDammitを自分で使うこともできます。このクラスの大部分は、Universal Feed Parserに基づいています。

もしPython 2.4より古いバージョンを使っている場合、cjkcodecsとiconvcodecをダウンロードしてインストールする必要があります。これにより、Pythonがより多くのエンコーディング、特にCJKを、扱えるようになります。また、自動検知が成功する確率を上げるためにchardetもしインストールするといいでしょう。

Beautiful Soupは以下のエンコーディングをこの順番で試してUnicodeに変換します。

  • fromEncoding引数としてsoupコンストラクタに渡したエンコーディング

  • ドキュメント自身にあるエンコーディング。例えば、HTMLドキュメントの場合、XML宣言あるいはhttp-equivMETAタグです。もし、Beautiful Soupがこの種類のエンコーディングをドキュメントの中に見つけた場合、そのエンコーディングを使ってもう一度最初からドキュメントをパースしなおします。例外は、エンコーディングを明示的に指定し、そのエンコーディングが正しい場合です。この場合、ドキュメント中にあるエンコーディング指定は無視されます。

  • ファイルの最初の数バイトを調べて分かったエンコーディング。もしこのステージでエンコーディングが分かった場合、UTF-*か、EBCDICかASCIIのどれかです。

  • chardetライブラリがインストールされていれば、chardetが調べたエンコーディング

  • UTF-8
  • Windows-1252

Beautiful Soupは推測できる場合はほとんどの場合正しく推測してくれます。しかし、宣言がなく変なエンコーディングな文章の場合、推測がほぼできません。この場合Windows-1252として扱いますが、だいたいにおいてこのエンコーディングは間違っています。ここにBeautiful Soupがエンコーディングを間違って推測してしまうEUC-JPの例を出します。(もう一度言うと、これはEUC-JPを使っているからで、この例はPython2.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'     # Wrong!

しかし、もしfromEncodingでエンコーディングを指定した場合、ドキュメントは正しくパースできるため、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'                 # Right!

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

Windows-1252エンコーディングあるいはそれに似ているISO-8859-1やISO-8859-2を使っているドキュメントをBeautiful Soupに渡した場合、Beautiful Soupはドキュメントのsmart quotes(訳註:開きと閉じとで向きが異なるクオーテーション)や他のWindows独自の文字を探して消します。Beautiful Soupはこれらの文字を同じUnicode文字に変換するのではなく、HTMLエンティティ(BeautifulSoupの場合) か XMLエンティティ (BeautifulStoneSoupの場合)に変換します。

これを避けるためには、smartQuotesTo=Noneをsoupコンストラクタに渡します。この場合、 Smart quote は他の文字と同じくUnicodeに変換されます。 “xml” か “html” をsmartQuotesToに渡すとBeautifulSoupBeautifulStoneSoupのデフォルトの挙動を変えられます。

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!'

ドキュメントを表示する

Beautiful Soup(やそのサブセット)のドキュメントはstr関数を使うとstring文字列にできます。あるいは、renderContentsメソッドを使うともうちょっとかっこよく表示してくれます。また、unicode関数を使うとドキュメント全体をUnicode文字列にしてくれます。

prettifyメソッドは重要な改行と空白を入れてドキュメントを見やすくしてくれます。これはまた空白しかないテキストノードを削除してくれますが、これはXMLドキュメントを変更してしまうかもしれません。strunicode関数は空白しかないテキストノードを削除しませんし、ノードの間に空白を挿入もしません。

例です。

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__prettifyrenderContentsを呼ぶ時にエンコーディングも選択できます。デフォルトのエンコーディングは(strで使われると同じく)UTF-8です。以下はISO-8859-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はString文字列に変換する時に新しいエンコーディングで宣言を書き直します。これはつまり、もしHTMLドキュメントをBeautifulSoupに読み込ませ表示した場合、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がパースして作成するデータ構造のことです。

パーサーオブジェクト (BeautifulSoupBeautifulStoneSoupインスタンス)は深い入れ子構造で強く結びついたデータ構造がXMLやHTMLのドキュメントと対応しています。パーサーオブジェクトは2種類のオブジェクトを持っています。 <TITLE>タグや<B>タグに対応するTagオブジェクトと、”ページタイトル”や”このパラグラフ”などの文字列に対応するNavigableStringオブジェクトです。

また、XML構造に対応するNavigableStringのサブクラス(CDataCommentDeclarationProcessingInstruction)があります。これらはNavigableStringと同じ振る舞いをしますが、表示する時にはデータを追加します。コメントを含んだドキュメントの例です。

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>

タグの属性(アトリビュート)

TagNavigableStringオブジェクトは多くの有用なメンバーを持っています。その大部分はパースツリーをナビゲートするパースツリーを検索するで解説されています。しかし、Tagオブジェクトについてここで解説したいことが一つだけあります。それは属性(アトリビュート)です。

SGMLタグは属性を持っています。例えば、上で出したHTMLの例で、それぞれの<P>タグは “id”属性と”align”属性を持っています。このようなタグの属性にTagオブジェクトを使って辞書形式でアクセスできます。

firstPTag, secondPTag = soup.findAll('p')

firstPTag['id']
# u'firstPara'

secondPTag['id']
# u'secondPara'

NavigableStringオブジェクトは属性を持っていません。Tagだけが属性を持っています。

パースツリーを検索する

Beautiful Soupはパースツリーを渡り、指定とマッチするTagNavigableStringを集めるたくさんのメソッドを提供しています。

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)はTagオブジェクトとトップレベルパーサーにだけ作用し、NavigableStringには使えません。(訳註: そうなの?)パースツリーの中を検索するで定義するメソッドはNavigableStringオブジェクトにも使えます。

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

findAllメソッドは与えられた場所からツリーを渡り始めます。そして、全ての適合するTagNavigableStringを見つけます。findAllメソッドのシグニチャはこの通りです。

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

これらの引数はBeautiful SoupのAPIの中で何度も何度も出てきます。この中で最も重要なのはnameとキーワード引数です。

  • name引数はタグをnameで制限します。nameを制限する方法はいくつかあり、それらはBeautiful SoupのAPIで何度も出てきます。

    1. 一番簡単な使用法は、タグの名前を渡すことです。このコードはドキュメント中の全ての<B>タグを見つけます。

      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']
      

      これは一見便利なようには見えませんが、属性値で絞る時に役に立ちます。

  1. Tagオブジェクトだけを引数に取り、真偽値だけを返す、呼び出し可能なオブジェクトを渡すこともできます。findAllが見つけた全てのTagオブジェクトがこの呼出可能オブジェクトに渡され、もし、その呼出が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>]

このコードは一文字の名前、かつ、属性を持たないタグを探してきます。

soup.findAll(lambda tag: len(tag.name) == 1 and not tag.attrs)
# [<b>one</b>, <b>two</b>]
  • キーワード引数はタグの属性を制限します。この簡単な例では、”aligin”属性が”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に呼出可能オブジェクトを渡し、Tagオブジェクトを処理してください。

    ここで問題があることに気がつくかもしれません。もし、nameという属性を定義したタグがあった場合にどうなるでしょう。nameという名前を持つキーワード引数を使うことは出来ません。なぜなら Beautiful Soupの検索用メソッドがすでにname引数を定義しているからです。他にも、Pythonの予約後はキーワード引数として使えません。

    Beautiful Soupはこういう場合に使えるattrという特別な引数を用意しています。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で使っているメソッドを名前として持つ属性を検索の条件としたい時にattrを使えます。

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クラスで検索する

attr引数はちょっと分かりにくい機能ですが、あるCSSのクラスを持つタグを検索するのに使います。 CSSの属性はclassというPythonの予約語と同じ名前を使っているため、そのままでは検索できないからです。

CSSクラスを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>
  • textTagの代わりにNavigableStringオブジェクトを検索するためのヒキスです。この値には文字列、正規表現、辞書型のリスト、TrueNoneNavigableStringオブジェクトを引数に取る呼び出し可能オブジェクトが使えます。

    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'.']
    
    If you use ``text``, then any values you give for ``name`` and the keyword arguments are ignored.
    
  • recursiveは真偽値(デフォルトはTrue)の引数です。これは Beautiful Soupがパースツリーを全てたどるか、Tagあるいはパーサーオブジェクトの直下の子供だけを見るかを指定します。この差を以下の例で示します。

    [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']
    

    recursiveFalseの時、<HTML>タグ直下の子供しか検索しません。直下だけを探せばいい時にこれを使うと、時間を短縮できます。

  • limit引数を設定すると、検索の数を制限できます。もしドキュメントに1000個のテーブルがあり、その4番目のテーブルだけ必要な場合、limitに4を入れれば時間を短縮できます。デフォルトでは制限はありません。

    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を呼び出すのと同じようにタグを呼び出す

ちょっとしたショートカットがありますよ。パーサーオブジェクトかTagを関数のように呼び出し、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)

OK。他の検索メソッドを見ていきましょう。これらはほとんどfindAllと同じ引数を取ります。

findメソッドはfindAllとほぼ同じですが、適合する全てのオブジェクトを返すのではなく、findは見つけた最初のオブジェクトだけを返します。これはlimitに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というメソッドがありました。これらのメソッドはまだありますが、deprecated(廃止予定)で、近いうちになくなります。これらの名前のメソッドが持つ効果はとても混乱します。新しい名前は名前に一貫性があります。上記で述べたように、メソッド名が複数形あるいはAllがついている場合、複数のオブジェクトを返します。そうでなければ単一のオブジェクトを返します。

パースツリーの中を検索する

findAllfindはパースツリーの指定した場所から始まり、辿って行きます。一番最後にたどり着くまでオブジェクトのcontentsを再帰的にイテレートします。

これはNavigableStringオブジェクトではこれらのメソッドを呼べないということを意味しています。なぜならNavigableStringオブジェクトはcontentsを持たない、パースツリーの葉の部分だからです。

しかし、下方向だけがドキュメントをイテレートする方向ではありません。パースツリーをナビゲートするで述べたように、他にもいろいろな方法があります。parentnextSiblingなどです。これらのイテレートは2つの対応するメソッドを持っています。一つはfindAllと似ており、もう一つはfindと似ています。NavigableStringオブジェクトはこれらの操作をサポートしているので、これらのメソッドをTagオブジェクトやパーサーオブジェクトと同じように呼び出すことが出来ます。

なんでこれが便利なんでしょうか。 欲しいTagNavigableStrinfindAllあるいは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>タグを手に入れる安定した方法ではないことは明白です。もし一回だけスクレイピングをするのであれば問題にはなりませんが、もし長期間に渡って何度もスクレイピングするのであればこのような配慮はとても重要になるでしょう。もし別の<LI>タグによって間違ったリストを作った場合、欲しいものとは別のタグを手に入れる事になり、間違ったデータであなたのスクリプトが壊れてしまうでしょう。

soup('ul', limit=2)[1].li
# <li>The data you want</li>

これはちょっとましになりました。なぜなら、間違ったリストになっても生き残れるからです。でも、ドキュメントが最初から間違った別のリストの場合、欲しい<LI>タグではない、最初の<LI>タグを手に入れることになります。もっと信頼性がある方法は、ドキュメント構造の中のulタグの場所を反映させる方法です。

このHTMLを見た時、欲しいリストは「<H1>タグの下にある<UL>タグ」であると思うでしょう。問題は、<UL>タグは<h1>タグに含まれているわけではないということです。単に続いているだけです。<H1>タグを得るのは簡単ですが、<UL>タグをfirstfetchを使って得る方法はありません。なぜなら、これらのメソッドは<H1>タグのcontentsだけを探すからです。<UL>タグを見つけるにはnextnextSiblingメンバ変数を使います。

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>

しかし、乗り越えなければならない問題がまだあります。この章で述べたメソッドは便利な省略法があります。これらはナビゲートする値の一つをループさせるのに使えます(訳註: 自信なし)。ツリーの中での開始地点を与えると、指定した条件に適合するTagNavigableStringオブジェクトを記録しつつツリーをナビゲートしていきます。上のコードの最初のループの代わりにこう書くことが出来ます。

soup.h1.findNextSibling('ul').li
# <li>The data you want</li>

二番目のループの代わりにこう書いても大丈夫です。

soup.find(text='Heading').findNext('ul').li
# <li>The data you want</li>

これらのループはfindNextSiblingfindNextで置き換えられます。この章の残りでこのようなメソッドの全部を解説します。もう一度言いますが、全てのナビゲート変数は2種類のメソッドがあります。一つはfindAllと同じようにリストを返し、もう一つはfindと同じように単一のオブジェクトを返します。

最後に、見慣れたsoupのドキュメントを例として使ってみます。

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メンバー変数を辿り、指定したTagあるいは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メンバ変数を辿り、指定したTagあるいは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)

これらのメソッドはあるオブジェクトのメンバ変数を辿り、指定したTagあるいは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メンバ変数を辿り、指定したTagあるいはNavigableTextを集めてきます。上記ドキュメントに関連します。

lastPTag = soup('p')[-1]
lastPTag.findAllPrevious(text=True)
# [u'.', u'one', u'This is paragraph ', u'Page title']
# Note the reverse order!

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) と findParent(name, attrs, **kwargs)

これらのメソッドはあるオブジェクトのparentメンバ変数を辿り、指定したTagあるいはNavigableTextを集めてきます。これらはtext引数を取りません。なぜなら、どんなオブジェクトもNavigableStringを親として持たないからです上記ドキュメントに関連します。

bTag = soup.find('b')

[tag.name for tag in bTag.findParents()]
# [u'p', u'body', u'html', '[document]']
# NOTE: "u'[document]'" means that that the parser object itself matched.

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

パースツリーを変更する。

これでパースツリーの中からなにかを見つける方法が分かりました。でも、たぶん見つけたものを変更したり表示したりしたいでしょう。その場合、単にそのエレメントをその親のcontentsから取り除けば良いです(訳註: ちょっと不明)。その場合でもドキュメントの残りは今取り除いたエレメントへの参照を持っています。Beautiful Soupは内部の一貫性を保ちつつ、パースツリーを変更するメソッドを提供します。

属性の値を変える

辞書形式になっているTagオブジェクトの属性値を直接変えると、エレメントの属性の値が変わったことになります。

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メソッドは一つのパースツリーを二つの別々のツリーに分解します。ナビゲーションのメンバ変数は変化するため、この二つのツリーはもう二度と一つにならないように見えます。

soup.a1.nextSibling
# <a2></a2>
soup.a2.previousSibling
# <a1></a1>
subtree.previousSibling == None
# True
subtree.parent == None
# True

あるエレメントを別なエレメントに置き換える

replaceWithメソッドは一つのページエレメントを取り除き、別のエレメントで置き換えます。新しいエレメントはTag(そのエレメントの直下のパースツリー全体かもしれません) でも、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>

まったく新しいエレメントを追加する

Tagクラスとパーサークラスはinsertというメソッドを持っています。これはPythonのリストのinsertメソッドと同じ働きをします。タグが含んでいるメンバーのインデックスを引数に取り、新しいエレメントをその場所に埋め込みます。

ドキュメント中のタグを新しいタグで置き換えるデモは、以前の章で行いました。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>

一つのエレメントは、一つのパースツリーにおいて一つの場所しか存在できません。もし、すでにsoupオブジェクトで使われているエレメントをinsertしようとすると、どこかに入れる前に (extractを使って)soupオブジェクトから取り外されます。この例では、NavigableStringをsoupの二つ目の場所に入れようとしていますが、もう一度入れることはできません。移動します。

tag2.insert(0, text)
print soup
# <mytag><myOtherTag>Hello!</myOtherTag><myThirdTag></myThirdTag></mytag>

これはそのエレメントが完全に別なsoupオブジェクトに属しても起こります。エレメントは単一の親、 単一のnextSibleingなどしか持ちません。そのため、エレメントは同時に一つの場所でしか存在しないのです。

トラブルシューティング

この章ではBeautiful Soupを使うときによくある問題について解説します。

なんでASCIIじゃない文字をちゃんと表示してくれないの?

もしそのエラーが“‘ascii’ codec can’t encode character ‘x’ in position y: ordinal not in range(128)”だったら、その問題はBeautiful SoupではなくPythonの問題です。非ASCIIの文字をBeautiful Soupを使わずに表示させて、同じ問題が起きるかどうかを確認してください。例えば、ここに実際に動くコードを記します。

latin1word = 'Sacr\xe9 bleu!'
unicodeword = unicode(latin1word, 'latin-1')
print unicodeword

もしこれが動いてBeautiful Soupでは動かなければ、それはたぶんBeautiful Soupのバグです。しかし、もしこれが動かなければあなたのPython環境の問題です。Pythonは安全に非ASCII文字をターミナルに御斬ります。この動作を上書きするためには二つの方法があります。

  1. 簡単な方法は、標準出力をISO-Latin-1かUTF-8の文字に対するコンバーターにマップしなおすことです。

import codecs
import sys
streamWriter = codecs.lookup('utf-8')[-1]
sys.stdout = streamWriter(sys.stdout)

codecs.lookupは使えるメソッドの数とコーデックに関連する他のオブジェクトを返します。最後のはアウトプットストリームをラップできるStreamWriteオブジェクトです。

  1. 難しい方法は、デフォルトのエンコーディングをISO-Latin-1かUTF-8に設定するsitecustomeize.pyファイルを作ってPythonの環境にインストールする方法です。これにより、個々のプログラムになにもすることなく、全てのPythonプログラムが標準出力にこのエンコーディングを使うようになります。私の環境では、/usr/lib/python/sitecustomize.pyがこんな感じになってます。

import sys
sys.setdefaultencoding("utf-8")

Pythonのユニコードサポートについてもっと知りたい場合は、Unicode for ProgrammersEnd to End Unicode Web Applications in Pythonを見てください。Pythonクックブックの1.20と1.21のレシピも参考になります。

ターミナルの表示がASCIIに制限されていたとしても、パース、処理、UTF-8や他のエンコーディングで書きだすことにBeautiful Soupが使えることは覚えておいてください。ただ単に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>
    
  • もし、あなたのドキュメントが宣言から始まり、終わらなかったら、Beautiful 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>
    

    これを直す方法はいくつかあります。ひとつはここに書かれています。

    Beautiful Soupはドキュメントの終りにいっても終わっていないエンティティの参照も無視します。 (訳註: あとで確認)

    BeautifulSoup("&lt;foo&gt")
    # &lt;foo
    

    私は実際のwebページでは見たことありません。しかし、どこかにはあるんでしょう。

  • 不正な形式のコメントがあると、Beautiful Soupは残りのドキュメントを全部無視してしまいます。これは正規表現でサニタイズするで述べています。

BeautifulSoupが作ったパースツリーはなんかおれに合わねーよ!

違うようにパースしたい場合は、他の組み込みパーサーか、パーサーをカスタマイズするを参考にしてください。

Beautiful Soupはすげー遅いよ!

Beautiful SoupはElementTreeや自分で作ったSGMLParserのサブクラスより早くなることはありません。ElementTreeはCで書かれていますし、SGMParserは自分の好きなように小さいBeautiful Soupを書けます。Beautiful Soupの特徴は、プログラマの時間を減らすことで、プロセッサの時間を減らすことではありませんから。

これはつまり、Beautiful Soupをドキュメントの一部だけをパースすることで、性能を向上させることができるということです。また、使っていないオブジェクトをextractかdecomposeを使ってGCに回収させることもできます。

もっと詳しく

これまで述べてきたことで、Beautiful Soupの基本的な使い方は終わりました。しかし、HTMLとXMLはトリッキーで、実際の世界はもっとトリッキーです。なので、実はBeautiful Soupはもうちょっと仕掛けを隠し持っています。

ジェネレーター

上で述べたSearchメソッドはジェネレーターメソッドで動いています。このメソッドを自分で使うことができます。具体的には、nextGeneratorpreviousGeneratornextSiblingGeneratorpreviousSiblingGeneratorparentGeneratorです。TagやパーサーオブジェクトはchildGeneratorrecursiveChildGeneratorをさらに持っています。

ドキュメントをイテレートして文字列を全て集めることで、HTMLタグを取り除く簡単な例を示します。

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("""<div>You <i>bet</i>
<a href="http://www.crummy.com/software/BeautifulSoup/">BeautifulSoup</a>
rocks!</div>""")

''.join([e for e in soup.recursiveChildGenerator()
         if isinstance(e,unicode)])
# u'You bet\nBeautifulSoup\nrocks!'

もちろん、タグの下の文字列を見つけるためにジェネレーターは必要ありません。次のコードは.findAll(text=True)と同じ事をします。

''.join(soup.findAll(text=True))
# u'You bet\nBeautifulSoup\nrocks!'

こっちはrecursiveChildGeneratorを使うもっと複雑な例です。ドキュメントのエレメントをイテレートし、出力します。

from BeautifulSoup import BeautifulSoup
soup = BeautifulSoup("1<a>2<b>3")
g = soup.recursiveChildGenerator()
while True:
    try:
        print g.next()
    except StopIteration:
        break
# 1
# <a>2<b>3</b></a>
# 2
# <b>3</b>
# 3

他の組み込みパーサー

Beautiful SoupにはBeautifulSoupBeautifulStoneSoup以外に3つのパーサークラスがあります。

  • MinimalSoupBeautifulSoupのサブクラスで、HTMLのような空要素タグ、<SCRIPT>タグの特殊な挙動、<META>タグがとりうるエンコーディングの種類などを理解します。しかし、これは経験則的アルゴリズムをまったく持ちません。そのため、<LI>タグが<UL>タグの下にくることなどを知りません。これは病的なマークアップをパースする時に使えます。

  • ICantBelieveItsBeautifulSoupクラスもBeautifulSoupクラスのサブクラスです。これはHTML標準に近い経験則を持ちますが、実世界でどのように使われているかは無視します。例えば、<B>タグを入れ子にすることは正しいHTMLなのですが、実世界では入れ子になっている<B>のほとんどは著者が最初の<B>タグを閉じ忘れたものなのです。もし、本当に<B>タグの入れ子をしたいる場合には、ICantBelieveItsBeautifulSoupを使ってください。

  • BeautifulSOAPBeautifulStoneSoupのサブクラスです。これは、単に親エレメントの属性を使うためだけにサブエレメントを使う、SOAPメッセージをパースするのに便利です。

    from BeautifulSoup import BeautifulStoneSoup, BeautifulSOAP
    xml = "<doc><tag>subelement</tag></doc>"
    print BeautifulStoneSoup(xml)
    # <doc><tag>subelement</tag></doc>
    print BeautifulSOAP(xml)
    <doc tag="subelement"><tag>subelement</tag></doc>
    

    BeautifulSOAPではタグの中身を見なくても<TAG>タグのコンテンツにアクセスできます。

パーサーをカスタマイズする

組み込みのパーサークラスがうまく動かないときは、カスタマイズが必要です。このカスタマイズとはだいたいの場合、入れ子可能なタグと空要素可能なタグのリストをいじることです。空要素タグはselfClosingTagsをsoupコンストラクタに渡すことでカスタマイズできます。入れ子可能なタグのリストをカスタマイズするには、サブクラスを作成する必要があります。

最も便利なクラスはMinimalSoup(HTML向け) とBeautifulStoneSoup(XML向け)です。これからサブクラスの中のRESET_NESTING_TAGSNESTABLE_TAGSをオーバーライドする方法を説明します。これはBeautiful Soupで最も分かりにくい部分で、上手く説明できません。でも、なにかしら書くことで、フィードバックを受けてもっと良くすることができます。

Beautiful Soupがドキュメントをパースする時、開始タグをスタックに詰みます。新しいタグを発見するたびに、スタックの一番上に積んでいきます。しかしその前に、他の開始タグを閉じ、スタックから取り除くことがあります。今まさに見つけたタグの質と、スタックの中のタグの質に応じてどのタグを閉じるか決定します。

一番良い方法は例を使って説明することです。例えば、スタックが [‘html’, ‘p’, ‘b’] となってるとしましょう。ここでBeautiful Soupが<P>タグを見つけました。もし単にもう一つの ‘p’ をスタックに積むだけであれば、二つ目の<P>タグは最初の<P>の中にあることとみなして、開いている<B>タグについてはなにも考えません。しかし、これでは<P>タグがちゃんと動きません。<P>タグは別の<P>タグの中には入れられません。<P>タグは”入れ子”にならないのです。

そのため、Beautiful Soupは<P>タグを見つけたら、今まで見つけた同じ種類のタグを含む全てのタグを閉じます。これは標準の振る舞いであり、BeautifulStoneSoupはどのようなのタグについても同様のことを行います。もしタグがNESTABLE_TAGSRESET_NESTING_TAGSに含まれていない場合、こうなります。もしあるタグがRESET_NESTING_TAGSに含まれており、NESTABLE_TAGSに含まれていない場合、<P>タグと同じようになります。

from BeautifulSoup import BeautifulSoup
BeautifulSoup.RESET_NESTING_TAGS['p'] == None
# True
BeautifulSoup.NESTABLE_TAGS.has_key('p')
# False

print BeautifulSoup("<html><p>Para<b>one<p>Para two")
# <html><p>Para<b>one</b></p><p>Para two</p></html>
#                      ^---^--The second <p> tag made those two tags get closed

スタックが [‘html’, ‘span’, ‘b’] であり、 Beautiful Soupが<SPAN>タグを見つけたとしましょう。<SPAN>タグは別の<SPAN>タグを無限に含むことができます。そのため、新しく見つけたからといって以前の<SPAN>タグを取り出す必要はありません。これはNESTABLE_TAGSでタグ名が空のリストにマップされていることで表現されています。この種類のタグはRESET_NESTING_TAGSに含まれてはいけません。 <SPAN>タグは他のどんなタグもスタックから取り出すことはしません。

from BeautifulSoup import BeautifulSoup
BeautifulSoup.NESTABLE_TAGS['span']
# []
BeautifulSoup.RESET_NESTING_TAGS.has_key('span')
# False

print BeautifulSoup("<html><span>Span<b>one<span>Span two")
# <html><span>Span<b>one<span>Span two</span></b></span></html>

三番目の例では、[‘ol’, ‘li’, ‘ul’]がスタックにのっています。これは、 orderd list(順序付きリスト) であり、最初のエレメントには unordered list(順序なしリスト)を含んでいます(訳註: 不明確)。ここでBeautiful Soupが<LI>タグを見つけたとしましょう。この場合、新しい<LI>タグは順序なしリストの一部ですから、最初の<LI>タグはスタックから取り出してはいけません。<UL>タグか<OL>タグがある間は、<LI>タグが別の<LI>タグの中に含まれていても構いません。(訳註:不明確)

from BeautifulSoup import BeautifulSoup
print BeautifulSoup("<ol><li>1<ul><li>A").prettify()
# <ol>
#  <li>
#   1
#   <ul>
#    <li>
#     A
#    </li>
#   </ul>
#  </li>
# </ol>

しかし、<UL>か<OL>が間になかった場合、一つの<LI>タグは他の<LI>タグの下にはいれられません。

print BeautifulSoup("<ol><li>1<li>A").prettify()
# <ol>
#  <li>
#   1
#  </li>
#  <li>
#   A
#  </li>
# </ol>

Beautiful Soupは<LI>タグをRESET_NESTING_TAGSに”li”を入れることで表現し、 “li” を入れ子可能であるNESTABLE_TAGSに含ませることで表現します。

BeautifulSoup.RESET_NESTING_TAGS.has_key('li')
# True
BeautifulSoup.NESTABLE_TAGS['li']
# ['ul', 'ol']

入れ子可能なタグをどうやって扱っているかを説明します。

BeautifulSoup.NESTABLE_TAGS['td']
# ['tr']
BeautifulSoup.NESTABLE_TAGS['tr']
# ['table', 'tbody', 'tfoot', 'thead']
BeautifulSoup.NESTABLE_TAGS['tbody']
# ['table']
BeautifulSoup.NESTABLE_TAGS['thead']
# ['table']
BeautifulSoup.NESTABLE_TAGS['tfoot']
# ['table']
BeautifulSoup.NESTABLE_TAGS['table']
# []

<TD>タグは<TR>タグの中に含められます。<TR>タグは<TABLE>, <TBODY>, <TFOOT>, <THEAD> タグの中に含められます。 <TBODY>, <TFOOT>, <THEAD>タグは<TABLE>の中に含められます。<TABLE>タグは別の<TABLE>タグに含められます。もし、HTMLテーブルを知っていれば、これらのルールが分かると思います。

もう一つ例を上げます。スタックが [‘html’, ‘p’, ‘table’] でBeautiful Soupが<P>タグにぶつかった時です。

最初に見た時、これは[‘html’, ‘p’, ‘b’]がスタックにあって、<P>タグを見つけた時の例と同じように見えます。あの例では、<B>タグと<P>タグを閉じました。なぜなら、一つのパラグラフは他のパラグラフを含められないからです。

でも、テーブルの中にはパラグラフを含められるのです。そのため、正しい実装ではこれらのタグを閉じてはいけないのです。Beautiful Soupは正しい動作をします。

from BeautifulSoup import BeautifulSoup
print BeautifulSoup("<p>Para 1<b><p>Para 2")
# <p>
#  Para 1
#  <b>
#  </b>
# </p>
# <p>
#  Para 2
# </p>

print BeautifulSoup("<p>Para 1<table><p>Para 2").prettify()
# <p>
#  Para 1
#  <table>
#   <p>
#    Para 2
#   </p>
#  </table>
# </p>

なにが違うのだって?違うところは <TABLE>はRESET_NESTING_TAGSに入っており、<B>は入っていないことです。RESET_NESTING_TAGSに入っているタグは、入っていないタグよりスタックから取り出されにくいのです。

オーケー。たぶんいいアイデアが出たでしょう。 BeautifulSoupクラスにはNESTABLE_TAGSがあります。これはあなたがHTMLについて知っていることと関連があります。つまり、普通のHTMLじゃない変なHTMLドキュメントや違う入れ子のルールを持つ他のXMLの方言のためには、自分でNESTABLE_TAGSを作る必要があります

from BeautifulSoup import BeautifulSoup
nestKeys = BeautifulSoup.NESTABLE_TAGS.keys()
nestKeys.sort()
for key in nestKeys:
    print "%s: %s" % (key, BeautifulSoup.NESTABLE_TAGS[key])
# bdo: []
# blockquote: []
# center: []
# dd: ['dl']
# del: []
# div: []
# dl: []
# dt: ['dl']
# fieldset: []
# font: []
# ins: []
# li: ['ul', 'ol']
# object: []
# ol: []
# q: []
# span: []
# sub: []
# sup: []
# table: []
# tbody: ['table']
# td: ['tr']
# tfoot: ['table']
# th: ['tr']
# thead: ['table']
# tr: ['table', 'tbody', 'tfoot', 'thead']
# ul: []

そして、これがBeautifulSoupのRESET_NESTING_TAGSです。keyだけが重要です。RESET_NESTING_TAGSは実際リストであり、高速なランダムアクセスのために辞書形式になっています。

from BeautifulSoup import BeautifulSoup
resetKeys = BeautifulSoup.RESET_NESTING_TAGS.keys()
resetKeys.sort()
resetKeys
# ['address', 'blockquote', 'dd', 'del', 'div', 'dl', 'dt', 'fieldset',
#  'form', 'ins', 'li', 'noscript', 'ol', 'p', 'pre', 'table', 'tbody',
#  'td', 'tfoot', 'th', 'thead', 'tr', 'ul']

サブクラスを作っているのなら、SELF_CLOSING_TAGSをオーバーライドするかもしれません。これは空要素タグの名前と他の全部の値(RESET_NESTING_TAGSのように、実際にはリストの辞書形式)です。それから、インスタンスを作成するたびに、そのSELF_CLOSING_TAGSをコンストラクタにselfClosingTagsとして渡す必要はありません。

エンティティの変換

ドキュメントをパースするとき、HTMLかXMLの実体参照を対応するユニコード文字に変換できます。このコードはHTMLの”&eacute;”をユニコードのLATIN SMALL LETTER E WITH ACUTEに、”&#101;”をLATIN SMALL LETTER Eに変換します。

from BeautifulSoup import BeautifulStoneSoup
BeautifulStoneSoup("Sacr&eacute; bl&#101;u!",
                   convertEntities=BeautifulStoneSoup.HTML_ENTITIES).contents[0]
# u'Sacr\xe9 bleu!'

これは、HTML_ENTITIES(中身は単なる”html”という文字列)を使った場合です。もし、XML_ENTITIES(あるいは”xml”という文字列)を使った場合、数字のエンティティと5つのXMLエンティティ(“&quot;”, “&apos;”, “&gt;”, “&lt;”, “&amp;”)だけが変換されます。もし、ALL_ENTITIES(あるいは [“xml”, “html”] というリスト)を使った場合、二種類のエンティティが変換されます。最後の手法は “&apos;” はXMLエンティティですが、HTMLエンティティではないため、必要になります。

BeautifulStoneSoup("Sacr&eacute; bl&#101;u!",
                   convertEntities=BeautifulStoneSoup.XML_ENTITIES)
# Sacr&eacute; bleu!

from BeautifulSoup import BeautifulStoneSoup
BeautifulStoneSoup("Il a dit, &lt;&lt;Sacr&eacute; bl&#101;u!&gt;&gt;",
                   convertEntities=BeautifulStoneSoup.XML_ENTITIES)
# Il a dit, <<Sacr&eacute; bleu!>>

Beautiful SoupにXMLかHTMLエンティティを対応するユニコード文字に変換するよう指示したら、 Windows-1252は(Microsoft smart qutesのように)も同じようにユニコード文字に変換されます。これはBeautiful Soupがこれらの文字をエンティティに変換する時にも起こります。

from BeautifulSoup import BeautifulStoneSoup
smartQuotesAndEntities = "Il a dit, \x8BSacr&eacute; bl&#101;u!\x9b"

BeautifulStoneSoup(smartQuotesAndEntities, smartQuotesTo="html").contents[0]
# u'Il a dit, &lsaquo;Sacr&eacute; bl&#101;u!&rsaquo;'

BeautifulStoneSoup(smartQuotesAndEntities, convertEntities="html",
                   smartQuotesTo="html").contents[0]
# u'Il a dit, \u2039Sacr\xe9 bleu!\u203a'

BeautifulStoneSoup(smartQuotesAndEntities, convertEntities="xml",
                   smartQuotesTo="xml").contents[0]
# u'Il a dit, \u2039Sacr&eacute; bleu!\u203a'

新しいHTML/XMLエンティティを作る時、今ある全てのエンティティをユニコード文字に変換するのは意味が分からない。(訳註:意味が取れなかった)

正規表現を使ってだめなデータをサニタイズする

Beautiful Soupは悪いマークアップをいい感じに扱ってくれます。ここで「悪いマークアップ」とはタグが間違った場所にあることです。しかし、時にはマークアップは単に変な形をしているだけで、基本的なパーサーは扱えなくなります。そのため、Beautiful Soupはパースする前に正規表現を走らせることができます。

標準ではBeautiful Soupは正規表現を入力されたドキュメントの置換機能として使います。<BR/>のような空要素タグを見つけ、<BR />に変換します。<! –コメント–>のように適切でない空白を持つ装飾を見つけ、<!–コメント–>のように空白を除去します。

もし、なにかしらの方法で直す必要がある悪いマークアップがある場合、 (regular exporession,replacement)の関数のタプルを持つリストをsoupコンストラクタにmarkupMassage引数として渡します。 (訳註:マッサージです。メッセージではありません)

例を見ましょう。変な形のコメントを持つページがあるとします。基本的なSGMLパーサーはこれに勝てず、コメントとそれ以降の文字を全て無視します。

from BeautifulSoup import BeautifulSoup
badString = "Foo<!-This comment is malformed.-->Bar<br/>Baz"
BeautifulSoup(badString)
# Foo

正規表現と関数で整形しましょう。

import re
myMassage = [(re.compile('<!-([^-])'), lambda match: '<!--' + match.group(1))]
BeautifulSoup(badString, markupMassage=myMassage)
# Foo<!--This comment is malformed.-->Bar

おおっと。まだ<BR>タグが残っていました。今定義したmarkupMassageは標準のmassageを上書きするため、標準の置換関数は動きません。パーサーはコメントを通り過ぎ、変な空要素タグで死んでしまいます。新しいmassage関数を標準のリストに付け加えましょう。そうすれば全ての関数が動きます。

import copy
myNewMassage = copy.copy(BeautifulSoup.MARKUP_MASSAGE)
myNewMassage.extend(myMassage)
BeautifulSoup(badString, markupMassage=myNewMassage)
# Foo<!--This comment is malformed.-->Bar<br />Baz

よし、これで全部動いた。

あなたのマークアップが正規表現による置換を必要とするかどうかを知りたい場合、FalsemarkupMassageに渡すことですぐに分かります。

SoupStrainerで楽しく

全ての検索メソッドは同じ引数を取ることを思い出してください。この裏側では、検索メソッドの引数はSoupStrainerオブジェクトに変換されます。もし、(findAllのような)リストを返すメソッドを呼んだ場合、SoupStrainerオブジェクトは結果のリストのsourceプロパティとして現れてきます。

from BeautifulSoup import BeautifulStoneSoup
xml = '<person name="Bob"><parent rel="mother" name="Alice">'
xmlSoup = BeautifulStoneSoup(xml)
results = xmlSoup.findAll(rel='mother')

results.source
# <BeautifulSoup.SoupStrainer instance at 0xb7e0158c>
str(results.source)
# "None|{'rel': 'mother'}"

SoupStrainerコンストラクタは name, attrs, text, **kwargsとfindとほとんど同じ引数を取ります。SoupStrainerを全部の検索メソッドに引数として渡すことができます。

xmlSoup.findAll(results.source) == results
# True

customStrainer = BeautifulSoup.SoupStrainer(rel='mother')
xmlSoup.findAll(customStrainer) == results
#  True

いえーい。知ったこっちゃないね。だろ?このメソッドの呼び出し引数をいろいろな方法で渡すことができます。しかし、別の方法は、SoupStrainerをsoupコンストラクタに渡し、ドキュメントのうち実際のパースする部分を制限することです。これの意味は次の章で述べます。

ドキュメントの一部だけをパースすることで、性能を向上させる

Beautiful Soupはドキュメントの全てのエレメントをPythonオブジェクトにして、他の複数のPythonオブジェクトに接続します。もし、ドキュメントの一部分しか必要なければ、全部をパースすることはとても遅いことになります。そこで、SoupStrainerparseOnlyThese引数としてsoupコンストラクタに渡すことができます。Beautiful Soupは個々のエレメントをSoupStrainerと照らし合わせ、適合したものだけをTagNavigableTextに変換してツリーに付け加えます。

エレメントがツリーに加えられたら、そのエレメントはSoupStrainerとマッチしていなくても子要素となります。これにより、あなたが欲しいデータを持つドキュメントだけがパースされることになります。

ちょっと違ったドキュメントを出しましょう。

doc = '''Bob reports <a href="http://www.bob.com/">success</a>
with his plasma breeding <a
href="http://www.bob.com/plasma">experiments</a>. <i>Don't get any on
us, Bob!</i>

<br><br>Ever hear of annular fusion? The folks at <a
href="http://www.boogabooga.net/">BoogaBooga</a> sure seem obsessed
with it. Secret project, or <b>WEB MADNESS?</b> You decide!'''

このドキュメントをsoupにパースするには、どの部分が欲しいかによって、いくつか方法があります。以下の例は全て、SoupStrainerを使うことでドキュメント全体をパースするより高速でメモリ消費が少ないです。

from BeautifulSoup import BeautifulSoup, SoupStrainer
import re

links = SoupStrainer('a')
[tag for tag in BeautifulSoup(doc, parseOnlyThese=links)]
# [<a href="http://www.bob.com/">success</a>,
#  <a href="http://www.bob.com/plasma">experiments</a>,
#  <a href="http://www.boogabooga.net/">BoogaBooga</a>]

linksToBob = SoupStrainer('a', href=re.compile('bob.com/'))
[tag for tag in BeautifulSoup(doc, parseOnlyThese=linksToBob)]
# [<a href="http://www.bob.com/">success</a>,
#  <a href="http://www.bob.com/plasma">experiments</a>]

mentionsOfBob = SoupStrainer(text=re.compile("Bob"))
[text for text in BeautifulSoup(doc, parseOnlyThese=mentionsOfBob)]
# [u'Bob reports ', u"Don't get any on\nus, Bob!"]

allCaps = SoupStrainer(text=lambda(t):t.upper()==t)
[text for text in BeautifulSoup(doc, parseOnlyThese=allCaps)]
# [u'. ', u'\n', u'WEB MADNESS?']

検索メソッドに渡したSoupStrinerとsoupコンソトラクタに渡したSoupStrinerとでは大きな違いが一つあります。name引数は、Tagオブジェクトを引数に取る関数を、引数に取ることが出来ることを思い出してください。これをSoupStrainernameにすることができます。なぜならSoupStrainerTagオブジェクトが最初の場所で作成されるかどうかを決定するために使われる為です。SoupStrainernameに関数を渡せますが、Tagオブジェクトは取れません。タグの名前か引数のマップだけを与えることが出来ます。

shortWithNoAttrs = SoupStrainer(lambda name, attrs: \
                                len(name) == 1 and not attrs)
[tag for tag in BeautifulSoup(doc, parseOnlyThese=shortWithNoAttrs)]
# [<i>Don't get any on us, Bob!</i>,
#  <b>WEB MADNESS?</b>]

extractを使ってメモリ使用量を削減する

Beautiful Soupがドキュメントをパースする時、ドキュメントを大きな密結合なデータ構造としてメモリにロードします。単にデータ構造から文字を取り出したいだけの場合、文字列を取って残りは全部ガベージコレクタにくれてやってもいいと思うでしょう。この文字列はNavigableStringです。これの親メンバはTagオブジェクトを指し、それがさらに別のTagオブジェクトを指し、…と続きます。だから、ツリーのどの一部分を持ったとしても、ツリー全部をメモリに持つことになるのです。

extractメソッドはこの接続を壊します。extractを必要な文字列に対して呼ぶと、残りのパースツリーから切り離します。ツリーの残りの部分はスコープを外れ、取った文字列を使っている間にガベージコレクタに回収されます。ツリーの小さな部分だけが必要な場合、トップレベルのTagに対してextractを呼ぶと、ツリーの残り全部がガベージコレクタに回収されます。

これは他の方法でも同じように動きます。必要がない巨大なドキュメントがあったとき、extractを呼ぶとツリーを破壊され、(もっと小さい)ツリーを作っている間にガベージコレクタに回収されます。

もしextractがちゃんと動かない場合、Tag.decomposeを使います。これはextractより遅いですが、もっと徹底的です。これはTagとそのコンテンツを再帰的に分解し、ツリーの全ての部分を、他の全ての部分から切り離します。

大きな塊のツリーを自分で破壊したい場合、最初からツリーの一部だけをパースするようにすればもっと時間を節約できるかもしれませんよ。

参考

Beautiful Soupを使っているアプリケーション

たくさんの実際に使われているアプリケーションがBeautiful Soupを使っています。私が知っている公開されているアプリケーションを記します。

  • Scrape ‘N’ FeedはRSSフィードを持たないサイトにフィードを作成するツールです。

  • htmlatexはLaTexの方程式を見つけて画像として描画する部分にBeautiful Soupを使っています。

  • chmtopdfはCHMファイルをPDFに変換します。これと私が議論してるって?(訳註: どういう意味だろう)

  • Duncan Gough のFotopic backupはBeautiful SoupをFotopic Webサイトをスクレイピングするのに使っています。

  • Iñigo Sernaのgooglenews.pyはBeautiful Soupをparse_entryとparse_category関数を使ってGoogle Newsをスクレイピングするのに使っています。。

  • Weather Office Screen Scraperはカナダ政府の気象サイトをスクレイピングするのに使っています。

  • News CluesはRSS feedをパースするのに使っています。

  • BlinkFlashはオンラインサービスの登録を自動化するのに使っています。

  • linkyというリンクチェッカーはBeautiful Soupをページのリンクとチェックが必要な画像を見つけるのに使っています。

  • Matt CroydonはBeautiful Soup 1.xを NokiaのSeries 60 スマートフォンで動かしました。C.R. SandeepはSeries 60で動くリアルタイムの外貨変換ツールを書きました。でも、どうやって実現したかは公開されていません。

  • jacobian.org からはallofmp3.comからダウンロードした音楽ファイルのメタデータを修正する短い文章が出ています。

  • Python Community Serverはspam検知に使っています。

類似ライブラリ

とんでもないマークアップを扱い、ツリーを渡り、標準的なパーサーよりもっと良いパーサーをいろいろな言語で実装しているのを見つけました。

  • 私は、Beautiful SoupをRubyに移植しました。Rubyful Soupです。

  • HpricotはRubyful Soupを相手に善戦しました。

  • ElementTreeは高速なPythonのXMLパーサーですが態度が悪いです。私はこれが好きです。

  • Tag SoupはJavaで書かれたXML/HTMLパーサーで、ダメなHTMLをパースできるHTMLに書きなおしてくれます。

  • HtmlPragはダメなHTMLをパースするSchemeライブラリです。

  • xmltrampは標準のXML/XHTMLパーサーです。他のパーサーと同じようにツリーを利用者が渡っていかなければなりませんが、これは簡単に使えます。

  • pullparserはツリーを渡るメソッドを提供します。

  • Mike FoordはBeautiful Soupがツリーを変えた時にHTMLを変更するのが気に入りませんでした。そのため、HTML Scraperを書きました。これはダメなHTMLを扱えるHTMLParserの一つです。これはBeautiful Soup 3.0のリリースを古びたものにするかもしれません。知らないけど。

  • Ka-Ping Yeeのscrape.pyはURLで開いたページをスクレイピングして、合成します。

最後に

これで終わりです!楽しんでください!私はBeautiful Soupをみんなの時間を節約するために作りました。これに慣れると、数分で貧弱なデザインのwebサイトのデータを議論できるようになりますよ。もしコメント、問題、Beautiful Soupをプロジェクトに使ってるよ!ということなどがあれば私にメールを下さい。

–Leonard


このドキュメントLeonard Richardsonが保有するWeb空間である、Crummyの一部です。連絡先。 (訳註: 日本語訳者は WAKAYAMA Shirouです)

Crummy is © 1996-2011 Leonard Richardson. 特別な言及がない限り、全てのテキストはクリエイティブ・コモンズ ライセンス(CC BY-SA 2.0)に従います。