Beautiful Soup

"The Fish-Footman began by producing from under his arm a great letter, nearly as large as himself."

Beautiful Soup はHTMLやXMLファイルからデータを取得するPythonのライブラリです。あなたの好きなパーサー(構文解析器)の方法で、パースツリー(構文木)の探索、検索、修正を行います。 これはプログラマーの作業時間を大幅に短縮してくれます。

(訳注)石鹸は食べられない

この文書は Beautiful Soup 4.2.0 Documentation の日本語訳です。”Beautiful Soup”を”ビューティフルソープ”と読んでしまう英語が苦手でちょっぴりHな後輩のために翻訳してみました。

2013年10月29日からこの文書の翻訳をはじめました。11月1日現在まだ全部を訳し終えていませんが、とりあえず使いそうなところは訳したので、これで研究室内で公開してしまい、あとは年内を目処にまったりとやっていきます。全部訳し終えたら一般公開し、オリジナル からリンクを張ってもらおうと思います。

誤訳やわかりづらいところを見つけたり、なにかご意見があるときには、近藤しげ(TwitterID:@shigex)までご連絡ください。

2013年10月現在、Beautiful Soupについての日本語Webページは、Beautiful Soup 3とBeautiful Soup 4(以下、BS3,BS4)の情報が混在しています。とくに、”Beautiful Soup”で日本語ページを対象にググると、最初に表示される10件中9件がBS3による情報であるために、初心者はそのままBS3を使って混乱しがちです。ご注意ください。

混乱しないように初心者が知っておくべきこと

  • BS3はPython3に対応していません
  • BS3のスクリプトのほとんどはimport文を変えるだけでBS4でも動きます
  • そのため、BS3による情報も問題解決の役に立ちます
  • 2012年5月にBS3の公式リリースが終了し、現在はBS4の利用が推奨されています
  • 詳しくは BS4への移行 を読んでください
  • この文書の クイックスタートfind_all() を読めば、それなりに用は足りると思います

この文書について

この文書は、Beautiful Soup 4 (訳注:以下BS4)の主な特徴について、例を挙げて説明します。どのライブラリがよいか、どのように動くか、どのように使うか、どのようにあなたの望むことを達成するか、予想外の動きをしたときは何をすればよいかといったことを示します。

この文書で挙げられる例は、Python2.7と3.2のどちらでも同じように動きます。

あなたは Beautiful Soup 3 (訳注:以下BS3)の文書を探しているのかもしれません。もしそうなら、BS3はもう開発を終えていて、BS4が全てのプロジェクトに推奨されていることを知るべきです。もし、あなたがBS3とBS4の違いを知りたいなら、BS4への移行 を見てください。

この文書は、ユーザーにより他の言語にも翻訳されています。

助けてほしいときは

Beautiful Soup について疑問が生じたり、問題に直面したときは、 ディスカッショングループにメールしてください。 もし問題がHTMLのパースのことであれば、そのHTMLについて diagnose() 関数の返す内容 を必ず書くようにしてください。

クイックスタート

以下のHTMLドキュメントは、このあと何回も例として用いられます。 ふしぎの国のアリス の一節です。:

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

この”three sisters”ドキュメントを Beautiful Soup にかけると、Beautiful Soup オブジェクトが得られます。このBeautifule Soupは入れ子データ構造でHTMLドキュメントを表現します。:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

print(soup.prettify())
# <html>
#  <head>
#   <title>
#    The Dormouse's story
#   </title>
#  </head>
#  <body>
#   <p class="title">
#    <b>
#     The Dormouse's story
#    </b>
#   </p>
#   <p class="story">
#    Once upon a time there were three little sisters; and their names were
#    <a class="sister" href="http://example.com/elsie" id="link1">
#     Elsie
#    </a>
#    ,
#    <a class="sister" href="http://example.com/lacie" id="link2">
#     Lacie
#    </a>
#    and
#    <a class="sister" href="http://example.com/tillie" id="link2">
#     Tillie
#    </a>
#    ; and they lived at the bottom of a well.
#   </p>
#   <p class="story">
#    ...
#   </p>
#  </body>
# </html>

以下は、データ構造を探索するいくつかのシンプルな方法です。:

soup.title
# <title>The Dormouse's story</title>

soup.title.name
# u'title'

soup.title.string
# u'The Dormouse's story'

soup.title.parent.name
# u'head'

soup.p
# <p class="title"><b>The Dormouse's story</b></p>

soup.p['class']
# u'title'

soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find(id="link3")
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

よくある処理として、ページの<a>タグ内にあるURLを全て抽出するというものがあります。:

for link in soup.find_all('a'):
    print(link.get('href'))
# http://example.com/elsie
# http://example.com/lacie
# http://example.com/tillie

また、ページからタグを除去して全テキストを抽出するという処理もあります。:

print(soup.get_text())
# The Dormouse's story
#
# The Dormouse's story
#
# Once upon a time there were three little sisters; and their names were
# Elsie,
# Lacie and
# Tillie;
# and they lived at the bottom of a well.
#
# ...

必要な情報は得られましたか?つづきをどうぞ。

インストール

DebianかUbuntuの最近のバージョンを使っていれば、Beautiful Soupはシステムのパッケージマネージャでインストールできます。:

$ apt-get install python-bs4

Beautiful Soup 4 は PyPiを通して公開されています。そのため、もしシステムパッケージで Beautiful Soup をインストールできないときは、easy_installpip でインストールできます。

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

( BeautifulSoup パッケージはおそらくあなたが探しているものではありません。これは、一つ前のメジャーリリース Beautiful Soup 3 です。多くのソフトウェアがBS3を使っていて、今でもBS3は利用できます。しかし、新しくコードを書く場合は、 beautifulsoup4 をインストールすべきです。)

もし、 easy_installpip をインストールしてないときは、download the Beautiful Soup 4 source tarball でソースをダウンロードし setup.py を用いてインストールできます。

$ python setup.py install

もしどの方法も失敗するのなら、あなたのアプリケーションにライブラリをそのままパッケージングするという手もあります。Beautiful Soupのライセンスはそれを認めています。.tar.gz形式でダウンロードし、それをアプリケーションのソースコード内に bs4 ディレクトリをコピーしてください。そうすれば、Beautiful Soupをインストールすることなしに使うことができます。

私は、Python 2.7とPython 3.2でBeautiful Soupを開発しましたが、他の最近のバージョンでも動くはずです。

インストール後の問題

Beautiful SoupはPython 2のコードとしてパッケージされています。 Beautiful SoupをPython 3環境で使おうとしてインストールすると、 それは自動的にPython 3のコードとして変換されます。 もし、Beautiful Soupパッケージをインストールしないと、コードは変換されません。 Windowsでは、間違ったバージョンが入っていると、それが報告されます。

ImportError “No module named HTMLParser” というエラーが表示されたら、 それはPython 3環境でPython 2で書かれたコードを実行しようとしたためです。

ImportError “No module named html.parser” というエラーが表示されたら、 それはPython 2環境でPython 3ので書かれたコードを実行しようとしたためです。

どちらの場合もとるべき対応は、Beautiful Soupを(tarballを解凍したときディレクトリを含め) 完全にアンインストールして、再インストールをすることです。

ROOT_TAG_NAME = u'[document]' 行で SyntaxError “Invalid syntax” のエラーが表示されたら、 Python 2で書かれたBeautiful SoupのコードをPython 3に変換しなければいけません。

そのためには、次のようにパッケージをインストールするか、:

$ python3 setup.py install

もしくは、手動で 2to3 変換スクリプトを bs4 ディレクトリで実行すればできます。:

$ 2to3-3.2 -w bs4

パーサーのインストール

Beautiful SoupはPythonの標準ライブラリに入っているHTMLパーサーをサポートすると同時に、多くのサードパーティーのPythonパーサーもサポートしています。一つには、 lxml parser. があります。環境に依りますが、以下のコマンドのどれかでlxmlはインストールできるでしょう。:

$ apt-get install python-lxml

$ easy_install lxml

$ pip install lxml

別のパーサーとして、Python純正の html5lib parser が挙げられます。これは HTMLをwebブラウザがするようにパースします。これも環境に依りますが、以下のコマンドのどれかでhtml5libはインストールできるでしょう。:

$ apt-get install python-html5lib

$ easy_install html5lib

$ pip install html5lib

この表は、各パーサーのライブラリの強みと弱みをまとめてあります。

パーサー 使用例 強み 弱み
Python’s html.parser BeautifulSoup(markup, "html.parser")
  • Batteries included
  • Decent speed
  • Lenient (as of Python 2.7.3 and 3.2.)
  • Not very lenient (before Python 2.7.3 or 3.2.2)
lxml’s HTML parser BeautifulSoup(markup, "lxml")
  • Very fast
  • Lenient
  • External C dependency
lxml’s XML parser BeautifulSoup(markup, ["lxml", "xml"]) BeautifulSoup(markup, "xml")
  • Very fast
  • The only currently supported XML parser
  • External C dependency
html5lib BeautifulSoup(markup, "html5lib")
  • Extremely lenient
  • Parses pages the same way a web browser does
  • Creates valid HTML5
  • Very slow
  • External Python dependency

できれば、速度のためにlxmlをインストールして使うことをお薦めします。 とくに、あなたがPython2.7.3のPython2系か、Python3.2.2より前のPython3系を使っているばあいは、必ずlxmlかhtml5libをインストールしたほうがよいです。 なぜなら、Pythonにはじめから組み込まれているHTMLパーサーは、古いバージョンのPythonではそこまで良く動かないからです。

構文が不正確なドキュメントのときは、パーサーが違うと生成されるパースツリーが異なってくることに注意してください。 詳しくは、 パーサーの違い を参照のこと。

スープの作成

ドキュメントをパース(構文解析)するには、 そのドキュメントを Beautiful Soup コンストラクタに渡します。 文字列でも開いたファイルハンドルでも渡せます。:

from bs4 import BeautifulSoup

soup = BeautifulSoup(open("index.html"))

soup = BeautifulSoup("<html>data</html>")

最初に、ドキュメントはUnicodeに変換され、HTMLエンティティはUnicode文字列に変換されます。:

BeautifulSoup("Sacr&eacute; bleu!")
<html><head></head><body>Sacré bleu!</body></html>

Beautiful Soupは、ドキュメントをもっとも適したパーサー(構文解析器)を使ってパースします。 XMLパーサーを使うように指定しなければ、HTMLパーサーが用いられます。( XMLのパース を参照)

4種類のオブジェクト

Beautiful Soup は複雑なHTMLドキュメントを、Pythonオブジェクトの複雑なツリー構造に変換します。 しかし、あなたは Tag, NavigableString, BeautifulSoup, Comment の4種類のオブジェクトだけ扱えばよいです。

Tag obj.

Tag オブジェクトは、元のドキュメント内のXMLやHTMLのタグに対応しています。:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>

Tag オブジェクトは、多くの属性とメソッドを持っています。それらのほとんどは、 パースツリーを探索パースツリーを検索 で説明します。この節では Tag オブジェクトの名前と属性について、最も重要な特徴について説明します。

名前

全てのHTMLタグは、 .name でアクセスできる名前を持っています。:

tag.name
# u'b'

Tag オブジェクトの name 属性を変えると、その変更はBeautiful Soupが生成する全てのドキュメントに反映されます。:

tag.name = "blockquote"
tag
# <blockquote class="boldest">Extremely bold</blockquote>

属性

HTMLタグは多くの属性を持ちます。 HTMLタグ<b class=”boldest”>は、”boldest”という値の’class’属性を持ちます。 Tag オブジェクトを辞書のように扱うことで、そのHTMLタグの属性にアクセスできます。:

tag['class']
# u'boldest'

.attrs で辞書に直接アクセスできます。:

tag.attrs
# {u'class': u'boldest'}

辞書のように Tag オブジェクトを扱うことにより、 HTMLタグの属性に対して追加, 削除, 修正も行うことができます。:

tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

tag['class']
# KeyError: 'class'
print(tag.get('class'))
# None

値が複数のとき

HTML4は、値を複数もてる2,3の属性を定義しています。 HTML5で、それらはなくなりましたが、別の同様の属性が定義されました。 もっとも一般的な値を複数もつ属性は class です。(たとえば、HTMLタグは複数のCSSクラスを持つことができます) また他の複数の値を持つ属性としては、 rel, rev, accept-charset, headers, and accesskey があります。 Beautiful Soupは、これらの属性の持つ値をリストとして示します。:

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.p['class']
# ["body", "strikeout"]

css_soup = BeautifulSoup('<p class="body"></p>')
css_soup.p['class']
# ["body"]

ある属性が複数の値をもっているようでも、HTML標準の定義から外れている場合、Beautiful Soupはその属性をひとまとまりの値として扱います。:

id_soup = BeautifulSoup('<p id="my id"></p>')
id_soup.p['id']
# 'my id'

??HTMLタグを文字列を変換したときは、値を複数もつ属性の値は一つにまとめられます。??:

rel_soup = BeautifulSoup('<p>Back to the <a rel="index">homepage</a></p>')
rel_soup.a['rel']
# ['index']
rel_soup.a['rel'] = ['index', 'contents']
print(rel_soup.p)
# <p>Back to the <a rel="index contents">homepage</a></p>

ドキュメントをXMLとしてパースすると、値を複数もつ属性はなくなります。:

xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml')
xml_soup.p['class']
# u'body strikeout'

BeautifulSoup obj.

Beautiful Soup オブジェクトは、それ自身で元のドキュメント全体を表しています。 たいていの場合、Tag obj. を扱うことで、用は足りるでしょう。 これは、Tag obj.パースツリーを探索パースツリーを検索. で述べられているメソッドの多くをサポートしているということです。

BeautifulSoup オブジェクトは、実際のHTMLやXMLタグに対応していないので、名前や属性を持たない。 しかし、 .name をみるような便利なものはいくつかある。そして、それらは特別な .name “[document]”を得られる(?訳がおかしい。けど次回まわし?):

soup.name
# u'[document]'

Comments obj. 他

Tag, NavigableString, BeautifulSoup はHTMLやXMLファイルのほぼ全てをカバーします。しかし、少しだけ残ったものがあります。それはコメントについてです。:

markup = "<b><!--Hey, buddy. Want to buy a used parser?--></b>"
soup = BeautifulSoup(markup)
comment = soup.b.string
type(comment)
# <class 'bs4.element.Comment'>

Comment オブジェクトは、 NavigableString オブジェクトの特別なタイプです。:

comment
# u'Hey, buddy. Want to buy a used parser'

コメントはHTMLの中にあらわれますが、 Comment は特別な書式で表示されます。:

print(soup.b.prettify())
# <b>
#  <!--Hey, buddy. Want to buy a used parser?-->
# </b>

Beautiful Soupは、XMLドキュメントのなかの他の全ての要素をクラス定義しています。 CData, ProcessingInstruction, Declaration, Doctype. Comment クラスのように、これらは文字に何かを加えた NavigableString のサブクラスです。 ここでは、コメントをCDDATAブロックに置換した例を示します。:

from bs4 import CData
cdata = CData("A CDATA block")
comment.replace_with(cdata)

print(soup.b.prettify())
# <b>
#  <![CDATA[A CDATA block]]>
# </b>

パースツリーを探索

ここで再び “Three sisters” のHTMLドキュメントです。:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

ドキュメントのある部分から他の部分へどのように移動するかを示すために、このドキュメントを例に使っていきます。

子要素へ下移動

Tags は string や他の tag を含んでいます。これらの要素は、tag の 子要素 です。Beautiful Soupは、tag の子要素を探索し扱うための多くの属性を提供します。

Beautiful Soupのstringは、これらの属性をサポートしません。なぜなら、string は子要素をもたないからです。

タグ名で探索

パースツリーを探索する一番簡単な方法は、あなたが取得したいタグの名前を使うことです。 もし、<head> タグを取得したければ、 soup.head と入力すればよいです。:

soup.head
# <head><title>The Dormouse's story</title></head>

soup.title
# <title>The Dormouse's story</title>

また、パースツリーのある部分から出発して、何度もズームインを繰り返す方法もあります。 このコードは、<body>タグ以下の最初の<b>タグを取得します。:

soup.body.b
# <b>The Dormouse's story</b>

属性としてタグ名を使うと、その名前のタグのうち 最初 にあるものを取得できます。:

soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

すべての <a>タグを取得したいときや、ある名前のタグのうち2番目以降のものをしたいときは、 パースツリーを検索 で述べられている find_all() のようなメソッドを使う必要があります。:

soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

.contents / .children

タグの子要素は、 .contents で呼び出すと、リストで取得できます。:

head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>

head_tag.contents
[<title>The Dormouse's story</title>]

title_tag = head_tag.contents[0]
title_tag
# <title>The Dormouse's story</title>
title_tag.contents
# [u'The Dormouse's story']

Beautiful Soup オブジェクトは、それ自身が子要素を持ちます。この場合、<html>タグが Beautiful Soup オブジェクトの子要素になります。:

len(soup.contents)
# 1
soup.contents[0].name
# u'html'

string は .contents を持ちません。なぜなら、それが何も含まないからです。:

text = title_tag.contents[0]
text.contents
# AttributeError: 'NavigableString' object has no attribute 'contents'

タグの子要素を、リストの代わりに、 .children ジェネレータを用いてイテレータで扱うこともできます。:

for child in title_tag.children:
    print(child)
# The Dormouse's story

.descendants

.contents.children 属性は、あるタグの 直下の 子要素のみを表します。 例えば、<head>タグは、ただ一つの直下の子要素である<title>タグを持ちます。:

head_tag.contents
# [<title>The Dormouse's story</title>]

しかし、この<title>タグ自身も、子要素に”The Dormouse’s story”文字列を持ちます。 この文字列もまた、<head>タグの子要素であるという意味になります。 そこで、 .descendants (子孫) 属性を用いると、 あるタグの 全ての 子要素を再帰的に取り出すことができます。 再帰的というのは、直下の子要素、そのまた子要素、そしてさらにといったふうに繰り返してということです。

for child in head_tag.descendants:
    print(child)
# <title>The Dormouse's story</title>
# The Dormouse's story

このドキュメントの<head>タグはただ1つの子要素しか持ちませんが、 <title>タグと<title>タグの子要素という2つの子孫要素を持ちます。 また、このドキュメントの BeautifulSoup オブジェクトには、 直下の子要素は<html>タグ1つしかありませんが、子孫要素はたくさんあります。:

len(list(soup.children))
# 1
len(list(soup.descendants))
# 25

.string

ある Tag オブジェクトが1つだけ子要素をもっていて、その子要素が NavigableString オブジェクトならば、 .string 属性で利用できます。:

title_tag.string
# u'The Dormouse's story'

ある Tag オブジェクトのただ1つの子要素が、別の Tag オブジェクトであって .string 属性を持つならば、元の Tag オブジェクトも同じ .string 属性を持つと考えられます。:

head_tag.contents
# [<title>The Dormouse's story</title>]

head_tag.string
# u'The Dormouse's story'

ある tag オブジェクトが複数の子要素を持ち、 .string 属性がどの子要素を参照しているかわからないとき、 .string 属性は None と定義されます。:

print(soup.html.string)
# None

.strings / .stripped_strings

あるタグの中にあるドキュメント本文が要素が複数であっても、それらの文字列をみることができます。 その場合は、 .strings ジェネレーターを使用します。:

for string in soup.strings:
    print(repr(string))
# u"The Dormouse's story"
# u'\n\n'
# u"The Dormouse's story"
# u'\n\n'
# u'Once upon a time there were three little sisters; and their names were\n'
# u'Elsie'
# u',\n'
# u'Lacie'
# u' and\n'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# u'...'
# u'\n'

これらの文字列は、大量の余計な空白が入りがちである。 そこで、 .stripped_strings ジェネレーターを代わりに用いることで、それら空白を除くことができる。:

for string in soup.stripped_strings:
    print(repr(string))
# u"The Dormouse's story"
# u"The Dormouse's story"
# u'Once upon a time there were three little sisters; and their names were'
# u'Elsie'
# u','
# u'Lacie'
# u'and'
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'...'

ここでは、文字列中に入る空白はそのままで、文字列の最初や最後に付く空白は削除されます。

親要素へ上移動

“家族ツリー”に例えると、すべてのタグや文字列はそれぞれが一つの親要素を持ちます。

.parent

.parent 属性で親要素にアクセスできます。 たとえば、”three sisters”ドキュメントでは、<head>タグは<title>タグの親要素です。:

title_tag = soup.title
title_tag
# <title>The Dormouse's story</title>
title_tag.parent
# <head><title>The Dormouse's story</title></head>

タイトル文字列はそれ自身が親要素を持ち、<title>タグはタイトル文字列を子要素に持ちます。:

title_tag.string.parent
# <title>The Dormouse's story</title>

<html>タグの様なトップレベルのタグは、 BeautifulSoup オブジェクトそれ自身になります。:

html_tag = soup.html
type(html_tag.parent)
# <class 'bs4.BeautifulSoup'>

そして、BeautifulSoup オブジェクトの .parent 属性は、Noneになります。:

print(soup.parent)
# None

.parents

あるタグに対する祖先要素全てを .parents で取得することができます。 以下は、HTMLドキュメントの深いところにある<a>タグからスタートして、最上層まで辿っています。:

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
for parent in link.parents:
    if parent is None:
        print(parent)
    else:
        print(parent.name)
# p
# body
# html
# [document]
# None

兄弟要素へ横移動

以下のようなシンプルなHTMLドキュメントを考えてみましょう。:

sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
print(sibling_soup.prettify())
# <html>
#  <body>
#   <a>
#    <b>
#     text1
#    </b>
#    <c>
#     text2
#    </c>
#   </a>
#  </body>
# </html>

<b>タグは<c>タグと同じレベルにあります。つまり、2つはともに同じタグの直下の子要素ということです。 こういった関係にあるタグを siblings (兄弟)といいます。 HTMLドキュメントをきれいに出力(?)したとき、siblingsは同じインデントレベルになります。 こういったタグの関係をコードで利用することができます。

.next_sibling / .previous_sibling

.next_sibling.previous_sibling を用いて、パースツリーの同じレベルの要素間を辿ることができます。:

sibling_soup.b.next_sibling
# <c>text2</c>

sibling_soup.c.previous_sibling
# <b>text1</b>

この<b>タグは .next_sibling は持ちますが、 .previous_sibling は持ちません。 なぜなら、<b>タグの前にはパースツリーで同レベルの要素がないからです。 同様に、<c>タグは .previous_sibling を持ちますが、.next_sibling は持ちません。:

print(sibling_soup.b.previous_sibling)
# None
print(sibling_soup.c.next_sibling)
# None

“text1”と”text”は兄弟ではありません。なぜなら、2つは同じ親をもたないからです。:

sibling_soup.b.string
# u'text1'

print(sibling_soup.b.string.next_sibling)
# None

実際のHTMLドキュメントをパースすると、 .next_sibling.previous_sibling は前後に空白を持ちます。 “three sisters”ドキュメントで見てみましょう。:

<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>

すなおに考えれば、最初の<a>タグの .next_sibling は2番目の<a>タグとなるはずですが、実際は違います。 それは、最初の<a>タグと2番目を分ける”コンマと改行コード”という文字列になります。:

link = soup.a
link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

link.next_sibling
# u',\n'

2番目の<a>タグは、そのコンマと改行コードの .next_sibling になります。:

link.next_sibling.next_sibling
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>

.next_siblings / .previous_siblings

複数の兄弟要素を .next_siblings.previous_siblings をイテレーターとして使って、まとめて扱えます。:

for sibling in soup.a.next_siblings:
    print(repr(sibling))
# u',\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u' and\n'
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
# u'; and they lived at the bottom of a well.'
# None

for sibling in soup.find(id="link3").previous_siblings:
    print(repr(sibling))
# ' and\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# u',\n'
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# u'Once upon a time there were three little sisters; and their names were\n'
# None

前後の要素へ移動

“three sisters”ドキュメントのはじめの部分を見てみましょう。:

<html><head><title>The Dormouse's story</title></head>
<p class="title"><b>The Dormouse's story</b></p>

HTMLパーサーは、この文字列を読み込み、イベントの連なりとして理解します。”open an <html> tag”, “open a <head> tag”, “open a <title> tag”, “add a string”, “close the <title> tag”, “open a <p>”... といったかんじです。Beautiful Soupはこのイベントの連なりを、さらに再構成して扱います。

.next_element / .previous_element

文字列やHTMLタグの .next_element 属性は、それの直後の要素を指し示します。 .next_string と同じようですが、決定的に違います。

“three sisters”ドキュメントの最後の<a>タグについて考えてみましょう。 それの .next_string はその<a>タグによって分割された文の後ろの部分の文字列です。(?):

last_a_tag = soup.find("a", id="link3")
last_a_tag
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_a_tag.next_sibling
# '; and they lived at the bottom of a well.'

一方、 .next_element は、<a>タグのすぐ後ろの要素である”Tillie”という単語を指し示します。文の残りの部分ではありません。:

last_a_tag.next_element
# u'Tillie'

これは元の文章で”Tillie”という単語がセミコロンの前に現れるからです。 パーサーは<a>タグに出会い、次に”Tillie”という単語、そして</a>という閉じるタグがきます。 そのあとは、セミコロンがあって、文の残りの部分です。 セミコロンは<a>タグと同じレベルにありますが、”Tillie”という単語が最初に出会います。

.previous_element 属性は、 .next_element とは逆です。 その要素の一つ前の要素を指し示します。:

last_a_tag.previous_element
# u' and\n'
last_a_tag.previous_element.next_element
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

.next_elements / .previous_elements

パースされたドキュメントの要素を、前後方向に取得していくイテレーターを使うこともできます。:

for element in last_a_tag.next_elements:
    print(repr(element))
# u'Tillie'
# u';\nand they lived at the bottom of a well.'
# u'\n\n'
# <p class="story">...</p>
# u'...'
# u'\n'
# None

パースツリーを検索

Beautiful Soupはパースパースツリーを検索する多くのメソッドを定義しています。 しかし、それらはどれもとても似通っています。 この章では、find()find_all() という2つの人気のメソッドの説明に、多くのスペースを費やします。 それ以外のメソッドは、ほとんど同じ引数を持つので、簡単な説明にとどめることにします。

ここでは再び、”three sisters”ドキュメントを例に使っていきます。:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)

find_all() のようなフィルターを通すことにより、 興味のあるドキュメントのある一部分にズームすることができます。

フィルターの種類

find_all() 等のメソッドの詳細を説明するまえに、これらのメソッドに渡せるフィルターの例を示します。 これらのフィルターは、検索APIを通して、何度も繰り返し示されます。 これらを使って、タグの名前、タグの属性、ドキュメント本文の文字列やそれらの組み合わせに対して、 フィルターをかけます。

文字列

一番シンプルなフィルターは文字列です。 検索メソッドとBeautiful Soupに文字列を渡すと、厳格に文字列を一致させます。 以下のコードは、ドキュメント内の<b>タグを全て見つけます。:

soup.find_all('b')
# [<b>The Dormouse's story</b>]

バイト文字列を渡した場合は、Beautiful SoupはUTF-8にエンコードされた文字列として扱います。 これを避けるには、代わりにUnicode文字列を渡せばよいです。

正規表現

正規表現オブジェクトを渡すと、Beautiful Soupは それの match() メソッドを用いて、 その正規表現に一致するものをマッチさせます。 以下のコードは、全ての”b”ではじまるつづりの名前のタグを見つけます。 ドキュメント例では、<body>タグと<b>タグです。:

import re
for tag in soup.find_all(re.compile("^b")):
    print(tag.name)
# body
# b

以下のコードでは、タグ名に”t”のつづりを含むもの全てを見つけます。:

for tag in soup.find_all(re.compile("t")):
    print(tag.name)
# html
# title

リスト

フィルターにリストで引数をわたすと、Beautiful Soupはそのリストの内のいずれかにマッチした要素を返します。 以下のコードは、全ての<a>タグと<b>タグを見つけます。:

soup.find_all(["a", "b"])
# [<b>The Dormouse's story</b>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

True値

True はすべての要素にマッチします。 以下のコードは、ドキュメント内の全てのタグをみつけます。ただしテキスト文字列はマッチされません。:

for tag in soup.find_all(True):
    print(tag.name)
# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p

関数

以上のフィルターで機能が足りないときは、自分で引数に要素をとる関数を定義することもできます。 その関数は、引数がマッチしたときは True を、そうでないときは False を返します。:

.. Here's a function that returns ``True`` if a tag defines the "class"
attribute but doesn’t define the “id” attribute:

以下の関数では、HTMLタグが “class” 属性を持ち、”id”属性を持たない場合に True を返します。:

def has_class_but_no_id(tag):
    return tag.has_attr('class') and not tag.has_attr('id')

この関数を find_all() に渡すと、”three sisters”ドキュメントから全ての<p>タグを取得できます。:

soup.find_all(has_class_but_no_id)
# [<p class="title"><b>The Dormouse's story</b></p>,
#  <p class="story">Once upon a time there were...</p>,
#  <p class="story">...</p>]

この関数は<p>タグだけを抽出します。 <a>タグは”class”と”id”の両方の属性を定義しているので抽出できません。 <html>や<title>のようなタグは、”class”を定義してないので、同様に抽出できません。

以下の関数は、HTMLタグがstringオブジェクトに囲まれているときは、 True を返します。(?):

from bs4 import NavigableString
def surrounded_by_strings(tag):
    return (isinstance(tag.next_element, NavigableString)
            and isinstance(tag.previous_element, NavigableString))

for tag in soup.find_all(surrounded_by_strings):
    print tag.name
# p
# a
# a
# a
# p

これで検索メソッドの詳細をみていくことの準備ができました。

find_all()

使い方: find_all(name, attrs, recursive, text, limit, **kwargs)

find_all() メソッドは、Tag オブジェクトが持つ子孫要素のうち、引数に一致する すべての ものを発見します。 フィルターの種類 でいくつかの例を挙げましたが、ここでもう少し説明します。:

soup.find_all("title")
# [<title>The Dormouse's story</title>]

soup.find_all("p", "title")
# [<p class="title"><b>The Dormouse's story</b></p>]

soup.find_all("a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.find_all(id="link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

import re
soup.find(text=re.compile("sisters"))
# u'Once upon a time there were three little sisters; and their names were\n'

これらの使い方は、すでに説明してるものもあれば、初出のものもあります。 textid に値を渡すのはどういう意味でしょうか? なぜ、find_all("p", "title") は、CSSの”title”タグをもつ<p>タグを発見したのでしょうか? find_all() の引数をみていきましょう。

name引数

find_all()name 引数に値を渡すと、タグの名前だけを対象に検索が行われます。 名前がマッチしないタグと同じように、テキスト文字列は無視されます。

以下の例は、もっともシンプルな使い方です。:

soup.find_all("title")
# [<title>The Dormouse's story</title>]

フィルターの種類 で述べたように、 name 引数は文字列, 正規表現, リスト, 関数, True値をとることができます。

キーワード引数

どのような理解できない引数でも、HTMLタグの属性の一つとして解釈されます。 キーワード引数 id に値を渡すと、Beautiful SoupはHTMLタグの’id’属性を対象にフィルタリングを行います。:

soup.find_all(id='link2')
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

キーワード引数 href に値を渡すと、Beautiful SoupはHTMLタグの’href’属性に対してフィルタリングを行います。:

soup.find_all(href=re.compile("elsie"))
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

キーワード引数の値もまた、 文字列, 正規表現, リスト, 関数, True値 をとることができます。

次のコードは、’id’属性に値が入っている全てのHTMLタグを見つけます。このとき、値は何でもあっても構いません。:

soup.find_all(id=True)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

複数のキーワード引数を一度に渡すことによって、複数の属性についてフィルタリングできます。:

soup.find_all(href=re.compile("elsie"), id='link1')
# [<a class="sister" href="http://example.com/elsie" id="link1">three</a>]

HTML5の ‘data-*’ 属性など、いくつかの属性についてはキーワード引数として用いることができません。:

data_soup = BeautifulSoup('<div data-foo="value">foo!</div>')
data_soup.find_all(data-foo="value")
# SyntaxError: keyword can't be an expression

しかし、これらの属性を辞書にして、キーワード引数 attr として値を渡せばフィルタリングすることができます。:

data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]

CSSのクラスで検索

HTMLタグが持つCSSのクラスで検索をかけるのはとても便利です。 しかし”class”はPythonの予約語のため、class をキーワード引数として用いると文法エラーになります。 そこで、Beautiful Soup 4.1.2からは、 class_ というキーワード引数でCSSのクラスを検索できるようになりました。:

soup.find_all("a", class_="sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

他のキーワード引数と同様、 class_ には文字列, 正規表現, 関数, True値を渡せます。:

soup.find_all(class_=re.compile("itl"))
# [<p class="title"><b>The Dormouse's story</b></p>]

def has_six_characters(css_class):
    return css_class is not None and len(css_class) == 6

soup.find_all(class_=has_six_characters)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Tag オブジェクトの属性の 値が複数のとき を思い出してください。 それと同様に、あるCSSクラスを検索するときは、複数のCSSクラスに対してマッチさせられます。:

css_soup = BeautifulSoup('<p class="body strikeout"></p>')
css_soup.find_all("p", class_="strikeout")
# [<p class="body strikeout"></p>]

css_soup.find_all("p", class_="body")
# [<p class="body strikeout"></p>]

class 属性の値は、文字列としても検索できます。:

css_soup.find_all("p", class_="body strikeout")
# [<p class="body strikeout"></p>]

しかし、文字列の値としての変数を検索することはできません。:

css_soup.find_all("p", class_="strikeout body")
# []

もしあなたが2つ以上のクラスをまっちさせたいなら、CSSセレクトを使ってください。:

css_soup.select("p.strikeout.body")
# [<p class="body strikeout"></p>]

Beautiful Soupの古いバージョンでは、 class_ 引数は使えません。 そこで、以下に述べる attrs トリックを使うことができます。 これは”class”をkeyに持つ辞書を attrs 引数に渡して、検索することができます。 この辞書のvalueには、文字列, 正規表現などが使えます。:

soup.find_all("a", attrs={"class": "sister"})
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

text引数

text 引数で、タグではなくテキスト文字列を対象に検索することができます。 name 引数やキーワード引数のように、 文字列 , 正規表現 , リスト , 関数 , True値 が使えます。 以下の例をごらんください。:

soup.find_all(text="Elsie")
# [u'Elsie']

soup.find_all(text=["Tillie", "Elsie", "Lacie"])
# [u'Elsie', u'Lacie', u'Tillie']

soup.find_all(text=re.compile("Dormouse"))
[u"The Dormouse's story", u"The Dormouse's story"]

def is_the_only_string_within_a_tag(s):
    """Return True if this string is the only child of its parent tag."""
    return (s == s.parent.string)

soup.find_all(text=is_the_only_string_within_a_tag)
# [u"The Dormouse's story", u"The Dormouse's story", u'Elsie', u'Lacie', u'Tillie', u'...']

text 引数はテキスト文字列の検索ですが、これにタグの検索を組みわせることもできます。 Beautiful Soupは、text 引数で指定した文字列を .string にもつタグ全てを見つけます。 次のコードは、.string に “Elsie”を持つ<a>タグを見つけます。:

soup.find_all("a", text="Elsie")
# [<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>]

limit引数

find_all() メソッドは、指定したフィルターにマッチした全てのタグと文字列を返します。 これはドキュメントが大きいときは時間がかかります。 もし、 全ての 結果を必要としなければ、limit 引数で取得する数を指定することができます。

“three siters”ドキュメントには3つのリンクがある、しかし以下のコードははじめの2つしか見つけない。:

soup.find_all("a", limit=2)
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

recursive引数

mytag.find_all() を実行すると、Beautiful Soupは、 mytag の全ての子孫要素を調べます。 (子要素、子要素の子要素、そのまた子要素というかんじで、、) もし、直下の子要素しか調べたくなければ、recursive=False という引数を渡せばよいです。 以下で違いをみてみましょう。:

soup.html.find_all("title")
# [<title>The Dormouse's story</title>]

soup.html.find_all("title", recursive=False)
# []

これはドキュメントの一部です。:

<html>
 <head>
  <title>
   The Dormouse's story
  </title>
 </head>
...

このドキュメントにおいて、<title>タグは<html>の下にはあるが、直下 にあるわけではありません。 Beautiful Soupが<title>タグを見つけることができるのは、<html>タグ以下の全ての子孫要素を探してよいときだけです。 もし、find_all() の引数に recurive=False という<html>タグの直下のみを検索するという制限がかかっていたら、<title>タグを見つけることはできません。

Beautiful Soupは、多くのパースツリーを検索するメソッドを提供しています。 それら多くは共通する引数を持ちます。 find_all()name, attrs, text, limit, キーワード引数は、他の多くのメソッドにも対応しています。 しかし、 recursive 引数は、 find_all(), find() の2つのメソッドしか対応していません。 find_parents() のようなメソッドに、引数 recursive=False を渡しても意味がありません。

find_all()的 タグのコール?

Because find_all() is the most popular method in the Beautiful Soup search API, you can use a shortcut for it. If you treat the BeautifulSoup object or a Tag object as though it were a function, then it’s the same as calling find_all() on that object. These two lines of code are equivalent:

soup.find_all("a")
soup("a")

These two lines are also equivalent:

soup.title.find_all(text=True)
soup.title(text=True)

find()

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

The find_all() method scans the entire document looking for results, but sometimes you only want to find one result. If you know a document only has one <body> tag, it’s a waste of time to scan the entire document looking for more. Rather than passing in limit=1 every time you call find_all, you can use the find() method. These two lines of code are nearly equivalent:

soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]

soup.find('title')
# <title>The Dormouse's story</title>

The only difference is that find_all() returns a list containing the single result, and find() just returns the result.

If find_all() can’t find anything, it returns an empty list. If find() can’t find anything, it returns None:

print(soup.find("nosuchtag"))
# None

Remember the soup.head.title trick from タグ名で探索? That trick works by repeatedly calling find():

soup.head.title
# <title>The Dormouse's story</title>

soup.find("head").find("title")
# <title>The Dormouse's story</title>

find_parents() / find_parent()

Signature: find_parents(name, attrs, text, limit, **kwargs)

Signature: find_parent(name, attrs, text, **kwargs)

I spent a lot of time above covering find_all() and find(). The Beautiful Soup API defines ten other methods for searching the tree, but don’t be afraid. Five of these methods are basically the same as find_all(), and the other five are basically the same as find(). The only differences are in what parts of the tree they search.

First let’s consider find_parents() and find_parent(). Remember that find_all() and find() work their way down the tree, looking at tag’s descendants. These methods do the opposite: they work their way up the tree, looking at a tag’s (or a string’s) parents. Let’s try them out, starting from a string buried deep in the “three daughters” document:

a_string = soup.find(text="Lacie")
a_string
# u'Lacie'

a_string.find_parents("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

a_string.find_parent("p")
# <p class="story">Once upon a time there were three little sisters; and their names were
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> and
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>;
#  and they lived at the bottom of a well.</p>

a_string.find_parents("p", class="title")
# []

One of the three <a> tags is the direct parent of the string in question, so our search finds it. One of the three <p> tags is an indirect parent of the string, and our search finds that as well. There’s a <p> tag with the CSS class “title” somewhere in the document, but it’s not one of this string’s parents, so we can’t find it with find_parents().

You may have made the connection between find_parent() and find_parents(), and the .parent and .parents attributes mentioned earlier. The connection is very strong. These search methods actually use .parents to iterate over all the parents, and check each one against the provided filter to see if it matches.

find_next_siblings() / find_next_sibling()

Signature: find_next_siblings(name, attrs, text, limit, **kwargs)

Signature: find_next_sibling(name, attrs, text, **kwargs)

These methods use .next_siblings to iterate over the rest of an element’s siblings in the tree. The find_next_siblings() method returns all the siblings that match, and find_next_sibling() only returns the first one:

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_next_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_next_sibling("p")
# <p class="story">...</p>

find_previous_siblings() / find_previous_sibling()

Signature: find_previous_siblings(name, attrs, text, limit, **kwargs)

Signature: find_previous_sibling(name, attrs, text, **kwargs)

These methods use .previous_siblings to iterate over an element’s siblings that precede it in the tree. The find_previous_siblings() method returns all the siblings that match, and find_previous_sibling() only returns the first one:

last_link = soup.find("a", id="link3")
last_link
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

last_link.find_previous_siblings("a")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

first_story_paragraph = soup.find("p", "story")
first_story_paragraph.find_previous_sibling("p")
# <p class="title"><b>The Dormouse's story</b></p>

find_all_next() / find_next()

Signature: find_all_next(name, attrs, text, limit, **kwargs)

Signature: find_next(name, attrs, text, **kwargs)

These methods use .next_elements to iterate over whatever tags and strings that come after it in the document. The find_all_next() method returns all matches, and find_next() only returns the first match:

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_next(text=True)
# [u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
#  u';\nand they lived at the bottom of a well.', u'\n\n', u'...', u'\n']

first_link.find_next("p")
# <p class="story">...</p>

In the first example, the string “Elsie” showed up, even though it was contained within the <a> tag we started from. In the second example, the last <p> tag in the document showed up, even though it’s not in the same part of the tree as the <a> tag we started from. For these methods, all that matters is that an element match the filter, and show up later in the document than the starting element.

find_all_previous() / find_previous()

Signature: find_all_previous(name, attrs, text, limit, **kwargs)

Signature: find_previous(name, attrs, text, **kwargs)

These methods use .previous_elements to iterate over the tags and strings that came before it in the document. The find_all_previous() method returns all matches, and find_previous() only returns the first match:

first_link = soup.a
first_link
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>

first_link.find_all_previous("p")
# [<p class="story">Once upon a time there were three little sisters; ...</p>,
#  <p class="title"><b>The Dormouse's story</b></p>]

first_link.find_previous("title")
# <title>The Dormouse's story</title>

The call to find_all_previous("p") found the first paragraph in the document (the one with class=”title”), but it also finds the second paragraph, the <p> tag that contains the <a> tag we started with. This shouldn’t be too surprising: we’re looking at all the tags that show up earlier in the document than the one we started with. A <p> tag that contains an <a> tag must have shown up before the <a> tag it contains.

CSS selectors

Beautiful Soup supports the most commonly-used CSS selectors. Just pass a string into the .select() method of a Tag object or the BeautifulSoup object itself.

You can find tags:

soup.select("title")
# [<title>The Dormouse's story</title>]

soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]

Find tags beneath other tags:

soup.select("body a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("html head title")
# [<title>The Dormouse's story</title>]

Find tags directly beneath other tags:

soup.select("head > title")
# [<title>The Dormouse's story</title>]

soup.select("p > a")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie"  id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("p > a:nth-of-type(2)")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

soup.select("p > #link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("body > a")
# []

Find the siblings of tags:

soup.select("#link1 ~ .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie"  id="link3">Tillie</a>]

soup.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Find tags by CSS class:

soup.select(".sister")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select("[class~=sister]")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Find tags by ID:

soup.select("#link1")
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select("a#link2")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

Test for the existence of an attribute:

soup.select('a[href]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

Find tags by attribute value:

soup.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.select('a[href^="http://example.com/"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

soup.select('a[href*=".com/el"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

Match language codes:

multilingual_markup = """
 <p lang="en">Hello</p>
 <p lang="en-us">Howdy, y'all</p>
 <p lang="en-gb">Pip-pip, old fruit</p>
 <p lang="fr">Bonjour mes amis</p>
"""
multilingual_soup = BeautifulSoup(multilingual_markup)
multilingual_soup.select('p[lang|=en]')
# [<p lang="en">Hello</p>,
#  <p lang="en-us">Howdy, y'all</p>,
#  <p lang="en-gb">Pip-pip, old fruit</p>]

This is a convenience for users who know the CSS selector syntax. You can do all this stuff with the Beautiful Soup API. And if CSS selectors are all you need, you might as well use lxml directly: it’s a lot faster, and it supports more CSS selectors . But this lets you combine simple CSS selectors with the Beautiful Soup API.

パースツリーを修正

Beautiful Soup’s main strength is in searching the parse tree, but you can also modify the tree and write your changes as a new HTML or XML document.

名前や属性の変更

I covered this earlier, in 属性, but it bears repeating. You can rename a tag, change the values of its attributes, add new attributes, and delete attributes:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b

tag.name = "blockquote"
tag['class'] = 'verybold'
tag['id'] = 1
tag
# <blockquote class="verybold" id="1">Extremely bold</blockquote>

del tag['class']
del tag['id']
tag
# <blockquote>Extremely bold</blockquote>

.string の修正

If you set a tag’s .string attribute, the tag’s contents are replaced with the string you give:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)

tag = soup.a
tag.string = "New link text."
tag
# <a href="http://example.com/">New link text.</a>

Be careful: if the tag contained other tags, they and all their contents will be destroyed.

append()

You can add to a tag’s contents with Tag.append(). It works just like calling .append() on a Python list:

soup = BeautifulSoup("<a>Foo</a>")
soup.a.append("Bar")

soup
# <html><head></head><body><a>FooBar</a></body></html>
soup.a.contents
# [u'Foo', u'Bar']

BeautifulSoup.new_string() / .new_tag()

If you need to add a string to a document, no problem–you can pass a Python string in to append(), or you can call the factory method BeautifulSoup.new_string():

soup = BeautifulSoup("<b></b>")
tag = soup.b
tag.append("Hello")
new_string = soup.new_string(" there")
tag.append(new_string)
tag
# <b>Hello there.</b>
tag.contents
# [u'Hello', u' there']

If you want to create a comment or some other subclass of NavigableString, pass that class as the second argument to new_string():

from bs4 import Comment
new_comment = soup.new_string("Nice to see you.", Comment)
tag.append(new_comment)
tag
# <b>Hello there<!--Nice to see you.--></b>
tag.contents
# [u'Hello', u' there', u'Nice to see you.']

(This is a new feature in Beautiful Soup 4.2.1.)

What if you need to create a whole new tag? The best solution is to call the factory method BeautifulSoup.new_tag():

soup = BeautifulSoup("<b></b>")
original_tag = soup.b

new_tag = soup.new_tag("a", href="http://www.example.com")
original_tag.append(new_tag)
original_tag
# <b><a href="http://www.example.com"></a></b>

new_tag.string = "Link text."
original_tag
# <b><a href="http://www.example.com">Link text.</a></b>

Only the first argument, the tag name, is required.

insert()

Tag.insert() is just like Tag.append(), except the new element doesn’t necessarily go at the end of its parent’s .contents. It’ll be inserted at whatever numeric position you say. It works just like .insert() on a Python list:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.insert(1, "but did not endorse ")
tag
# <a href="http://example.com/">I linked to but did not endorse <i>example.com</i></a>
tag.contents
# [u'I linked to ', u'but did not endorse', <i>example.com</i>]

insert_before() / insert_after()

The insert_before() method inserts a tag or string immediately before something else in the parse tree:

soup = BeautifulSoup("<b>stop</b>")
tag = soup.new_tag("i")
tag.string = "Don't"
soup.b.string.insert_before(tag)
soup.b
# <b><i>Don't</i>stop</b>

The insert_after() method moves a tag or string so that it immediately follows something else in the parse tree:

soup.b.i.insert_after(soup.new_string(" ever "))
soup.b
# <b><i>Don't</i> ever stop</b>
soup.b.contents
# [<i>Don't</i>, u' ever ', u'stop']

clear()

Tag.clear() removes the contents of a tag:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
tag = soup.a

tag.clear()
tag
# <a href="http://example.com/"></a>

extract()

PageElement.extract() removes a tag or string from the tree. It returns the tag or string that was extracted:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

i_tag = soup.i.extract()

a_tag
# <a href="http://example.com/">I linked to</a>

i_tag
# <i>example.com</i>

print(i_tag.parent)
None

At this point you effectively have two parse trees: one rooted at the BeautifulSoup object you used to parse the document, and one rooted at the tag that was extracted. You can go on to call extract on a child of the element you extracted:

my_string = i_tag.string.extract()
my_string
# u'example.com'

print(my_string.parent)
# None
i_tag
# <i></i>

decompose()

Tag.decompose() removes a tag from the tree, then completely destroys it and its contents:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

soup.i.decompose()

a_tag
# <a href="http://example.com/">I linked to</a>

replace_with()

PageElement.replace_with() removes a tag or string from the tree, and replaces it with the tag or string of your choice:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

new_tag = soup.new_tag("b")
new_tag.string = "example.net"
a_tag.i.replace_with(new_tag)

a_tag
# <a href="http://example.com/">I linked to <b>example.net</b></a>

replace_with() returns the tag or string that was replaced, so that you can examine it or add it back to another part of the tree.

wrap()

PageElement.wrap() wraps an element in the tag you specify. It returns the new wrapper:

soup = BeautifulSoup("<p>I wish I was bold.</p>")
soup.p.string.wrap(soup.new_tag("b"))
# <b>I wish I was bold.</b>

soup.p.wrap(soup.new_tag("div")
# <div><p><b>I wish I was bold.</b></p></div>

This method is new in Beautiful Soup 4.0.5.

unwrap()

Tag.unwrap() is the opposite of wrap(). It replaces a tag with whatever’s inside that tag. It’s good for stripping out markup:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
a_tag = soup.a

a_tag.i.unwrap()
a_tag
# <a href="http://example.com/">I linked to example.com</a>

Like replace_with(), unwrap() returns the tag that was replaced.

出力

きれいに出力

The prettify() method will turn a Beautiful Soup parse tree into a nicely formatted Unicode string, with each HTML/XML tag on its own line:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup)
soup.prettify()
# '<html>\n <head>\n </head>\n <body>\n  <a href="http://example.com/">\n...'

print(soup.prettify())
# <html>
#  <head>
#  </head>
#  <body>
#   <a href="http://example.com/">
#    I linked to
#    <i>
#     example.com
#    </i>
#   </a>
#  </body>
# </html>

You can call prettify() on the top-level BeautifulSoup object, or on any of its Tag objects:

print(soup.a.prettify())
# <a href="http://example.com/">
#  I linked to
#  <i>
#   example.com
#  </i>
# </a>

一行に出力

If you just want a string, with no fancy formatting, you can call unicode() or str() on a BeautifulSoup object, or a Tag within it:

str(soup)
# '<html><head></head><body><a href="http://example.com/">I linked to <i>example.com</i></a></body></html>'

unicode(soup.a)
# u'<a href="http://example.com/">I linked to <i>example.com</i></a>'

The str() function returns a string encoded in UTF-8. See エンコード for other options.

You can also call encode() to get a bytestring, and decode() to get Unicode.

フォーマットを指定

If you give Beautiful Soup a document that contains HTML entities like “&lquot;”, they’ll be converted to Unicode characters:

soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'

If you then convert the document to a string, the Unicode characters will be encoded as UTF-8. You won’t get the HTML entities back:

str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'

By default, the only characters that are escaped upon output are bare ampersands and angle brackets. These get turned into “&amp;”, “&lt;”, and “&gt;”, so that Beautiful Soup doesn’t inadvertently generate invalid HTML or XML:

soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>")
soup.p
# <p>The law firm of Dewey, Cheatem, &amp; Howe</p>

soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>')
soup.a
# <a href="http://example.com/?foo=val1&amp;bar=val2">A link</a>

You can change this behavior by providing a value for the formatter argument to prettify(), encode(), or decode(). Beautiful Soup recognizes four possible values for formatter.

The default is formatter="minimal". Strings will only be processed enough to ensure that Beautiful Soup generates valid HTML/XML:

french = "<p>Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;</p>"
soup = BeautifulSoup(french)
print(soup.prettify(formatter="minimal"))
# <html>
#  <body>
#   <p>
#    Il a dit &lt;&lt;Sacrテゥ bleu!&gt;&gt;
#   </p>
#  </body>
# </html>

If you pass in formatter="html", Beautiful Soup will convert Unicode characters to HTML entities whenever possible:

print(soup.prettify(formatter="html"))
# <html>
#  <body>
#   <p>
#    Il a dit &lt;&lt;Sacr&eacute; bleu!&gt;&gt;
#   </p>
#  </body>
# </html>

If you pass in formatter=None, Beautiful Soup will not modify strings at all on output. This is the fastest option, but it may lead to Beautiful Soup generating invalid HTML/XML, as in these examples:

print(soup.prettify(formatter=None))
# <html>
#  <body>
#   <p>
#    Il a dit <<Sacrテゥ bleu!>>
#   </p>
#  </body>
# </html>

link_soup = BeautifulSoup('<a href="http://example.com/?foo=val1&bar=val2">A link</a>')
print(link_soup.a.encode(formatter=None))
# <a href="http://example.com/?foo=val1&bar=val2">A link</a>

Finally, if you pass in a function for formatter, Beautiful Soup will call that function once for every string and attribute value in the document. You can do whatever you want in this function. Here’s a formatter that converts strings to uppercase and does absolutely nothing else:

def uppercase(str):
    return str.upper()

print(soup.prettify(formatter=uppercase))
# <html>
#  <body>
#   <p>
#    IL A DIT <<SACRテ BLEU!>>
#   </p>
#  </body>
# </html>

print(link_soup.a.prettify(formatter=uppercase))
# <a href="HTTP://EXAMPLE.COM/?FOO=VAL1&BAR=VAL2">
#  A LINK
# </a>

If you’re writing your own function, you should know about the EntitySubstitution class in the bs4.dammit module. This class implements Beautiful Soup’s standard formatters as class methods: the “html” formatter is EntitySubstitution.substitute_html, and the “minimal” formatter is EntitySubstitution.substitute_xml. You can use these functions to simulate formatter=html or formatter==minimal, but then do something extra.

Here’s an example that replaces Unicode characters with HTML entities whenever possible, but also converts all strings to uppercase:

from bs4.dammit import EntitySubstitution
def uppercase_and_substitute_html_entities(str):
    return EntitySubstitution.substitute_html(str.upper())

print(soup.prettify(formatter=uppercase_and_substitute_html_entities))
# <html>
#  <body>
#   <p>
#    IL A DIT &lt;&lt;SACR&Eacute; BLEU!&gt;&gt;
#   </p>
#  </body>
# </html>

One last caveat: if you create a CData object, the text inside that object is always presented exactly as it appears, with no formatting. Beautiful Soup will call the formatter method, just in case you’ve written a custom method that counts all the strings in the document or something, but it will ignore the return value:

from bs4.element import CData
soup = BeautifulSoup("<a></a>")
soup.a.string = CData("one < three")
print(soup.a.prettify(formatter="xml"))
# <a>
#  <![CDATA[one < three]]>
# </a>

get_text()

If you only want the text part of a document or tag, you can use the get_text() method. It returns all the text in a document or beneath a tag, as a single Unicode string:

markup = '<a href="http://example.com/">\nI linked to <i>example.com</i>\n</a>'
soup = BeautifulSoup(markup)

soup.get_text()
u'\nI linked to example.com\n'
soup.i.get_text()
u'example.com'

You can specify a string to be used to join the bits of text together:

# soup.get_text("|")
u'\nI linked to |example.com|\n'

You can tell Beautiful Soup to strip whitespace from the beginning and end of each bit of text:

# soup.get_text("|", strip=True)
u'I linked to|example.com'

But at that point you might want to use the .stripped_strings generator instead, and process the text yourself:

[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']

使うパーサーの指定

If you just need to parse some HTML, you can dump the markup into the BeautifulSoup constructor, and it’ll probably be fine. Beautiful Soup will pick a parser for you and parse the data. But there are a few additional arguments you can pass in to the constructor to change which parser is used.

The first argument to the BeautifulSoup constructor is a string or an open filehandle–the markup you want parsed. The second argument is how you’d like the markup parsed.

If you don’t specify anything, you’ll get the best HTML parser that’s installed. Beautiful Soup ranks lxml’s parser as being the best, then html5lib’s, then Python’s built-in parser. You can override this by specifying one of the following:

  • What type of markup you want to parse. Currently supported are “html”, “xml”, and “html5”.
  • The name of the parser library you want to use. Currently supported options are “lxml”, “html5lib”, and “html.parser” (Python’s built-in HTML parser).

The section パーサーのインストール contrasts the supported parsers.

If you don’t have an appropriate parser installed, Beautiful Soup will ignore your request and pick a different parser. Right now, the only supported XML parser is lxml. If you don’t have lxml installed, asking for an XML parser won’t give you one, and asking for “lxml” won’t work either.

パーサーの違い

Beautiful Soup presents the same interface to a number of different parsers, but each parser is different. Different parsers will create different parse trees from the same document. The biggest differences are between the HTML parsers and the XML parsers. Here’s a short document, parsed as HTML:

BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>

Since an empty <b /> tag is not valid HTML, the parser turns it into a <b></b> tag pair.

Here’s the same document parsed as XML (running this requires that you have lxml installed). Note that the empty <b /> tag is left alone, and that the document is given an XML declaration instead of being put into an <html> tag.:

BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>

There are also differences between HTML parsers. If you give Beautiful Soup a perfectly-formed HTML document, these differences won’t matter. One parser will be faster than another, but they’ll all give you a data structure that looks exactly like the original HTML document.

But if the document is not perfectly-formed, different parsers will give different results. Here’s a short, invalid document parsed using lxml’s HTML parser. Note that the dangling </p> tag is simply ignored:

BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>

Here’s the same document parsed using html5lib:

BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>

Instead of ignoring the dangling </p> tag, html5lib pairs it with an opening <p> tag. This parser also adds an empty <head> tag to the document.

Here’s the same document parsed with Python’s built-in HTML parser:

BeautifulSoup("<a></p>", "html.parser")
# <a></a>

Like html5lib, this parser ignores the closing </p> tag. Unlike html5lib, this parser makes no attempt to create a well-formed HTML document by adding a <body> tag. Unlike lxml, it doesn’t even bother to add an <html> tag.

Since the document “<a></p>” is invalid, none of these techniques is the “correct” way to handle it. The html5lib parser uses techniques that are part of the HTML5 standard, so it has the best claim on being the “correct” way, but all three techniques are legitimate.

Differences between parsers can affect your script. If you’re planning on distributing your script to other people, or running it on multiple machines, you should specify a parser in the BeautifulSoup constructor. That will reduce the chances that your users parse a document differently from the way you parse it.

エンコード

Any HTML or XML document is written in a specific encoding like ASCII or UTF-8. But when you load that document into Beautiful Soup, you’ll discover it’s been converted to Unicode:

markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'

It’s not magic. (That sure would be nice.) Beautiful Soup uses a sub-library called Unicode, Dammit to detect a document’s encoding and convert it to Unicode. The autodetected encoding is available as the .original_encoding attribute of the BeautifulSoup object:

soup.original_encoding
'utf-8'

Unicode, Dammit guesses correctly most of the time, but sometimes it makes mistakes. Sometimes it guesses correctly, but only after a byte-by-byte search of the document that takes a very long time. If you happen to know a document’s encoding ahead of time, you can avoid mistakes and delays by passing it to the BeautifulSoup constructor as from_encoding.

Here’s a document written in ISO-8859-8. The document is so short that Unicode, Dammit can’t get a good lock on it, and misidentifies it as ISO-8859-7:

markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>ホスホオホシマ</h1>
soup.original_encoding
'ISO-8859-7'

We can fix this by passing in the correct from_encoding:

soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>ラ旛勉慵ゥ</h1>
soup.original_encoding
'iso8859-8'

In rare cases (usually when a UTF-8 document contains text written in a completely different encoding), the only way to get Unicode may be to replace some characters with the special Unicode character “REPLACEMENT CHARACTER” (U+FFFD, �ス). If Unicode, Dammit needs to do this, it will set the .contains_replacement_characters attribute to True on the UnicodeDammit or BeautifulSoup object. This lets you know that the Unicode representation is not an exact representation of the original–some data was lost. If a document contains �ス, but .contains_replacement_characters is False, you’ll know that the �ス was there originally (as it is in this paragraph) and doesn’t stand in for missing data.

出力のエンコード

When you write out a document from Beautiful Soup, you get a UTF-8 document, even if the document wasn’t in UTF-8 to begin with. Here’s a document written in the Latin-1 encoding:

markup = b'''
 <html>
  <head>
   <meta content="text/html; charset=ISO-Latin-1" http-equiv="Content-type" />
  </head>
  <body>
   <p>Sacr\xe9 bleu!</p>
  </body>
 </html>
'''

soup = BeautifulSoup(markup)
print(soup.prettify())
# <html>
#  <head>
#   <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
#  </head>
#  <body>
#   <p>
#    Sacrテゥ bleu!
#   </p>
#  </body>
# </html>

Note that the <meta> tag has been rewritten to reflect the fact that the document is now in UTF-8.

If you don’t want UTF-8, you can pass an encoding into prettify():

print(soup.prettify("latin-1"))
# <html>
#  <head>
#   <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...

You can also call encode() on the BeautifulSoup object, or any element in the soup, just as if it were a Python string:

soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'

soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'

Any characters that can’t be represented in your chosen encoding will be converted into numeric XML entity references. Here’s a document that includes the Unicode character SNOWMAN:

markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b

The SNOWMAN character can be part of a UTF-8 document (it looks like 笘), but there’s no representation for that character in ISO-Latin-1 or ASCII, so it’s converted into “&#9731” for those encodings:

print(tag.encode("utf-8"))
# <b>笘</b>

print tag.encode("latin-1")
# <b>&#9731;</b>

print tag.encode("ascii")
# <b>&#9731;</b>

Unicode, Dammit

You can use Unicode, Dammit without using Beautiful Soup. It’s useful whenever you have data in an unknown encoding and you just want it to become Unicode:

from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacrテゥ bleu!
dammit.original_encoding
# 'utf-8'

Unicode, Dammit’s guesses will get a lot more accurate if you install the chardet or cchardet Python libraries. The more data you give Unicode, Dammit, the more accurately it will guess. If you have your own suspicions as to what the encoding might be, you can pass them in as a list:

dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacrテゥ bleu!
dammit.original_encoding
# 'latin-1'

Unicode, Dammit has two special features that Beautiful Soup doesn’t use.

quote

You can use Unicode, Dammit to convert Microsoft smart quotes to HTML or XML entities:

markup = b"<p>I just \x93love\x94 Microsoft Word\x92s smart quotes</p>"

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="html").unicode_markup
# u'<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just &#x201C;love&#x201D; Microsoft Word&#x2019;s smart quotes</p>'

You can also convert Microsoft smart quotes to ASCII quotes:

UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'

Hopefully you’ll find this feature useful, but Beautiful Soup doesn’t use it. Beautiful Soup prefers the default behavior, which is to convert Microsoft smart quotes to Unicode characters along with everything else:

UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'

Inconsistent encodings

Sometimes a document is mostly in UTF-8, but contains Windows-1252 characters such as (again) Microsoft smart quotes. This can happen when a website includes data from multiple sources. You can use UnicodeDammit.detwingle() to turn such a document into pure UTF-8. Here’s a simple example:

snowmen = (u"\N{SNOWMAN}" * 3)
quote = (u"\N{LEFT DOUBLE QUOTATION MARK}I like snowmen!\N{RIGHT DOUBLE QUOTATION MARK}")
doc = snowmen.encode("utf8") + quote.encode("windows_1252")

This document is a mess. The snowmen are in UTF-8 and the quotes are in Windows-1252. You can display the snowmen or the quotes, but not both:

print(doc)
# 笘��笘�ソスI like snowmen!�ス

print(doc.decode("windows-1252"))
# テ「ヒ愴津「ヒ愴津「ヒ愴停廬 like snowmen!窶

Decoding the document as UTF-8 raises a UnicodeDecodeError, and decoding it as Windows-1252 gives you gibberish. Fortunately, UnicodeDammit.detwingle() will convert the string to pure UTF-8, allowing you to decode it to Unicode and display the snowmen and quote marks simultaneously:

new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# 笘��笘�廬 like snowmen!窶

UnicodeDammit.detwingle() only knows how to handle Windows-1252 embedded in UTF-8 (or vice versa, I suppose), but this is the most common case.

Note that you must know to call UnicodeDammit.detwingle() on your data before passing it into BeautifulSoup or the UnicodeDammit constructor. Beautiful Soup assumes that a document has a single encoding, whatever it might be. If you pass it a document that contains both UTF-8 and Windows-1252, it’s likely to think the whole document is Windows-1252, and the document will come out looking like ` テ「ヒ愴津「ヒ愴津「ヒ愴停廬 like snowmen!窶拜.

UnicodeDammit.detwingle() is new in Beautiful Soup 4.1.0.

ドキュメントの一部をパース

Let’s say you want to use Beautiful Soup look at a document’s <a> tags. It’s a waste of time and memory to parse the entire document and then go over it again looking for <a> tags. It would be much faster to ignore everything that wasn’t an <a> tag in the first place. The SoupStrainer class allows you to choose which parts of an incoming document are parsed. You just create a SoupStrainer and pass it in to the BeautifulSoup constructor as the parse_only argument.

(Note that this feature won’t work if you’re using the html5lib parser. If you use html5lib, the whole document will be parsed, no matter what. This is because html5lib constantly rearranges the parse tree as it works, and if some part of the document didn’t actually make it into the parse tree, it’ll crash. To avoid confusion, in the examples below I’ll be forcing Beautiful Soup to use Python’s built-in parser.)

SoupStrainer

The SoupStrainer class takes the same arguments as a typical method from パースツリーを検索: name, attrs, text, and **kwargs. Here are three SoupStrainer objects:

from bs4 import SoupStrainer

only_a_tags = SoupStrainer("a")

only_tags_with_id_link2 = SoupStrainer(id="link2")

def is_short_string(string):
    return len(string) < 10

only_short_strings = SoupStrainer(text=is_short_string)

I’m going to bring back the “three sisters” document one more time, and we’ll see what the document looks like when it’s parsed with these three SoupStrainer objects:

html_doc = """
<html><head><title>The Dormouse's story</title></head>

<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_a_tags).prettify())
# <a class="sister" href="http://example.com/elsie" id="link1">
#  Elsie
# </a>
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>
# <a class="sister" href="http://example.com/tillie" id="link3">
#  Tillie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_tags_with_id_link2).prettify())
# <a class="sister" href="http://example.com/lacie" id="link2">
#  Lacie
# </a>

print(BeautifulSoup(html_doc, "html.parser", parse_only=only_short_strings).prettify())
# Elsie
# ,
# Lacie
# and
# Tillie
# ...
#

You can also pass a SoupStrainer into any of the methods covered in パースツリーを検索. This probably isn’t terribly useful, but I thought I’d mention it:

soup = BeautifulSoup(html_doc)
soup.find_all(only_short_strings)
# [u'\n\n', u'\n\n', u'Elsie', u',\n', u'Lacie', u' and\n', u'Tillie',
#  u'\n\n', u'...', u'\n']

トラブルシューティング

diagnose()

If you’re having trouble understanding what Beautiful Soup does to a document, pass the document into the diagnose() function. (New in Beautiful Soup 4.2.0.) Beautiful Soup will print out a report showing you how different parsers handle the document, and tell you if you’re missing a parser that Beautiful Soup could be using:

from bs4.diagnose import diagnose
data = open("bad.html").read()
diagnose(data)

# Diagnostic running on Beautiful Soup 4.2.0
# Python version 2.7.3 (default, Aug  1 2012, 05:16:07)
# I noticed that html5lib is not installed. Installing it may help.
# Found lxml version 2.3.2.0
#
# Trying to parse your data with html.parser
# Here's what html.parser did with the document:
# ...

Just looking at the output of diagnose() may show you how to solve the problem. Even if not, you can paste the output of diagnose() when asking for help.

パース時に出るエラー

There are two different kinds of parse errors. There are crashes, where you feed a document to Beautiful Soup and it raises an exception, usually an HTMLParser.HTMLParseError. And there is unexpected behavior, where a Beautiful Soup parse tree looks a lot different than the document used to create it.

Almost none of these problems turn out to be problems with Beautiful Soup. This is not because Beautiful Soup is an amazingly well-written piece of software. It’s because Beautiful Soup doesn’t include any parsing code. Instead, it relies on external parsers. If one parser isn’t working on a certain document, the best solution is to try a different parser. See パーサーのインストール for details and a parser comparison.

The most common parse errors are HTMLParser.HTMLParseError: malformed start tag and HTMLParser.HTMLParseError: bad end tag. These are both generated by Python’s built-in HTML parser library, and the solution is to install lxml or html5lib.

The most common type of unexpected behavior is that you can’t find a tag that you know is in the document. You saw it going in, but find_all() returns [] or find() returns None. This is another common problem with Python’s built-in HTML parser, which sometimes skips tags it doesn’t understand. Again, the solution is to install lxml or html5lib.

バージョン違いの問題

  • SyntaxError: Invalid syntax (on the line ROOT_TAG_NAME = u'[document]'): Caused by running the Python 2 version of Beautiful Soup under Python 3, without converting the code.
  • ImportError: No module named HTMLParser - Caused by running the Python 2 version of Beautiful Soup under Python 3.
  • ImportError: No module named html.parser - Caused by running the Python 3 version of Beautiful Soup under Python 2.
  • ImportError: No module named BeautifulSoup - Caused by running Beautiful Soup 3 code on a system that doesn’t have BS3 installed. Or, by writing Beautiful Soup 4 code without knowing that the package name has changed to bs4.
  • ImportError: No module named bs4 - Caused by running Beautiful Soup 4 code on a system that doesn’t have BS4 installed.

XMLのパース

By default, Beautiful Soup parses documents as HTML. To parse a document as XML, pass in “xml” as the second argument to the BeautifulSoup constructor:

soup = BeautifulSoup(markup, "xml")

You’ll need to have lxml installed.

その他のパーサーの問題

  • If your script works on one computer but not another, it’s probably because the two computers have different parser libraries available. For example, you may have developed the script on a computer that has lxml installed, and then tried to run it on a computer that only has html5lib installed. See パーサーの違い for why this matters, and fix the problem by mentioning a specific parser library in the BeautifulSoup constructor.
  • Because HTML tags and attributes are case-insensitive, all three HTML parsers convert tag and attribute names to lowercase. That is, the markup <TAG></TAG> is converted to <tag></tag>. If you want to preserve mixed-case or uppercase tags and attributes, you’ll need to parse the document as XML.

その他

  • UnicodeEncodeError: 'charmap' codec can't encode character u'\xfoo' in position bar (or just about any other UnicodeEncodeError) - This is not a problem with Beautiful Soup. This problem shows up in two main situations. First, when you try to print a Unicode character that your console doesn’t know how to display. (See this page on the Python wiki for help.) Second, when you’re writing to a file and you pass in a Unicode character that’s not supported by your default encoding. In this case, the simplest solution is to explicitly encode the Unicode string into UTF-8 with u.encode("utf8").
  • KeyError: [attr] - Caused by accessing tag['attr'] when the tag in question doesn’t define the attr attribute. The most common errors are KeyError: 'href' and KeyError: 'class'. Use tag.get('attr') if you’re not sure attr is defined, just as you would with a Python dictionary.
  • AttributeError: 'ResultSet' object has no attribute 'foo' - This usually happens because you expected find_all() to return a single tag or string. But find_all() returns a _list_ of tags and strings–a ResultSet object. You need to iterate over the list and look at the .foo of each one. Or, if you really only want one result, you need to use find() instead of find_all().
  • AttributeError: 'NoneType' object has no attribute 'foo' - This usually happens because you called find() and then tried to access the .foo` attribute of the result. But in your case, find() didn’t find anything, so it returned None, instead of returning a tag or a string. You need to figure out why your find() call isn’t returning anything.

パフォーマンス改善

Beautiful Soup will never be as fast as the parsers it sits on top of. If response time is critical, if you’re paying for computer time by the hour, or if there’s any other reason why computer time is more valuable than programmer time, you should forget about Beautiful Soup and work directly atop lxml.

That said, there are things you can do to speed up Beautiful Soup. If you’re not using lxml as the underlying parser, my advice is to start. Beautiful Soup parses documents significantly faster using lxml than using html.parser or html5lib.

You can speed up encoding detection significantly by installing the cchardet library.

ドキュメントの一部をパース won’t save you much time parsing the document, but it can save a lot of memory, and it’ll make searching the document much faster.

Beautiful Soup 3

Beautiful Soup 3 is the previous release series, and is no longer being actively developed. It’s currently packaged with all major Linux distributions:

$ apt-get install python-beautifulsoup

It’s also published through PyPi as BeautifulSoup.:

$ easy_install BeautifulSoup

$ pip install BeautifulSoup

You can also download a tarball of Beautiful Soup 3.2.0.

If you ran easy_install beautifulsoup or easy_install BeautifulSoup, but your code doesn’t work, you installed Beautiful Soup 3 by mistake. You need to run easy_install beautifulsoup4.

The documentation for Beautiful Soup 3 is archived online. If your first language is Chinese, it might be easier for you to read the Chinese translation of the Beautiful Soup 3 documentation, then read this document to find out about the changes made in Beautiful Soup 4.

BS4への移行

多くのBS3で書かれたコードは、一か所変更するだけでBS4で動きます。パッケージ名を BeautifulSoup から BS4 に変更するだけです。これを、、:

from BeautifulSoup import BeautifulSoup

以下のようにします。:

from bs4 import BeautifulSoup
  • ImportError “No module named BeautifulSoup” が表示された場合、BS4しかインストールされていないのに、BS3のコードを実行しようとしたのが問題です。
  • ImportError “No module named bs4” が表示された場合、BS3しかインストールされていないのに、BS4のコードを実行しようとしたのが問題です。

Although BS4 is mostly backwards-compatible with BS3, most of its methods have been deprecated and given new names for PEP 8 compliance. There are numerous other renames and changes, and a few of them break backwards compatibility.

Here’s what you’ll need to know to convert your BS3 code and habits to BS4:

パーサー

Beautiful Soup 3 used Python’s SGMLParser, a module that was deprecated and removed in Python 3.0. Beautiful Soup 4 uses html.parser by default, but you can plug in lxml or html5lib and use that instead. See パーサーのインストール for a comparison.

Since html.parser is not the same parser as SGMLParser, it will treat invalid markup differently. Usually the “difference” is that html.parser crashes. In that case, you’ll need to install another parser. But sometimes html.parser just creates a different parse tree than SGMLParser would. If this happens, you may need to update your BS3 scraping code to deal with the new tree.

メソッド名

  • renderContents -> encode_contents
  • replaceWith -> replace_with
  • replaceWithChildren -> unwrap
  • findAll -> find_all
  • findAllNext -> find_all_next
  • findAllPrevious -> find_all_previous
  • findNext -> find_next
  • findNextSibling -> find_next_sibling
  • findNextSiblings -> find_next_siblings
  • findParent -> find_parent
  • findParents -> find_parents
  • findPrevious -> find_previous
  • findPreviousSibling -> find_previous_sibling
  • findPreviousSiblings -> find_previous_siblings
  • nextSibling -> next_sibling
  • previousSibling -> previous_sibling

Some arguments to the Beautiful Soup constructor were renamed for the same reasons:

  • BeautifulSoup(parseOnlyThese=...) -> BeautifulSoup(parse_only=...)
  • BeautifulSoup(fromEncoding=...) -> BeautifulSoup(from_encoding=...)

I renamed one method for compatibility with Python 3:

  • Tag.has_key() -> Tag.has_attr()

I renamed one attribute to use more accurate terminology:

  • Tag.isSelfClosing -> Tag.is_empty_element

I renamed three attributes to avoid using words that have special meaning to Python. Unlike the others, these changes are not backwards compatible. If you used these attributes in BS3, your code will break on BS4 until you change them.

  • UnicodeDammit.unicode -> UnicodeDammit.unicode_markup
  • Tag.next -> Tag.next_element
  • Tag.previous -> Tag.previous_element

ジェネレーター

I gave the generators PEP 8-compliant names, and transformed them into properties:

  • childGenerator() -> children
  • nextGenerator() -> next_elements
  • nextSiblingGenerator() -> next_siblings
  • previousGenerator() -> previous_elements
  • previousSiblingGenerator() -> previous_siblings
  • recursiveChildGenerator() -> descendants
  • parentGenerator() -> parents

So instead of this:

for parent in tag.parentGenerator():
    ...

You can write this:

for parent in tag.parents:
    ...

(But the old code will still work.)

Some of the generators used to yield None after they were done, and then stop. That was a bug. Now the generators just stop.

There are two new generators, .strings and .stripped_strings. .strings yields NavigableString objects, and .stripped_strings yields Python strings that have had whitespace stripped.

XML

There is no longer a BeautifulStoneSoup class for parsing XML. To parse XML you pass in “xml” as the second argument to the BeautifulSoup constructor. For the same reason, the BeautifulSoup constructor no longer recognizes the isHTML argument.

Beautiful Soup’s handling of empty-element XML tags has been improved. Previously when you parsed XML you had to explicitly say which tags were considered empty-element tags. The selfClosingTags argument to the constructor is no longer recognized. Instead, Beautiful Soup considers any empty tag to be an empty-element tag. If you add a child to an empty-element tag, it stops being an empty-element tag.

エンティティ

An incoming HTML or XML entity is always converted into the corresponding Unicode character. Beautiful Soup 3 had a number of overlapping ways of dealing with entities, which have been removed. The BeautifulSoup constructor no longer recognizes the smartQuotesTo or convertEntities arguments. (Unicode, Dammit still has smart_quotes_to, but its default is now to turn smart quotes into Unicode.) The constants HTML_ENTITIES, XML_ENTITIES, and XHTML_ENTITIES have been removed, since they configure a feature (transforming some but not all entities into Unicode characters) that no longer exists.

If you want to turn Unicode characters back into HTML entities on output, rather than turning them into UTF-8 characters, you need to use an output formatter.

その他

Tag.string now operates recursively. If tag A contains a single tag B and nothing else, then A.string is the same as B.string. (Previously, it was None.)

値が複数のとき like class have lists of strings as their values, not strings. This may affect the way you search by CSS class.

If you pass one of the find* methods both text and a tag-specific argument like name, Beautiful Soup will search for tags that match your tag-specific criteria and whose Tag.string matches your value for text. It will not find the strings themselves. Previously, Beautiful Soup ignored the tag-specific arguments and looked for strings.

The BeautifulSoup constructor no longer recognizes the markupMassage argument. It’s now the parser’s responsibility to handle markup correctly.

The rarely-used alternate parser classes like ICantBelieveItsBeautifulSoup and BeautifulSOAP have been removed. It’s now the parser’s decision how to handle ambiguous markup.

The prettify() method now returns a Unicode string, not a bytestring.

Table Of Contents

This Page