Beautiful Soup はHTMLやXMLファイルからデータを取得するPythonのライブラリです。あなたの好きなパーサー(構文解析器)を使って、パースツリー(構文木)の探索、検索、修正を行います。 これはプログラマーの作業時間を大幅に短縮してくれます。
この文書は Beautiful Soup 4.2.0 Documentation の日本語訳です。”Beautiful Soup”を”ビューティフルソープ”と読んでしまう英語が苦手でちょっぴりHな後輩のために翻訳しました。
2013年10月29日からこの文書の翻訳をはじめました。11月1日現在まだ全てを訳し終えていませんが、スクレイピングに使う主な部分はとりあえず訳したので、一旦これで公開して、あとは年内を目処にまったりと翻訳をすすめ、あわせて質を高めていこうと思っています。今のところ、パースツリーを修正 以降は、ざっくり訳のためにおかしな表現が多々あることにご注意ください。
誤訳やわかりづらいところを見つけたり、なにかご意見があるときには、近藤茂徳()までご連絡ください。こういった翻訳をするのははじめてなので、つっこみ大歓迎です。よろしくお願いします。
2013年10月現在、Beautiful Soupについての日本語Webページは、Beautiful Soup 3とBeautiful Soup 4(以下、BS3,BS4)の情報が混在しています。とくに、”Beautiful Soup”で日本語ページを対象にググると、最初に表示される10件中9件がBS3による情報であるために、初心者はそのままBS3を使って混乱しがちです。ご注意ください。
混乱しないように初心者が知っておくべきこと
この文書は、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 オブジェクトが得られます。これは入れ子データ構造でドキュメントを表現します。:
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_install か pip でインストールできます。
$ easy_install beautifulsoup4
$ pip install beautifulsoup4
( BeautifulSoup パッケージはおそらくあなたが探しているものでは ありません 。これは、一つ前のメジャーリリース Beautiful Soup 3 です。多くのソフトウェアがBS3を使っていて、今でもBS3は利用できます。しかし、新しくコードを書く場合は、 beautifulsoup4 をインストールすべきです。)
もし、 easy_install や pip をインストールしてないときは、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") |
|
|
lxml’s HTML parser | BeautifulSoup(markup, "lxml") |
|
|
lxml’s XML parser | BeautifulSoup(markup, ["lxml", "xml"]) BeautifulSoup(markup, "xml") |
|
|
html5lib | BeautifulSoup(markup, "html5lib") |
|
|
できれば、速度のために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é bleu!")
<html><head></head><body>Sacré bleu!</body></html>
Beautiful Soupは、ドキュメントをもっとも適したパーサー(構文解析器)を使ってパースします。 XMLパーサーを使うように指定しなければ、HTMLパーサーが用いられます。( XMLのパース を参照)
Beautiful Soup は複雑なHTMLドキュメントを、Pythonオブジェクトの複雑なツリー構造に変換します。 しかし、あなたは Tag, NavigableString, BeautifulSoup, Comment の 4種類のオブジェクト だけを扱えばよいです。
Tag オブジェクトは、元のドキュメント内のXMLやHTMLのタグに対応しています。:
soup = BeautifulSoup('<b class="boldest">Extremely bold</b>')
tag = soup.b
type(tag)
# <class 'bs4.element.Tag'>
Tag オブジェクトは、多くの属性とメソッドを持っています。それらのほとんどは、 パースツリーを探索 と パースツリーを検索 で説明します。この節では Tag オブジェクトの重要な機能である、名前と属性について説明します。
タグはそれぞれ名前を持っていますが、 .name でアクセスできます。:
tag.name
# u'b'
タグの名前を変えると、その変更はBeautiful Soupが生成する全てのマークアップに反映されます。:
tag.name = "blockquote"
tag
# <blockquote class="boldest">Extremely bold</blockquote>
タグは多くの属性を持ちます。 <b class=”boldest”>は、”boldest”という値の’class’属性を持ちます。 Tag オブジェクトを辞書のように扱うことで、そのタグの属性にアクセスできます。:
tag['class']
# u'boldest'
.attrs で辞書に直接アクセスできます。:
tag.attrs
# {u'class': u'boldest'}
繰り返しになりますが、辞書のように Tag オブジェクトを扱うことにより、タグの属性に対して追加, 削除, 修正も行うことができます。:
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, 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'
タグを文字列に変換したときは、これらの属性の複数の値は一つにまとめられます。:
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'
Beautiful Soup オブジェクトは、それ自身で元のドキュメント全体を表しています。 たいていの場合、Tag obj. を扱うことで、用は足りるでしょう。 これは、Tag obj. が パースツリーを探索 と パースツリーを検索. で述べられているメソッドの多くをサポートしているということです。
BeautifulSoup オブジェクトは、実際のHTMLやXMLタグに対応していないので、名前や属性を持たない。 しかし、 .name をみるような便利なものはいくつかある。そして、それらは特別な .name “[document]”を得られる(?訳がおかしい。けど次回まわし?):
soup.name
# u'[document]'
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)
ドキュメントのある部分から他の部分へどのように移動するかを示すために、このドキュメントを例に使っていきます。
タグはその間に(ドキュメント本文のテキスト)文字列や他のタグを挟んでいます。これらの要素は、 タグの 子要素 です。Beautiful Soupは、タグの子要素を探索し扱うための多くの属性を提供します。
Beautiful Soupの文字列は、これらの属性をサポートしません。なぜなら、文字列は子要素をもたないからです。
パースツリーを探索する一番簡単な方法は、あなたが取得したいタグの名前を使うことです。 もし、<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 で呼び出すと、リストで取得できます。:
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'
文字列は .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
.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
ある 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 ジェネレーターを使用します。:
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 属性で親要素にアクセスできます。 たとえば、”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 で取得することができます。 以下は、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 を用いて、パースツリーの同じレベルの要素間を辿ることができます。:
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 をイテレーターとして使って、まとめて扱えます。:
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はこのイベントの連なりを、さらに再構成して扱います。
文字列や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>
パースされたドキュメントの要素を、前後方向に取得していくイテレーターを使うこともできます。:
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”ではじまるつづりの名前のタグを見つけます。 “three sisters”ドキュメントでは、<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値 は全ての要素にマッチします。 以下のコードは、ドキュメント内の 全て のタグをみつけます。 ただし、ドキュメント本文のテキスト文字列はマッチされません。:
for tag in soup.find_all(True):
print(tag.name)
# html
# head
# title
# body
# p
# b
# p
# a
# a
# a
# p
以上のフィルターで機能が足りないときは、自分で引数に要素をとる関数を定義することもできます。 その関数は、引数がマッチしたときは True を、そうでないときは False を返します。
以下の関数では、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(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'
これらの使い方は、すでに説明してるものもあれば、初出のものもあります。 text や id に値を渡すのはどういう意味でしょうか? なぜ、find_all("p", "title") は、CSSの”title”タグをもつ<p>タグを発見したのでしょうか? find_all() の引数をみていきましょう。
find_all() の name 引数に値を渡すと、タグの名前だけを対象に検索が行われます。 名前がマッチしないタグと同じように、テキスト文字列は無視されます。
以下の例は、もっともシンプルな使い方です。:
soup.find_all("title")
# [<title>The Dormouse's story</title>]
フィルターの種類 で述べたように、 name 引数は文字列, 正規表現, リスト, 関数, True値をとることができます。
どのような理解できない引数でも、タグの属性の一つとして解釈されます。 キーワード引数 id に値を渡すと、Beautiful Soupはタグの’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 属性に値が入っている全てのタグを見つけます。このとき、値は何でもあっても構いません。:
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
しかし、これらの属性を辞書にして、キーワード引数 attrs として値を渡せばフィルタリングすることができます。:
data_soup.find_all(attrs={"data-foo": "value"})
# [<div data-foo="value">foo!</div>]
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 引数で、タグに挟まれている文字列を対象に検索することができます。 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>]
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>]
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() はBeautiful Soupの検索APIの中で、一番使われるものなので、ショートカットがあります。 Beautiful Soup オブジェクトや Tag オブジェクトを関数のように扱って、 find_all() メソッドを呼び出すことができます。 以下の2行は等価です。:
soup.find_all("a")
soup("a")
以下の2行もまた等価です。:
soup.title.find_all(text=True)
soup.title(text=True)
使い方: find(name, attrs, recursive, text, **kwargs)
find_all() メソッドは、検索結果を得るためにHTMLドキュメント全部をスキャンします。 しかし、1つだけの検索結果が必要なときがあります。 もし、HTMLドキュメントに<body>タグが1つだけなら、HTMLドキュメント全体をスキャンするのは時間の無駄です。 その場合は find_all() メソッドに limit=1 という引数を渡さずに、 find() メソッドを使うことができます。 以下の2行は、ほぼ等価です。:
soup.find_all('title', limit=1)
# [<title>The Dormouse's story</title>]
soup.find('title')
# <title>The Dormouse's story</title>
ただ1つ違う点は、find_all() は要素1のリストを返し、find() は要素をそのまま返すことです。
find_all() が何もみつけられないときは空リストを返します。 find() が何もみつけられないときは、 None を返します。:
print(soup.find("nosuchtag"))
# None
タグ名で探索 で出てきた soup.head.title で探索する方法を覚えていますか? おkれは、find() についても適用できます。:
soup.head.title
# <title>The Dormouse's story</title>
soup.find("head").find("title")
# <title>The Dormouse's story</title>
使い方: find_parents(name, attrs, text, limit, **kwargs)
使い方: find_parent(name, attrs, text, **kwargs)
ここまで find_all() と find() について述べてきました。 Beautiful Soup APIにはパースツリーを検索するためのメソッドが、あと10あります。 しかし、おそれる必要はありません。 そのうち5つは、find_all() と基本的に同じです。 そして、のこりの5つは find() と基本的に同じです。 違いは、ツリーのどの部分を検索対象にするのかという点のみです。
最初に、 find_parents() と find_parent() を見てみましょう。 find_all() と find() がタグの子孫を見て、ツリーを下りていったことを思い出してください。 find_parents() と find_parent() は逆です。 これらはタグや文字列の親をみて、ツリーを’上に’検索していきます。 以下の”three daughters”ドキュメントの例で、深いレベルにある文字列から検索していく様子を見てください。:
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")
# []
3つの<a>タグのうちの1つは、検索の起点になる文字列の直接の親要素なので、それが返されました。 3つの<p>タグのうちの1つは、起点の文字列の直接の親ではありませんが、やはりそれも返されました。 CSSクラス”title”をもつ<p>タグは、”three daughers”ドキュメント中にはあるのですが、起点の文字列の親要素ではないので、 find_parents() では見つけることができませんでした。
find_parent() と find_parents() のつながりはわかったでしょうか。 .parent と .parents 属性については、以前に述べてあります。 そのつながりはとても強いです。 これらの検索メソッドは実際には .parents で、全ての親要素の連なりをイテレートして扱います。 そして、要素それぞれについてフィルターにマッチするかどうかをチェックします。
使い方: find_next_siblings(name, attrs, text, limit, **kwargs)
使い方: find_next_sibling(name, attrs, text, **kwargs)
これらのメソッドは、後方にある兄弟要素を扱うのに、 .next_siblings を使います。 find_next_siblings() メソッドはマッチする兄弟要素を全て返し、 find_next_sibling() は最初の一つを返します。:
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(name, attrs, text, limit, **kwargs)
使い方: find_previous_sibling(name, attrs, text, **kwargs)
これらのメソッドは、HTMLドキュメントの前方にあった兄弟要素を扱うのに .previous_siblings を使います。 find_previous_siblings() メソッドはマッチする兄弟要素を全て返し、 find_previous_sibling() は最初の一つを返します。:
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(name, attrs, text, limit, **kwargs)
使い方: find_next(name, attrs, text, **kwargs)
これらのメソッドは、HTMLドキュメントのその後にあらわれるタグと文字列の要素全てイテレートして扱うために、 .next_elements メソッドを使います。 find_all_next() メソッドはマッチするもの全てを返し、 find_next() は最初にマッチしたものを返します。(!要改善):
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>
最初の例では、起点となった<a>タグに挟まれている、文字列”Elsie”が返されています。 2番めの例では、起点となった<a>タグと同じパートじゃないにも関わらず、最後の<p>タグが示されています。 これらのメソッドでは、問題はフィルターにマッチするか否かと、スタートした要素よりも後にでてきたかということが問われます。(!要改善)
使い方: find_all_previous(name, attrs, text, limit, **kwargs)
使い方: find_previous(name, attrs, text, **kwargs)
これらのメソッドは、ドキュメントの起点のタグの前にあらわれるタグと文字列の要素全てをイテレートして扱うために、 .previous_elements メソッドを使います。(!要改善):
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>
find_all_previous("p") は”three sisters”ドキュメントの最初の段落を見つけます。(class=”title”のやつです) しかし、第2段落でも見つけます。<p>タグは内に起点にした<a>タグを含んでいます。 驚きすぎないでください。 我々は、起点のタグより前方に現れた全てのタグを見ているのです。<a>タグを挟んでいる<p>タグは、<a>タグよりも前に示されねばなりません。(!要改善)
Beautiful Soupは、よく使われるCSSセレクタをほとんどサポートしています。 Tag オブジェクトや BeautifulSoup オブジェクトに .select() メソッドで文字列を渡すだけで使えます。
タグを見つけるには次のようにします。:
soup.select("title")
# [<title>The Dormouse's story</title>]
soup.select("p nth-of-type(3)")
# [<p class="story">...</p>]
あるタグより後ろの指定されたタグを見つけます。:
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>]
あるタグの直後の指定されたタグを見つけます。:
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")
# []
タグの兄弟要素を見つけます。:
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>]
CSSクラスによってタグを見つけます。:
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>]
CSSの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>]
指定の属性の有無でタグを見つけます。:
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>]
属性が持つ値によってタグを見つけます。:
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>]
languageコードで、マッチさせます。:
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>]
このやり方は、CSSセレクタの文法を知っているユーザにとっては、とても便利です。 これでBeautiful Soup APIの全てのキモを使えるようになりました。 もしCSSセレクタを使いこなしたいなら、lxmlを使ってみるのもよいでしょう。 lxmlは処理がとても速く、さらに多くのCSSセレクタをサポートしています。 しかし、ここではBeautiful Soup APIを使って、シンプルなCSSセレクタの組み合わせるによる方法を説明しました。
Beautiful Soupの主な強みは、パースツリーの検索するところにあります。 しかしまた、Beautiful Soupは、ツリーを修正したり、変更したツリーを新しいHTMLやXMLのドキュメントに出力することもできます。
属性 の節でも述べましたが、タグの名前変更、属性値の変更、追加、削除ができます。:
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>
Tag オブジェクトの .string を変更すると、そのタグが挟む文字列がその値に変更されます。:
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>
注意点: 変更したタグが他のタグを挟んでいると、それらのタグ全てが破壊されます。
Tag.append() により、タグが挟んでいる文字列に追加をすることができます。 まるでPythonのリストの .append() のように作用します。:
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']
ドキュメントに文字列を加えたいときは、Pythonの文字列を append() に渡してください。 もしくは、 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']
新しいコメントや 他の NavigableString のサブクラスを生成したいときは、 new_string() の第2引数にそのクラスを渡してください。:
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.']
(これはBeautiful Soup 4.2.1 の新機能です)
完全に新しいタグを生成したいときは、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>
第1引数のタグ名だけは必須です。
Tag.insert() は Tag.append() に似ています。 違うのは、タグの .contents の最後以外にも、要素を挿入できるという点です。:
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() メソッドは、あるタグの直前に、別のタグや文字列を挿入します。:
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>
insert_after() メソッドは、あるタグの直後に、別のタグや文字列を挿入します。:
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']
Tag.clear() は、タグが挟んでいるcontentsを削除します。(訳注:要チェック?):
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>
PageElement.extract() はツリーからタグや文字列を除去します。 返値は、その抽出されたタグや文字列です。:
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
このとき、2つのパースツリーがあります。1つは BeautifulSoup オブジェクトを根ノードとしたあなたがパースしたドキュメントです。もう1つは、抽出したタグを根ノードとするものです。抽出した要素の子要素を extract でコールできます。:
my_string = i_tag.string.extract()
my_string
# u'example.com'
print(my_string.parent)
# None
i_tag
# <i></i>
Tag.decompose() はパースツリーからタグを除去します。 そのタグと挟んでいる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>
PageElement.replace_with() はツリーからタグと文字列を除去し、 引数に与えたタグや文字をその代わりに置き換えます。:
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() は置き換えられたタグや文字列を返します。 それを、調査したり、ツリーの他の部分に加えることができます。
PageElement.wrap() は、その要素を引数で指定したタグを挟みます。 新しく挟まれたものを返します。
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>
このメソッドは、Beautiful Soup 4.0.5 からの新機能です。
Tag.unwrap() は wrap() の反対です。 それは、タグの中身がなんであれ、それとタグを置き換えます。 マークアップをはずすのに便利です。(訳注:やりなおし??):
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>
replace_with() のように、 unwrap() は置き換えられたタグを返します。
prettify() メソッドは、BeautifulSoupパースツリーを、1行に1タグのきれいにフォーマットされたUnicode文字列に変換します。:
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>
prettify() メソッドは、 トップレベルの BeautifulSoup オブジェクトでも、それ以外の Tag オブジェクトでも呼び出すことができます。:
print(soup.a.prettify())
# <a href="http://example.com/">
# I linked to
# <i>
# example.com
# </i>
# </a>
フォーマットされたテキストではなく単なる文字列がほしければ、 BeautifulSoup や Tag オブジェクトの unicode() や str() を呼び出せます。:
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>'
str() 関数は、UTF-8にエンコードされた文字列を返します。 他のオプションを知りたければ、 エンコード をみてください。
バイト文字列を得るのに、 encode() を用いることもできます。 decode() を用いると、Unicodeを得ることができます。
Beautiful Soupに、”&lquot;”のようなHTMLエンティティを含んだドキュメントを渡すと、それらはUnicodeキャラクタに変換されます。:
soup = BeautifulSoup("“Dammit!” he said.")
unicode(soup)
# u'<html><head></head><body>\u201cDammit!\u201d he said.</body></html>'
そのドキュメントを文字列に変換すると、Unicode文字列はUTF-8キャラクタとしてエンコードされます。 それを、HTMLエンティティに戻すことはできません。:
str(soup)
# '<html><head></head><body>\xe2\x80\x9cDammit!\xe2\x80\x9d he said.</body></html>'
デフォルトでは、出力するときエスケープされるのは、裸の&と角かっこのみです。 これらは、”&”,”<”,”>”に変換されます。 そのためBeautifulSoupはうっかり不正確なHTMLやXMLを生成することはありません。:
soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>")
soup.p
# <p>The law firm of Dewey, Cheatem, & 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&bar=val2">A link</a>
prettify(), encode(), decode() の formatter 属性に値を与えると、出力を変更することができます。 formatter は、4種類の値をとり得ます。
デフォルトでは、 formatter="minimal" です。 文字列は、Beautiful Soupが正しいHTML/XMLを生成することを十分に保証するように、加工されるだけです。:
french = "<p>Il a dit <<Sacré bleu!>></p>"
soup = BeautifulSoup(french)
print(soup.prettify(formatter="minimal"))
# <html>
# <body>
# <p>
# Il a dit <<Sacrテゥ bleu!>>
# </p>
# </body>
# </html>
もし、 formatter="html" を渡せば、BSは 可能なときはいつでも、Unicode文字列を HTMLエンティティに変換します。:
print(soup.prettify(formatter="html"))
# <html>
# <body>
# <p>
# Il a dit <<Sacré bleu!>>
# </p>
# </body>
# </html>
もし、 formatter=None を渡せば、BSは出力においてまったく文字列を修正しません。 これは、最速のオプションですが、BSが正しくないHTML/XMLを生成することになります。 次の例をご覧ください。:
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>
formatter に関数を渡すと、ドキュメントの文字列や属性値のたびに、BSはその関数をコールします。 関数内で望むことはなんであれできます。 以下では、formatterは文字列を大文字にコンバートし、他には何もしません。:
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>
もしあなたがあなたの関数を書いたなら、あなたは bs4.dammit の EntitySubstitution クラスについて知るべきです。 このクラスは、BSの標準的なformatter をクラスメソッドとして内包します。 “html” formatterは EntitySubstitution.substitute_html , “minimal” formatterは EntitySubstitution.substitute_xml です。 あなたは、これらの関数を、 formatter==html や formatter==minimal をシュミレーションします。 しかし、それに加えて他のこともします。
これは例です。UnicodeキャラクタをHTMLエンティティに置換します。可能なときはいつでも。 しかし、 また 全ての文字列を大文字に変換します。:
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 <<SACRÉ BLEU!>>
# </p>
# </body>
# </html>
最後に一点(最終通告?): もし CData オブジェクトを生成したときは、そのオブジェクト内のテキストは 正確にあるがまま、フォーマットされることなく いつも表されます。 BSは formatterメソッドを呼出します。あなたがカスタムメソッドを書いた場合にのみ。どういうカスタムメソッド化というと、全てのドキュメント内の文字列やなにかをcountする。しかし、それは返り値を無視します。:
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() メソッドを使います。 それは、全ドキュメントや下層のタグを、ユニコードの単一文字列として返します。:
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'
テキストをまとめる際の区切り文字を指定することができます。:
# soup.get_text("|")
u'\nI linked to |example.com|\n'
各文字列パーツの最初と最後の空白を除去することもできます。:
# soup.get_text("|", strip=True)
u'I linked to|example.com'
空白を除去するのに、 stripped_strings ジェネレーターを使って処理することもできます。:
[text for text in soup.stripped_strings]
# [u'I linked to', u'example.com']
もし貴方がいくつかのhtmlをパースしたいなら、あなたは、 Beautiful Soup コンストラクタに、マークアップをダンプできる。 それはたぶんうまくいきます。 Beautiful Soupはパーサーを選んで、データをパースします。 しかし、どのパーサーが使われるか変更するために、コンストラクタに渡すいくつかの引数があります
1つ目の BeautifulSoup コンストラクタの引数は、 あなたがパースしたいマークアップの、文字列または開いているファイルハンドルです。 2つ目の引数は、どのように マークアップをぱーすするかについてです。
もし何も指定しなかった場合は、インストールされているなかで最高のHTMLパーサーを使います。 Beautiful Soupは、lxmlのパーサーを最高のものとしています。そして、html5libとPythonの組み込みパーサー。 あなたは次のうちの一つを指定することで、これを上書きできます。
この パーサーのインストール の章は、サポートしているパーサーを比較します。
もし適切なパーサーをインストールしていないときは、Beautiful Soupはあなたのリクエストを無視し、違うパーサーを選びます。 現在、ただ一つサポートされているXMLパーサーは、lxmlです。 もし、lxmlをインストールしてないとき、XMLの要求はあなたに何も与えませんし、”lxml”へのリクエストも動きません。(要改善!)
Beautiful Soupは多くの異なるパーサーに同じインターフェースを提供しています。 しかし、パーサーはそれぞれは異なります。 パーサーが異なると、同じドキュメントでも、生成されるパースツリーは異なってきます。 HTMLパーサーとXMLパーサーには大きな違いがあります。 以下は、短いドキュメントをHTMLとしてパースしたものです。:
BeautifulSoup("<a><b /></a>")
# <html><head></head><body><a><b></b></a></body></html>
空の<b />タグは、正式なHTMLではないため、パーサーはそれを<b></b>のタグの組に変換します。
以下は、同じドキュメントをXMLとしてパースしたものです。 (これを実行するにはlxmlをインストールしておく必要があります) <b />タグはそのまま残っており、ドキュメントはXML宣言が<html>タグの代わりに加えられたことに気づいてください。:
BeautifulSoup("<a><b /></a>", "xml")
# <?xml version="1.0" encoding="utf-8"?>
# <a><b/></a>
HTMLパーサー同士でも、違いはあります。 完全な形のHTMLドキュメントをBeautiful Soupに与えたときは、その違いは問題になりません。 あるパーサーは、他のパーサーよりも速いでしょう。 しかし、それらは全て元のHTMLドキュメントを正確に反映したデータ構造を与えるでしょう。
しかし、不完全な形のHTMLドキュメントのときは、異なるパーサーは異なる結果を出力します。 以下は、lxmlのHTMLパーサーによってパースされた短く不正なドキュメントです。 ぶらさがっている</p>タグは、単に無視されていることに気づいてください。:
BeautifulSoup("<a></p>", "lxml")
# <html><body><a></a></body></html>
以下は、html5libによってパースされた同じドキュメントです。:
BeautifulSoup("<a></p>", "html5lib")
# <html><head></head><body><a><p></p></a></body></html>
ぶらさがっている</p>タグを無視する代わりに、html5libは、それを開始の<p>タグと組にしました。 このパーサーはまた、ドキュメントに空の<head>タグも加えました。
以下は、Python組み込みのHTMLパーサーで同じドキュメントをパースしたものです。:
BeautifulSoup("<a></p>", "html.parser")
# <a></a>
html5libのように、このパーサーは終わりの</p>タグを無視します。 html5libとは違い、このパーサーは<body>タグを加えて正しい書式のHTMLドキュメントを作成しようとはしません。 lxmlとは違い、なんとかして<html>タグを加えようとはしません。
“<a></p>”というドキュメントは不正なので、これについての”正しい”処理方法はありません。 html5libパーサーはhtml5標準のいち部分のテクニックを使います。 それは、ただしい主張を正しい方法についてします。しかし、これらの3つの方法全て、道理に合っています。(?あとで再チェック)
パーサー間の違いは、あなたのスクリプトにも影響するでしょう。 もし、スクリプトを他の人に配布したり、複数の計算機で実行しようとするならば、 Beautiful Soup コンストラクタについてパーサーを指定するべきです。 そうすることによって、あなたがパースした方法と違うやりかたでドキュメントをパースする可能性を減らすでしょう。
HTMLやXMLドキュメントは全て、ASCIIやUTF-8のような特定の文字コードで書かれています。 しかし、BeautifulSoupにドキュメントをロードすると、それらはUnicode型に変換されます。:
markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup)
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# u'Sacr\xe9 bleu!'
これは魔法ではありません。Beautiful Soupは Unicode, Dammit を内部でライブラリとして呼び出し、文字コードを判別してUnicodeに変換するのに使っています。 自動判別された文字コードは、 BeautifulSoup オブジェクトの .original_encoding 属性で参照することができます。:
soup.original_encoding
'utf-8'
Unicode, Dammit はほとんどの場合正しく判別しますが、たまに失敗します。 たいてい適切に判別しますが、バイト毎の検索の場合は、とてもながい時間がかかります。 もし、ドキュメントの文字コードが分かっているのなら、失敗や遅延を避けるために、 BeautifulSoup コンストラクタに from_encoding として渡すとよいです。
次の例は、ISO-8859-8(訳注:ラテン文字等の文字コード)で書かれたドキュメントです。 このドキュメントは短いために、Unicode, Dammitはそれが何か判別できず、それをISO-8859-7(訳注:ギリシア文字等の文字コード)と誤認します。:
markup = b"<h1>\xed\xe5\xec\xf9</h1>"
soup = BeautifulSoup(markup)
soup.h1
<h1>νεμω</h1>
soup.original_encoding
'ISO-8859-7'
正しい from_encoding を渡すことで、これを正すことができます。:
soup = BeautifulSoup(markup, from_encoding="iso-8859-8")
soup.h1
<h1>ם ו ל ש</h1>
soup.original_encoding
'iso8859-8'
(通常、UTF-8のドキュメントは複数の文字コードを含むことができますが、) ごくまれに、変換できない文字をユニコードの特殊文字”REPLACEMENT CHARACTER” (U+FFFD,�) に置き換えることがあります。 Unicode, Dammit がこれを行うときは、同時に、 UnicodeDammit か BeautifulSoup オブジェクトの .contains_replacement_characters 属性にTrueをセットします。 これにより、変換後のユニコードの文字列は、元の文字コードの文字列を正確に表現しておらず、いくつかのデータが損なわれているということがわかります。 もし、 .contains_replacement_characters が False のときは、ドキュメント内に特殊文字�があっても、それは(この段落の�のように)もともとあり、データは損なわれていないということです。
Beautiful Soupでドキュメントを出力すると、元のドキュメントがUTF-8でなくても、UTF-8で出力されます。 次の例は、Latin-1で書かれたドキュメントについてです。:
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>
<meta>タグは書き換えられ、ドキュメントが現在UTF-8であることを示しています。:
.. If you don't want UTF-8, you can pass an encoding into ``prettify()``::
UTF-8以外で出力したいときは、 prettify() にその文字コードを渡してください。:
print(soup.prettify("latin-1"))
# <html>
# <head>
# <meta content="text/html; charset=latin-1" http-equiv="Content-type" />
# ...
Pythonのstr型であるかのように、BeautifulSoup オブジェクトや、その要素のencode()をコールすることができます。:
soup.p.encode("latin-1")
# '<p>Sacr\xe9 bleu!</p>'
soup.p.encode("utf-8")
# '<p>Sacr\xc3\xa9 bleu!</p>'
あなたが選んだ文字コードでは表せない文字は、XMLエンティティリファレンスの数字に変換されます。 次の例は、スノーマンのユニコード文字を含んだドキュメントです。:
markup = u"<b>\N{SNOWMAN}</b>"
snowman_soup = BeautifulSoup(markup)
tag = snowman_soup.b
スノーマンの文字はUTF-8のドキュメントに組み込めます。(それは☃と表示されます。しかし、ISO-Latin-1やASCIIにはその文字がありません。そこで、これらの文字コードでは”☃”に変換されます。):
print(tag.encode("utf-8"))
# <b>☃</b>
print tag.encode("latin-1")
# <b>☃</b>
print tag.encode("ascii")
# <b>☃</b>
Beautiful Soup 抜きで、Unicode, Dammitを使えます。 文字コードがわからないデータを持つときや、Unicodeにそのデータを変換したいときは、それは便利です。:
from bs4 import UnicodeDammit
dammit = UnicodeDammit("Sacr\xc3\xa9 bleu!")
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'utf-8'
Pythonライブラリ chardet か cchardet をインストールしていれば、Unicode, Dammitはさらに正確に文字コードを推測できます。:
dammit = UnicodeDammit("Sacr\xe9 bleu!", ["latin-1", "iso-8859-1"])
print(dammit.unicode_markup)
# Sacré bleu!
dammit.original_encoding
# 'latin-1'
Unicode, Dammitには、Beautiful Soupが使わない2つの機能があります。
(訳注: スマート引用符とは、引用符’で左右の向き(open/close)が区別されているもののことです。 ASCIIコードやシフトJISの引用符は区別されていません。 [ 参考リンク ])
Unicode, Dammitは Microsoftスマート引用符を、HTMLやXMLのエンティティに変換します。:
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 “love” Microsoft Word’s smart quotes</p>'
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="xml").unicode_markup
# u'<p>I just “love” Microsoft Word’s smart quotes</p>'
Microsoftスマート引用符をASCII引用符に変換できます。:
UnicodeDammit(markup, ["windows-1252"], smart_quotes_to="ascii").unicode_markup
# u'<p>I just "love" Microsoft Word\'s smart quotes</p>'
できればこの機能を便利に使ってほしいですが、Beautiful Soupはそれを使いません。 Beautiful Soupは、他の文字と同じように、Microsoftスマート引用符をUnicodeキャラクタに変換するという、デフォルトの振るまいを選びます。:
UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# u'<p>I just \u201clove\u201d Microsoft Word\u2019s smart quotes</p>'
ときどき、ほぼUTF-8で書かれているが、一部Microsoftスマート引用符のような文字コードがWindows-1252の文字を含むドキュメントがあります。:
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")
このドキュメントは扱いに困ります。 スノーマンはUTF-8ですが、スマート引用符はWindows-1252です。 スノーマンか引用符のどちらかしか表示できません。:
print(doc)
# ☃☃☃�I like snowmen!�
print(doc.decode("windows-1252"))
# ☃☃☃“I like snowmen!”
ドキュメントをUTF-8としてデコードすると、 UnicodeDecodeError が発生し、Windows-1252でデコードすると意味不明(gibberish?)なことになります。 幸いなことに、 UnicodeDammit.detwingle() はその文字をpure UTF-8に変換し、それをUnicodeにデコードし、スノーマンと引用符を並べて表示することを許可します。:
new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”
UnicodeDammit.detwingle() UTF-8に埋め込まれたWindows-1252の文字を扱う方法(とその逆)のみを知っています。しかしこれは、よくあるケースではありません。
データを BeautifulSoup や UnicodeDammit コンストラクタに渡す前に、 UnicodeDammit.detwingle() をコールしなければならないことに注意してください。 Beautiful Soupは 何らかの単一の文字コードでドキュメントが記されていると想定しています。 もし、UTF-8とWindows-1252の両方を含むドキュメントを渡したら、ドキュメント全体がWindows-1252と判断しがちです。そして、そしてそのドキュメントの出力は、 ` テ「ヒ愴津「ヒ愴津「ヒ愴停廬 like snowmen!窶拜` のようになります。
UnicodeDammit.detwingle() はBeautiful Soup 4.1.0からの機能です。
あるドキュメントの<a>タグに対してBeautiful Soupを使いたい場合、ドキュメント全部をパースして、その中から<a>タグを探すのは、時間とメモリの無駄です。 最初にでてくる<a>タグ以外を全て無視すれば、処理は速くなります。 SoupStrainer クラスは、与えられたドキュメントのどの部分をパースするかを選ぶことができます。 そのためには、 SoupStrainer を作成し、それを BeautifulSoup コンストラクタに parse_only 属性として渡すだけです。
(この機能はhtml5libパーサーを使っているときは、使えないことにご注意ください。 もしhtml5libを使うときはどんなときでも、ドキュメント全体がパースされます。 これは、html5libがパースツリーをそのように継続的に再構築するためです。 もし、ドキュメントの一部がパースツリーに組み込まれてない場合は、それは裏ッシュします。 それをさけるためには、例において、Beautiful SoupがPythonの組み込みパーサーを利用させてください)
SoupStrainer (スープ漉し器)クラスは、 パースツリーを検索: するときの典型的なメソッドである name, attrs, text, and **kwargs をもちます。 以下は、 SoupStrainer オブジェクトの3通りの例です。:
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)
ここで、”three sisters”ドキュメントをもう一回とりあげます。 ドキュメントを SoupStrainer オブジェクトで3通りにパースするので、どうなるかを見てみましょう。:
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
# ...
#
SoupStrainer パースツリーを検索 のメソッドに渡すことができます。 これは、とても便利です。少しだけ説明します。:
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']
もし、Beautiful Soupがドキュメントに何かをしてトラブルになっているときは、どのドキュメントを diagnose() 関数に渡してみてください。(これはBeautiful Soup 4.2.0の新機能です) Beautiful Soupは、どのようにパーサーがそのドキュメントを扱ったかというレポートを出力し、BeautifulSoupが使っているパーサーが失っているかどうかを教えてくれます。:
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:
# ...
diagnose()の出力をみてみると、どのように問題を解決すればよいかわかるでしょう。もし、わからなくても、助けをもとめるときに、 diagnose() の出力を貼り付けることができます。
パースエラーには2種類あります。 1つは、クラッシュです。Beautifuls Soupにドキュメントを読み込ませたときに、例外が発生します。たいていそれは HTMLParser.HTMPParserError です。 もう1つは、想定外の動作です。Beautiful Soupのパースツリーが、元のドキュメントのパースツリーとかなり違うことがあります。
これらのエラーは、たいていBeautiful Soupが原因ではありません。そのように言えるのは、Beautiful Soupがよくできたソフトウェアだからではなく、Beautiful Soupがパース処理のコードを含んでいないためです。 代わりに、Beautiful Soupは外部のパーサーに頼っています。もしあるパーサーが正しいドキュメントをパースできないときは、他のパーサーを試してみるというのが一番良い対処です。 パーサーのインストール に、これについての詳細とパーサーの比較が載っています。
一番よくみるパースエラーは、 HTMLParser.HTMLParseError: malformed start tag と HTMLParser.HTMLPraseError: bad end tag でしょう。 これらはともに、Python組み込みのHTMLパーサーライブラリが返します。 この場合は、 lxml か html5lib をインストール するとよいです。
想定外の動作のエラーで最も多いのは、あると思っていたタグを見つけられないときです。 見たことあると思いますが、そのとき find_all() は [] を返し、 find() は None を返します。 これも、Python組み込みHTMLパーサーにとっては、よくある問題です。やはり、一番よい対処は、 lxml か html5lib をインストール することです。
デフォルトでは、Beautiful SoupはドキュメントをHTMLとしてパースします。XMLとしてパースするには、 BeautifulSoup コンストラクタの第二引数に、 “xml” を渡します。:
soup = BeautifulSoup(markup, "xml")
このためには、 lxml をインストール している必要があります。
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は一つ前のリリースで、すでに開発は停止しています。 現在でも、全ての主なLinuxディストリビューションに含まれています。:
$ apt-get install python-beautifulsoup
Pypiでも BeautifulSoup として利用できます。
$ easy_install BeautifulSoup
$ pip install BeautifulSoup
次のリンクからダウンロードできます。tarball of Beautiful Soup 3.2.0.
easy_install beautifulsoup , easy_install BeautifulSoup というコマンドでBeautiful Soupをインストールすると、あなたのコードは動きません。 easy_install beautifulsoup4 と入力しましょう。
Beautiful Soup 3 のドキュメントはアーカイブされています。
日本語版は次のリンクから参照できます。 Beautiful Soup ドキュメント Beautiful Soup 4での変更点が理解するために、これらのドキュメントを読んでみてください。
多くのBS3で書かれたコードは、一か所変更するだけでBS4で動きます。パッケージ名を BeautifulSoup から bs4 に変更するだけです。これを、、:
from BeautifulSoup import BeautifulSoup
以下のようにします。:
from bs4 import BeautifulSoup
BS4はBS3の大部分について後方互換性がありますが、それらのメソッドのほとんどは変更され`PEP 8 規約 <http://www.python.org/dev/peps/pep-0008/>`_ に沿った新しい名前になっています。多くの名前等の変更により、後方互換性の一部が損なわれています。
以下は、BS3のコードを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.
Some arguments to the Beautiful Soup constructor were renamed for the same reasons:
I renamed one method for compatibility with Python 3:
I renamed one attribute to use more accurate terminology:
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.
I gave the generators PEP 8-compliant names, and transformed them into properties:
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.
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.