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.12.0 Documentation の日本語訳です。

以前、”Beautiful Soup”を”ビューティフルソープ”と読んでしまう英語が苦手でちょっぴりHな後輩のために Beautiful Soup 4.2.0 Documentation を翻訳しました。それから10年が経ち、内容が古くなったので、2024年8月時点で最新のドキュメントをあらためて訳しました。

原版と比べてレイアウトや表の項目が異なっている箇所がありますが、原版のソースファイルを参照した上で趣旨に沿わない形でhtmlが生成されたと考えられるところは、訳者の判断で修正しています。(訳注をご確認ください)

誤訳やデッドリンクを見つけたり、わかりづらい記述があるときは、近藤茂徳( shigemail )までお知らせいただけるとありがたいです。

この文書について

この文書は、Beautiful Soup 4の主要な機能をすべて、例とともに説明しています。このライブラリが何に役立つのか、どのように機能するのか、どのように使用するのか、希望通りに動作させる方法、そして期待に反した場合にどう対処するかを紹介します。

このドキュメントは、Beautiful Soupバージョン4.12.0を対象としています。ドキュメント内の例はPython 3.8向けに書かれています。

Beautiful Soup 3のドキュメントをお探しの場合、Beautiful Soup 3はすでに開発が終了しており、2020年12月31日をもってすべてのサポートが終了したことを知っておいてください。Beautiful Soup 3とBeautiful Soup 4(以下、BS3,BS4)の違いについて学びたい場合は、「BS4へのコード移行」を参照してください。

このドキュメントは、Beautiful Soupのユーザーによって他の言語にも翻訳されています。

助けてほしいときは

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(3人の姉妹)」ドキュメントを Beautiful Soup にかけると、 BeautifulSoup オブジェクトが得られます。これは入れ子データ構造でドキュメントを表現します。:

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')

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="link3">
#     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 python3-bs4

BS4 は PyPi を通じて公開されています。そのため、システムのパッケージ管理ツールでインストールできない場合でも、easy_installpip を使ってインストールできます。パッケージ名は beautifulsoup4 です。使用している Python のバージョンに対応した pipeasy_install を使用することを確認してください(これらは pip3easy_install3 と名付けられている場合があります)。

$ easy_install beautifulsoup4

$ pip install beautifulsoup4

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

easy_installpip がインストールされていない場合は、BS4の.tar.gzファイルをダウンロード して、setup.py を使ってインストールすることができます。

$ python setup.py install

他の方法が全てうまくいかない場合でも、Beautiful Soup のライセンスにより、ライブラリ全体を自分のアプリケーションと一緒にパッケージ化することが許可されています。.tar.gzファイルをダウンロードし、その中の bs4 ディレクトリを自分のアプリケーションのコードベースにコピーすることで、Beautiful Soup をインストールせずに使用することができます。

私は Python 3.10 を使って Beautiful Soup を開発していますが、他の最近のバージョンでも動作するはずです。

パーサー(構文解析器)のインストール

Beautiful Soupは、Pythonの標準ライブラリに含まれているHTMLパーサーをサポートしていますが、他にもいくつかのサードパーティ製Pythonパーサーをサポートしています。その一つが lxmlパーサー です。環境によっては、以下のコマンドのいずれかを使用してlxmlをインストールすることができます。:

$ apt-get install python-lxml

$ easy_install lxml

$ pip install lxml

もう一つの選択肢として、pure-Pythonで書かれた html5lib パーサー があります。これは、ウェブブラウザと同じ方法でHTMLを解析します。環境によっては、以下のコマンドのいずれかを使用してhtml5libをインストールすることができます。

$ apt-get install python-html5lib

$ easy_install html5lib

$ pip install html5lib

この表は、各パーサーライブラリの利点と欠点をまとめたものです。

パーサー

一般的な使用法

利点

欠点

Pythonのhtml.parser

BeautifulSoup(markup, “html.parser”)

  • 標準ライブラリに含まれている

  • そこそこの速度

  • 柔軟(Python 3.2以降)

  • lxmlほど高速ではない

  • html5libほど柔軟ではない

lxmlのHTMLパーサー

BeautifulSoup(markup, “lxml”)

  • 非常に高速

  • 柔軟

  • 外部Cに依存

lxmlのXMLパーサー

BeautifulSoup(markup, “lxml-xml”) BeautifulSoup(markup, “xml”)

  • 非常に高速

  • 現在サポートされている唯一の XMLパーサー

  • 外部Cに依存

html5lib

BeautifulSoup(markup, “html5lib”)

  • 非常に柔軟

  • ウェブブラウザと同じ方法で ページを解析する

  • 有効なHTML5を生成する

  • 非常に遅い

  • 外部Pythonに依存

(訳注: 原版の Beautiful Soup 4.12.0 Documentation. のhtmlファイルには、 Lenient(柔軟) の項目がありませんが、Sphinxのソースである index.rst.txt をみると Lenient(柔軟) の記述があります。原版のhtml生成時に抜け落ちたと判断して、日本語版には記載されるように修正しました。)

(訳注: 「柔軟」とは、HTMLやXMLの構文に厳密に従っていない場合にも、できるだけ解析を試みることを指します。)

可能であれば、速度のためにlxmlをインストールして使用することをお勧めします。

なお、ドキュメントが無効な場合、異なるパーサーが異なるBeautiful Soupツリーを生成することがあります。詳細については パーサー毎の違い を参照してください。

スープ作り(ドキュメントの解析)

ドキュメントを解析するには、それを BeautifulSoup のコンストラクタに渡します。コンストラクタには、文字列やオープンファイルハンドルを渡すことができます:

from bs4 import BeautifulSoup

with open("index.html") as fp:
    soup = BeautifulSoup(fp, 'html.parser')

soup = BeautifulSoup("<html>a web page</html>", 'html.parser')

まず、ドキュメントはUnicodeに変換され、HTMLエンティティがUnicode文字に変換されます:

print(BeautifulSoup("<html><head></head><body>Sacr&eacute; bleu!</body></html>", "html.parser"))
# <html><head></head><body>Sacré bleu!</body></html>

その後、Beautiful Soupは利用可能な最高のパーサーを使用してドキュメントを解析します。特に指定がない限り、HTMLパーサーを使用しますが、XMLパーサーを使用するように指示することもできます。(XMLの解析 を参照)

オブジェクトの種類

Beautiful Soupは、複雑なHTMLドキュメントをPythonオブジェクトの複雑なツリーに変換します。しかし、扱う必要があるのは主に 4種類 のオブジェクトだけです: Tag, NavigableString, BeautifulSoup, そして Comment です。

class bs4.Tag

Tag オブジェクトは、元のドキュメントにおけるXMLまたはHTMLタグに対応します。:

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

タグには多くの属性とメソッドがありますが、そのほとんどは 解析木を移動解析木を検索 で説明します。今のところ、タグの最も重要なメソッドは、その名前と属性にアクセスするためのものです。

name

すべてのタグには名前があります:

tag.name
# 'b'

タグの名前を変更すると、その変更はBeautiful Soupによって生成されるマークアップ全体に反映されます:

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

attrs

HTMLまたはXMLタグには任意の数の属性が存在することがあります。例えば、<b id="boldest"> というタグには、”id”という属性があり、その値は”boldest”です。タグの属性には、タグを辞書のように扱うことでアクセスできます:

tag = BeautifulSoup('<b id="boldest">bold</b>', 'html.parser').b
tag['id']
# 'boldest'

属性の辞書には直接 .attrs としてアクセスできます:

tag.attrs
# {'id': 'boldest'}

タグの属性を追加、削除、または変更することができます。これもまた、タグを辞書のように扱うことで行われます:

tag['id'] = 'verybold'
tag['another-attribute'] = 1
tag
# <b another-attribute="1" id="verybold"></b>

del tag['id']
del tag['another-attribute']
tag
# <b>bold</b>

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

複数の値を持つ属性

HTML 4では、複数の値を持つことができる属性がいくつか定義されています。HTML 5では、そのうちのいくつかが削除されましたが、さらにいくつか新たに定義されました。最も一般的な複数値を持つ属性は class です(つまり、1つのタグに複数のCSSクラスを持たせることができます)。他には、relrevaccept-charsetheadersaccesskey などがあります。デフォルトでは、Beautiful Soupは複数の値を持つ属性の値をリストとして解析します:

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

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

ある属性が複数の値を 持っているように 見えても、その属性がHTML標準のいずれかのバージョンで複数値を持つ属性として定義されていない場合、Beautiful Soupはその属性を、リストにはせずに、1つの文字列として扱います:

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

タグを文字列に戻すと、複数の属性値が統合されます:

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

すべての属性を文字列として解析したい場合は、キーワード引数として multi_valued_attributes=NoneBeautifulSoup コンストラクタに渡します:

no_list_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser', multi_valued_attributes=None)
no_list_soup.p['class']
# 'body strikeout'

get_attribute_list を使用して、複数値の属性であってもそうでなくても、常にリストとして値を取得することができます:

id_soup.p.get_attribute_list('id')
# ["my id"]

XMLとしてドキュメントを解析する場合、複数値を持つ属性は存在しません:

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

multi_valued_attributes 引数を使用することで、元のようにリストで取得できます:

class_is_multi= { '*' : 'class'}
xml_soup = BeautifulSoup('<p class="body strikeout"></p>', 'xml', multi_valued_attributes=class_is_multi)
xml_soup.p['class']
# ['body', 'strikeout']

おそらく、普通に使う分には、複数の値を持つことができる属性を変更する必要はないでしょう。しかし、特別な理由で設定を変えたい場合は、以下の最初から用意されているデフォルトの設定を参考にしてください。このデフォルトの設定は、HTMLの公式なルールに従って実装されています。:

from bs4.builder import builder_registry
builder_registry.lookup('html').DEFAULT_CDATA_LIST_ATTRIBUTES
class bs4.NavigableString

文字列は、タグ内のテキストの一部に対応します。Beautiful Soupは、このようなテキストの断片を格納するために NavigableString クラスを使用します:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
tag = soup.b
tag.string
# 'Extremely bold'
type(tag.string)
# <class 'bs4.element.NavigableString'>

NavigableString は、通常のPythonのUnicode文字列とほぼ同じですが、解析木を移動解析木を検索 で説明されているいくつかの機能もサポートしています。 NavigableString をUnicode文字列に変換するには、 str を使用します:

unicode_string = str(tag.string)
unicode_string
# 'Extremely bold'
type(unicode_string)
# <type 'str'>

文字列をその場で編集することはできませんが、replace_with() を使って別の文字列に置き換えることはできます:

tag.string.replace_with("No longer bold")
tag
# <b class="boldest">No longer bold</b>

NavigableString は、解析木を移動解析木を検索 で説明されているほとんどの機能をサポートしていますが、すべてではありません。特に、文字列は何も含むことができない(タグが文字列や別のタグを含むことができるように)ため、文字列には .contents.string 属性、または find() メソッドはサポートされていません。

NavigableString をBeautiful Soupの外で使用したい場合は、それを通常のPython Unicode文字列に変換するために unicode() を呼び出す必要があります。これを行わないと、文字列はBeautiful Soupの解析木全体への参照を保持し続け、使用が終わった後でもメモリを無駄に消費することになります。

class bs4.BeautifulSoup

BeautifulSoup オブジェクトは、解析されたドキュメント全体を表します。ほとんどの場合、このオブジェクトを Tag オブジェクトのように扱うことができます。これは、 解析木を移動解析木を検索 で説明されているほとんどのメソッドをサポートしていることを意味します。

また、 BeautifulSoup オブジェクトを ツリーの修正 で定義されているメソッドに渡すこともできます。これは、 Tag と同様に使用できます。これにより、2つの解析されたドキュメントを組み合わせることができるようになります:

doc = BeautifulSoup("<document><content/>INSERT FOOTER HERE</document", "xml")
footer = BeautifulSoup("<footer>Here's the footer</footer>", "xml")
doc.find(text="INSERT FOOTER HERE").replace_with(footer)
# 'INSERT FOOTER HERE'
print(doc)
# <?xml version="1.0" encoding="utf-8"?>
# <document><content/><footer>Here's the footer</footer></document>

BeautifulSoup オブジェクトは、実際のHTMLやXMLタグに対応していないため、名前や属性はありません。しかし、時々その .name を確認するのが便利な場合があります。そのため、このオブジェクトには特別に .name “[document]” が与えられています:

soup.name
# '[document]'

特別な文字列

TagNavigableStringBeautifulSoup といったオブジェクトは、HTMLやXMLファイル内のほとんどすべての文字列をカバーしますが、いくつか取得できない要素があります。その中でおそらく最もよく出現するのは Comment です。

class bs4.Comment

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

Comment オブジェクトは、単に特別な種類の NavigableString です:

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

しかし、HTMLドキュメントの一部として表示されると、 Comment は特別な書式で表示されます:

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

HTMLドキュメント独自の文字列

Beautiful Soup は、特定の HTML タグ内に見つかった文字列を保持するためのいくつかの NavigableString サブクラスを定義しています。これにより、ページの主要な本文を見つけやすくし、ページ内のプログラム指示を表す可能性がある文字列を無視することができます。(これらのクラスは Beautiful Soup 4.9.0 で新しく追加されましたが、html5lib パーサーでは使用されていません。)

class bs4.Stylesheet

NavigableString サブクラスであり、埋め込み CSS スタイルシートを表します。すなわち、ドキュメントの解析中に <style> タグ内に見つかった文字列を保持します。

class bs4.Script

NavigableString サブクラスであり、埋め込み Javascript を表します。すなわち、ドキュメントの解析中に <script> タグ内に見つかった文字列を保持します。

class bs4.Template

NavigableString サブクラスであり、埋め込み HTML テンプレートを表します。すなわち、ドキュメントの解析中に <template> タグ内に見つかった文字列を保持します。

XMLドキュメント独自の文字列

Beautiful Soup は、XML ドキュメント内で見つかる特別な種類の文字列を保持するために、いくつかの NavigableString クラスを定義しています。これらのクラスは Comment のように、出力時に文字列に何かを追加する NavigableString のサブクラスです。

class bs4.Declaration

XML ドキュメントの最初にある XML宣言 を表す NavigableString のサブクラス。

class bs4.Doctype

XML ドキュメントの最初近くに見つかる可能性がある DOCTYPE宣言 を表す NavigableString のサブクラス。

class bs4.CData

CDATAセクション を表す NavigableString のサブクラス。

class bs4.ProcessingInstruction

XML処理命令 の内容を表す NavigableString のサブクラス。

解析木を移動

再度、「Three sisters(3人の姉妹)」の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>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')

この例を使って、ドキュメント内の一部から別の部分へどのように移動するかを説明します。

下へ移動

タグは文字列や他のタグを含むことがあります。これらの要素はタグの 子要素(children) と呼ばれます。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>タグを取得する必要がある場合や、特定の名前を持つ最初のタグ以上に複雑なものを取得する必要がある場合は、解析木を検索 の節で説明されている 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
# ['The Dormouse's story']

BeautifulSoup オブジェクト自体にも子要素があります。この場合、<html> タグが BeautifulSoup オブジェクトの子要素です:

len(soup.contents)
# 1
soup.contents[0].name
# '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 リストを直接変更しないでください。これをすると、細かくて見つけにくい問題が発生する可能性があります。

.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> タグには子要素が一つしかありませんが、子孫は二つあります。それは、<title> タグと <title> タグの子要素です。また、BeautifulSoup オブジェクトには直接の子要素が <html> タグ一つだけですが、子孫はたくさんあります:

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

.string

タグに一つの子要素しかなく、その子要素が NavigableString である場合、その子要素は .string としてアクセスできます:

title_tag.string
# 'The Dormouse's story'

タグの唯一の子要素が別のタグであり、そのタグ.string を持っている場合、親タグもその子要素と同じ .string を持つと見なされます:

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

head_tag.string
# 'The Dormouse's story'

タグが複数の要素を含んでいる場合、.string が何を指すべきかが不明確になるため、.stringNone と定義されます:

print(soup.html.string)
# None

.strings.stripped_strings

タグの中に複数の要素がある場合でも、文字列だけを確認することができます。.strings ジェネレーターを使用します:

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

これらの文字列には余分な空白が多く含まれることがよくありますが、代わりに .stripped_strings ジェネレーターを使用すると、それを取り除くことができます:

for string in soup.stripped_strings:
    print(repr(string))
# "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'
# ';\n and they lived at the bottom of a well.'
# '...'

ここでは、空白だけで構成されている文字列は無視され、文字列の先頭と末尾にある空白は削除されます。

上へ移動

引き続き「家系図」の例えでみてみると、すべてのタグや文字列にはそれを含む 親要素(parent) があります。

.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 を使って反復処理することができます。この例では、ドキュメント内に深く埋もれた<a>タグからドキュメントの最上部まで .parents を使って移動しています:

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

横へ移動

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

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

この場合、<b>タグと<c>タグは同じ階層にあり、同じタグの直接の子要素であるため、これらを 兄弟要素(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” と “text2” は 兄弟要素ではありません。なぜなら、それらは同じ親を持っていないからです:

sibling_soup.b.string
# 'text1'

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

実際のドキュメントでは、タグの .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> and
# <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;

最初の <a> タグの .next_sibling が次の <a> タグだと思うかもしれませんが、実際には最初の <a> タグと次の <a> タグを分けるカンマと改行の文字列です:

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

link.next_sibling
# ',\n '

次の <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))
# ',\n'
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
# ' and\n'
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
# '; and they lived at the bottom of a well.'

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>
# ',\n'
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
# 'Once upon a time there were three little sisters; and their names were\n'

前後の移動

“Three sisters” のドキュメントの冒頭を見てみましょう:

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

HTMLパーサーは、この文字列を一連のイベントに変換します。”<html>タグを開く”、”<head>タグを開く”、”<title>タグを開く”、”文字列を追加する”、”<title>タグを閉じる”、”<p>タグを開く”などです。Beautiful Soupは、このドキュメントの最初の解析を再構築するためのツールを提供します。

.next_element.previous_element

文字列やタグの .next_element 属性は、その直後に解析されたものを指します。これは .next_sibling と同じ場合もありますが、通常は大きく異なります。

“Three sisters” のドキュメントにおける最後の <a> タグを見てみましょう。その .next_sibling は文字列であり、<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
# ';\nand they lived at the bottom of a well.'

しかし、その <a> タグの .next_element は、その文章の 残りではなく 、”Tillie” という単語です:

last_a_tag.next_element
# 'Tillie'

これは、元のマークアップでは、”Tillie” という単語がセミコロンの前に現れていたためです。パーサーは、最初に <a> タグに遭遇し、その後に “Tillie” という単語、次に閉じる </a> タグ、そしてセミコロンと文章の残り部分に遭遇しました。セミコロンは <a> タグと同じレベルにありますが、最初に “Tillie” という単語が出現しました。

.previous_element 属性は .next_element の正反対で、その直前に解析された要素を指します:

last_a_tag.previous_element
# ' 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))
# 'Tillie'
# ';\nand they lived at the bottom of a well.'
# '\n'
# <p class="story">...</p>
# '...'
# '\n'

解析木を検索

Beautiful Soup は解析木を検索するための多くのメソッドを定義していますが、それらはすべて非常に似ています。ここでは、最も人気のある2つのメソッドである find()find_all() について詳しく説明します。その他のメソッドもほとんど同じ引数を取るので、簡単に触れておきます。

再び、”Three sisters” のドキュメントを例として使用します:

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>
"""

from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')

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 はその正規表現を使用して search() メソッドを使ってフィルタリングを行います。このコードは、名前が “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 を返します。

ここでは、”class” 属性を持っているが “id” 属性を持っていないタグのときに True を返す関数の例を示します:

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

この関数を find_all() に渡すと、すべての <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…bottom of a well.</p>,
#  <p class="story">...</p>]

この関数は <p> タグのみを取得します。<a> タグは “class” と “id” の両方を定義しているため、取得しません。また、<html> や <title> のような “class” を定義していないタグも取得しません。

特定の属性(例えば href )でフィルタリングするために関数を渡す場合、その関数にはタグ全体ではなく属性値が引数として渡されます。以下は、 href 属性が特定の正規表現に 一致しない すべての a タグを見つける関数です:

import re
def not_lacie(href):
    return href and not re.compile("lacie").search(href)

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

関数は必要に応じて複雑にすることができます。次に、タグが文字列オブジェクトに囲まれている場合に 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)
# body
# p
# a
# a
# a
# p

これで、検索メソッドの詳細について説明する準備が整いました。

find_all()

メソッドシグネチャ: find_all(name, attrs, recursive, string, limit, **kwargs)

find_all() メソッドは、タグの子孫を調べて、指定したフィルターに一致する すべての 子孫を取得します。フィルターの種類 でいくつかの例を挙げましたが、ここではさらにいくつかの例を示します:

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(string=re.compile("sisters"))
# 'Once upon a time there were three little sisters; and their names were\n'

これらのうちいくつかは見覚えがあるかもしれませんが、他のものは初出でしょう。stringid に値を渡すとはどういう意味でしょうか?なぜ find_all("p", "title") は、CSSクラス “title”を持つ <p> タグを見つけるのでしょうか? find_all() の引数について見ていきましょう。

name引数

name に値を渡すと、Beautiful Soup は特定の名前を持つタグのみを考慮します。テキスト文字列や名前が一致しないタグは無視されます。

これは最もシンプルな使用例です:

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

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

キーワード引数

未定義の引数は、タグの属性に基づくフィルターに変換されます。例えば、id という名前の引数に値を渡すと、Beautiful Soup は各タグの ‘id’ 属性に対してフィルターを適用します

(訳注: find_all() メソッドは、”name”、”attrs”、”recursive”、”string” といった特定の引数を受け取ることを定義しています。それ以外の名前の引数が渡された場合、それは「未定義の引数」として扱われます。このとき、Beautiful Soup はその引数をタグの属性に対するフィルターとして解釈します。):

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

同様に、href に値を渡すと、Beautiful Soup は各タグの ‘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">Elsie</a>]

HTML 5 の data-* 属性のように、キーワード引数の名前として使用できない属性名も存在します:

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

このような属性を検索するには、属性を辞書に入れ、その辞書を find_all()attrs 引数に渡します:

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

また、HTML の ‘name’ 要素を検索するためにキーワード引数を使用することはできません。なぜなら、Beautiful Soup は name 引数をタグ自体の名前を指定するために使用するからです。この場合、attrs 引数に ‘name’ の値を指定します:

name_soup = BeautifulSoup('<input name="email"/>', 'html.parser')
name_soup.find_all(name="email")
# []
name_soup.find_all(attrs={"name": "email"})
# [<input name="email"/>]

CSSクラスでの検索

特定のCSSクラスを持つタグを検索することは非常に便利です。しかし、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>]

1つのタグに複数の “class” 属性の値を持てることに 注意してください。特定のCSSクラスに一致するタグを検索する際には、そのタグの いずれかの CSSクラスに一致するかどうかが判断されます:

css_soup = BeautifulSoup('<p class="body strikeout"></p>', 'html.parser')
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")
# []

複数のCSSクラスに一致するタグを検索する場合は、CSSセレクタを使用するのが良いでしょう:

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

class_ ショートカットがない古いバージョンのBeautiful Soupでは、上記の attrs トリックを使用できます。classの値に文字列(または正規表現など)を設定した辞書を作成し、それを find_all() メソッドのattrs引数として渡します:

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

string 引数

string 引数を使用すると、タグではなく文字列を検索することができます。 name や他のキーワード引数と同様に、 文字列正規表現リスト関数、 または True値 を渡すことができます。以下にいくつかの例を示します:

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

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

soup.find_all(string=re.compile("Dormouse"))
# ["The Dormouse's story", "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(string=is_the_only_string_within_a_tag)
# ["The Dormouse's story", "The Dormouse's story", 'Elsie', 'Lacie', 'Tillie', '...']

string は文字列を検索するためのものですが、タグを検索する引数と組み合わせることもできます。Beautiful Soup は、指定した string の値と一致する .string を持つすべてのタグを見つけます。このコードは、 .string が “Elsie” である <a> タグを見つけます:

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

string 引数は Beautiful Soup 4.4.0 で新しく導入されました。それ以前のバージョンでは text と呼ばれていました:

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

limit 引数

find_all() は、指定したフィルタに一致するすべてのタグや文字列を返します。ドキュメントが大きい場合、この処理に時間がかかることがあります。すべて の結果が必要でない場合、 limit に数値を指定することができます。これは SQL の LIMIT キーワードと同じように機能し、指定した数に達した時点で Beautiful Soup に結果の収集を停止させます。

「three sisters」ドキュメントには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 のすべての子孫(その子要素、その子の子要素、さらにその先の要素など)を調べます。もし、Beautiful Soup に直接の子要素のみを考慮させたい場合は、 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> タグの下にありますが、それは<html> タグの 直接の 下ではありません。<head> タグがその間にあります。Beautiful Soup は <html> タグのすべての子孫を検索できる場合は <title> タグを見つけますが、 recursive=False で制限して <html> タグの直接の子要素だけを検索する場合は、何も見つけません。

Beautiful Soup は多くのツリー検索メソッドを提供しています(以下で説明します)が、それらはほとんどが find_all() と同じ引数( nameattrsstringlimit、およびキーワード引数)を取ります。しかし、recursive 引数は異なります。 find_all()find() のみがこれをサポートします。 find_parents() のようなメソッドに recursive=False を渡しても、それほど役に立たないでしょう。

find_all() 的なオブジェクトの呼び出し

Beautiful Soup の検索 API の中で find_all() は最も人気のあるメソッドであるため、これのショートカットを使用することができます。もし、BeautifulSoup オブジェクトや Tag オブジェクトを関数のように扱うと、それはそのオブジェクトに対して find_all() を呼び出すのと同じ意味になります。以下の二つのコード行は等価です:

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

これら二つのコード行も等価です:

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

find()

メソッドシグネチャ: find(name, attrs, recursive, string, **kwargs)

find_all() メソッドはドキュメント全体をスキャンして結果を探しますが、時には結果が1つしか見つからない場合もあります。例えば、ドキュメントに <body> タグが1つしかないことが分かっている場合、ドキュメント全体をスキャンするのは無駄です。毎回 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>

唯一の違いは、 find_all() が単一の結果を含むリストを返すのに対して、 find() はその結果だけを返すことです。

find_all() が何も見つけられなかった場合、空のリストを返します。一方、 find() が何も見つけられなかった場合、 None を返します:

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

Remember the soup.head.title trick from タグ名を使った移動 ? That trick works by repeatedly calling find()

タグ名を使った移動 のセクションで紹介した soup.head.title のトリックを覚えていますか? このトリックは、 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()

メソッドシグネチャ: find_parents(name, attrs, string, limit, **kwargs)

メソッドシグネチャ: find_parent(name, attrs, string, **kwargs)

これまでに find_all()find() について詳しく説明してきましたが、Beautiful Soup の API には解析木を検索するための他の10のメソッドも定義されています。ただし、心配しないでください。これらのうち5つは基本的に find_all() と同じで、他の5つは find() と基本的に同じです。違いは、どの部分を検索するかだけです。

まずは find_parents()find_parent() について考えてみましょう。 find_all()find() が解析木を下に向かってタグの子孫を探すのに対し、これらのメソッドはその逆を行います。つまり、解析木を に向かって、タグ(または文字列)の親を探します。では、”Three sisters” ドキュメントの中に深く埋もれた文字列から試してみましょう:

a_string = soup.find(string="Lacie")
a_string
# '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つがこの文字列の間接的な親であり、そのタグも検索で見つかります。しかし、ドキュメントの どこか` には “title” というCSSクラスを持つ <p> タグが存在しますが、それはこの文字列の親ではないため、 find_parents() では見つかりません。

ここで find_parent()find_parents() が、以前に説明した .parent.parents の属性と関連していることに気づいたかもしれません。実際、この関連性は非常に強いです。これらの検索メソッドは実際に .parents を使ってすべての親を繰り返しチェックし、提供されたフィルターに一致するかどうかを確認します。

find_next_siblings()find_next_sibling()

メソッドシグネチャ: find_next_siblings(name, attrs, string, limit, **kwargs)

メソッドシグネチャ: find_next_sibling(name, attrs, string, **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()find_previous_sibling()

メソッドシグネチャ: find_previous_siblings(name, attrs, string, limit, **kwargs)

メソッドシグネチャ: find_previous_sibling(name, attrs, string, **kwargs)

これらのメソッドは .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()find_next()

メソッドシグネチャ: find_all_next(name, attrs, string, limit, **kwargs)

メソッドシグネチャ: find_next(name, attrs, string, **kwargs)

これらのメソッドは .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(string=True)
# ['Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
#  ';\nand they lived at the bottom of a well.', '\n', '...', '\n']

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

最初の例では、文字列 “Elsie” が表示されましたが、これは <a> タグ内に含まれていました。二番目の例では、ドキュメント内の最後の <p> タグが表示されましたが、これは開始要素である <a> タグと同じツリーの部分にはありません。これらのメソッドでは、フィルターに一致する要素が、開始要素より後にドキュメントに現れることが重要です。

find_all_previous()find_previous()

メソッドシグネチャ: find_all_previous(name, attrs, string, limit, **kwargs)

メソッドシグネチャ: find_previous(name, attrs, string, **kwargs)

これらのメソッドは .previous_elements を使用して、ドキュメント内でその要素より前に出現したタグや文字列をたどります。 find_all_previous() メソッドは一致するすべての要素を返し、 find_previous() は最初に一致する要素のみを返します:

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") の呼び出しは、ドキュメント内の最初の段落(class=”title” のもの)を見つけましたが、同時に最初の <p> タグも見つけました。この <p> タグは、開始要素である <a> タグを含んでいます。これは当然のことです。ドキュメント内で、開始要素より前に表示されるすべてのタグを探しているため、<a> タグを含む <p> タグは、含まれる <a> タグよりも前に表示されるはずです。

CSSセレクタの使用

BeautifulSoup および Tag オブジェクトは、 .css プロパティを通じてCSSセレクタをサポートしています。実際のセレクタの実装は、Soup Sieve パッケージによって行われており、このパッケージはPyPIで soupsieve として利用可能です。もし pip を使用してBeautiful Soupをインストールした場合、Soup Sieveも同時にインストールされているので、特別な設定は必要ありません。

Soup Sieveのドキュメントには、 現在サポートされているすべてのCSSセレクタ のリストがありますが、ここでは基本的な例をいくつか紹介します。タグを見つけることができます:

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

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

他のタグの下にあるタグを見つける:

soup.css.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.css.select("html head title")
# [<title>The Dormouse's story</title>]

他のタグの 直接 下にあるタグを見つける:

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

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

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

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

タグの兄弟を見つける:

soup.css.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.css.select("#link1 + .sister")
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]

CSSクラスでタグを見つける:

soup.css.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.css.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>]

IDでタグを見つける:

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

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

セレクタリストから一致するタグを見つける:

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

属性の存在をテストする:

soup.css.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.css.select('a[href="http://example.com/elsie"]')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]

soup.css.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.css.select('a[href$="tillie"]')
# [<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

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

select_one() というメソッドもあり、セレクタに一致する最初のタグだけを見つけます:

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

便利なことに、 .css プロパティを省略して、BeautifulSoupTag オブジェクトに直接 select()select_one() を呼び出すことができます:

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

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

CSSセレクタのサポートは、すでにCSSセレクタの構文を知っている人にとっては便利です。これらはすべてBeautiful Soup APIで実行できますが、もしCSSセレクタだけが必要なら、Beautiful Soupを使わずに lxml でドキュメントを解析する方がはるかに高速です。ただし、Soup Sieveを使用すると、CSSセレクタをBeautiful Soup APIと 組み合わせる ことができます。

上級者向けのSoup Sieve機能

Soup Sieveは、 select()select_one() メソッド以外にも豊富なAPIを提供しており、これらのAPIのほとんどは TagBeautifulSoup.css 属性を通じてアクセスできます。以下にサポートされているメソッドのリストを示しますが、詳細なドキュメントについては Soup Sieveのドキュメント を参照してください。

iselect() メソッドは select() と同様に動作しますが、リストではなくジェネレーターを返します:

[tag['id'] for tag in soup.css.iselect(".sister")]
# ['link1', 'link2', 'link3']

closest() メソッドは、指定された Tag の親要素の中で、指定したCSSセレクタに一致する最も近い要素を返します。これはBeautiful Soupの find_parent() メソッドに似ています:

elsie = soup.css.select_one(".sister")
elsie.css.closest("p.story")
# <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>

match() メソッドは、特定の Tag がセレクタに一致するかどうかに基づいてブール値を返します:

# elsie.css.match("#link1")
True

# elsie.css.match("#link2")
False

filter() メソッドは、指定したセレクタに一致するタグの直接の子要素の部分集合を返します:

[tag.string for tag in soup.find('p', 'story').css.filter('a')]
# ['Elsie', 'Lacie', 'Tillie']

escape() メソッドは、無効になる可能性があるCSS識別子をエスケープします:

soup.css.escape("1-strange-identifier")
# '\\31 -strange-identifier'

CSSセレクタにおける名前空間

名前空間を定義しているXMLをパースした場合、その名前空間をCSSセレクタで使用することができます。:

from bs4 import BeautifulSoup
xml = """<tag xmlns:ns1="http://namespace1/" xmlns:ns2="http://namespace2/">
  <ns1:child>I'm in namespace 1</ns1:child>
  <ns2:child>I'm in namespace 2</ns2:child>
</tag> """
namespace_soup = BeautifulSoup(xml, "xml")

namespace_soup.css.select("child")
# [<ns1:child>I'm in namespace 1</ns1:child>, <ns2:child>I'm in namespace 2</ns2:child>]

namespace_soup.css.select("ns1|child")
# [<ns1:child>I'm in namespace 1</ns1:child>]

Beautiful Soupは、ドキュメントのパース中に見た内容に基づいて、適切な名前空間接頭辞を使用しようとしますが、独自の省略形の辞書を提供することもできます。:

namespaces = dict(first="http://namespace1/", second="http://namespace2/")
namespace_soup.css.select("second|child", namespaces=namespaces)
# [<ns1:child>I'm in namespace 2</ns1:child>]

CSSセレクタのサポートの歴史

.css プロパティは、Beautiful Soup 4.12.0で追加されました。それ以前は、.select().select_one() の便利メソッドのみがサポートされていました。

Soup Sieveの統合は、Beautiful Soup 4.7.0で追加されました。それ以前のバージョンにも .select() メソッドがありましたが、サポートされていたのは、最も一般的に使用されるCSSセレクタのみでした。

ツリーの修正

Beautiful Soupの主な強みは解析木の検索にありますが、ツリーを修正して、新しいHTMLやXMLドキュメントとして変更を保存することもできます。

タグ名と属性の変更

以前に Tag.attrs で説明しましたが、再度触れておく価値があります。タグをリネームしたり、属性の値を変更したり、新しい属性を追加したり、属性を削除したりすることができます。以下に例を示します:

soup = BeautifulSoup('<b class="boldest">Extremely bold</b>', 'html.parser')
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 の修正

タグの .string 属性に新しい文字列を設定すると、そのタグの内容がその文字列で置き換えられます:

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

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

タグに他のタグが含まれていた場合、それらとその内容はすべて削除されてしまうことに注意してください。

append()

Tag.append() を使って、タグの内容に要素を追加することができます。これはPythonのリストで .append() を呼び出すのと同じように機能します:

soup = BeautifulSoup("<a>Foo</a>", 'html.parser')
soup.a.append("Bar")

soup
# <a>FooBar</a>
soup.a.contents
# ['Foo', 'Bar']

extend()

Beautiful Soup 4.7.0から、Tag.extend() というメソッドもサポートしています。このメソッドは、リストの各要素を順番に Tag に追加します:

soup = BeautifulSoup("<a>Soup</a>", 'html.parser')
soup.a.extend(["'s", " ", "on"])

soup
# <a>Soup's on</a>
soup.a.contents
# ['Soup', ''s', ' ', 'on']

insert()

Tag.insert()Tag.append() とほぼ同じですが、新しい要素が必ずしも親タグの .contents の末尾に追加されるわけではありません。指定した数値位置に挿入されます。これはPythonリストの .insert() と同じように機能します:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup, 'html.parser')
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
# ['I linked to ', 'but did not endorse', <i>example.com</i>]

insert_before()insert_after()

insert_before() メソッドは、解析木内の他の要素の直前にタグや文字列を挿入します:

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

insert_after() メソッドは、解析木内の他の要素の直後にタグや文字列を挿入します:

div = soup.new_tag('div')
div.string = 'ever'
soup.b.i.insert_after(" you ", div)
soup.b
# <b><i>Don't</i> you <div>ever</div> leave</b>
soup.b.contents
# [<i>Don't</i>, ' you', <div>ever</div>, 'leave']

clear()

Tag.clear() は、タグの内容を削除します:

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

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

extract()

PageElement.extract() は、タグや文字列をツリーから削除します。削除されたタグや文字列が返されます:

markup = '<a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup, 'html.parser')
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

この時点で、ドキュメントを解析するために使用した BeautifulSoup オブジェクトをルートとする解析木と、抽出されたタグをルートとする別の解析木が存在することになります。抽出した要素の子要素に対して extract を呼び出すこともできます:

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

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

decompose()

Tag.decompose() は、ツリーからタグを削除し、その後 完全にタグとその内容を破壊します

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

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

decomposeされた TagNavigableString の動作は定義されておらず、何かに使用するべきではありません。何かが decompose されたかどうかを確認するために、その .decomposed プロパティをチェックすることができます (Beautiful Soup 4.9.0 で追加)

i_tag.decomposed
# True

a_tag.decomposed
# False

replace_with()

PageElement.replace_with() は、ツリーからタグや文字列を削除し、指定した1つ以上のタグや文字列で置き換えることができます:

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

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

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

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

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

replace_with() は、置き換えられたタグや文字列を返すため、それを確認したり、ツリーの別の部分に追加したりすることができます。

replace_with() に複数の引数を渡す機能は、Beautiful Soup 4.10.0で追加されました。

wrap()

PageElement.wrap() は、指定したタグで要素を包みます。これにより、新しいラッパーが返されます:

soup = BeautifulSoup("<p>I wish I was bold.</p>", 'html.parser')
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 で新しく追加されました。

unwrap()

Tag.unwrap()wrap() の反対です。タグをその中身に置き換えます。これはマークアップを取り除くのに便利です:

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

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

replace_with() と同様に、 unwrap() は置き換えられたタグを返します。

smooth()

解析木を修正するためのメソッドをいくつか呼び出した後、複数の NavigableString オブジェクトが隣り合って配置されることがあります。Beautiful Soup はこれに問題はありませんが、新しく解析されたドキュメントではこのようなことが起こらないため、次のような動作が予想外に感じられるかもしれません:

soup = BeautifulSoup("<p>A one</p>", 'html.parser')
soup.p.append(", a two")

soup.p.contents
# ['A one', ', a two']

print(soup.p.encode())
# b'<p>A one, a two</p>'

print(soup.p.prettify())
# <p>
#  A one
#  , a two
# </p>

Tag.smooth() を呼び出すことで、隣接する文字列を統合し、解析木を整理できます:

soup.smooth()

soup.p.contents
# ['A one, a two']

print(soup.p.prettify())
# <p>
#  A one, a two
# </p>

このメソッドは Beautiful Soup 4.8.0 で新しく追加されました。

出力

整形して出力

prettify() メソッドは、Beautiful Soup の解析木を整形されたUnicode文字列に変換します。各タグや文字列が個別の行に配置されます:

markup = '<html><head><body><a href="http://example.com/">I linked to <i>example.com</i></a>'
soup = BeautifulSoup(markup, 'html.parser')
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>

改行などの空白を追加するため、 prettify() はHTMLドキュメントの意味を変更してしまう可能性があり、再フォーマットのために使用すべきではありません。 prettify() の目的は、作業するドキュメントの構造を視覚的に理解しやすくすることです。

整形無しで出力

特別なフォーマットをせずに単純に文字列が欲しい場合は、BeautifulSoup オブジェクトやその中の Tag に対して str() を呼び出せます:

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

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

str() 関数は、UTF-8 でエンコードされた文字列を返します。他のオプションについては エンコーディング を参照してください。

また、encode() を呼び出してバイト文字列を取得したり、 decode() を呼び出してUnicodeを取得することもできます。

出力形式のオプション

Beautiful Soup に HTML エンティティ(例えば “&lquot;”)を含むドキュメントを渡すと、それらは Unicode 文字に変換されます:

soup = BeautifulSoup("&ldquo;Dammit!&rdquo; he said.", 'html.parser')
str(soup)
# '“Dammit!” he said.'

その後、ドキュメントをバイト文字列に変換すると、Unicode 文字は UTF-8 としてエンコードされます。HTML エンティティは元に戻りません:

soup.encode("utf8")
# b'\xe2\x80\x9cDammit!\xe2\x80\x9d he said.'

デフォルトでは、出力時にエスケープされる文字は、アンパサンドと山括弧だけです。これらは “&amp;”, “&lt;”, “&gt;” に変換されるため、Beautiful Soup が誤って無効な HTML や XML を生成しないようにします:

soup = BeautifulSoup("<p>The law firm of Dewey, Cheatem, & Howe</p>", 'html.parser')
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>', 'html.parser')
soup.a
# <a href="http://example.com/?foo=val1&amp;bar=val2">A link</a>

この動作は、 prettify()encode()、または decode()formatter 引数を指定することで変更できます。Beautiful Soup では、 formatter に5つの値を指定できます。

デフォルトは formatter="minimal" です。文字列は、Beautiful Soup が有効な HTML/XML を生成できるように最低限の処理だけが行われます:

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

formatter="html" を指定すると、Beautiful Soup は可能な限り Unicode 文字を HTML エンティティに変換します:

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

formatter="html5" を指定すると、 formatter="html" と似ていますが、Beautiful Soup は HTML の空タグ(例: “br”)の閉じスラッシュを省略します:

br = BeautifulSoup("<br>", 'html.parser').br

print(br.encode(formatter="html"))
# b'<br/>'

print(br.encode(formatter="html5"))
# b'<br>'

さらに、値が空文字列の属性は HTML スタイルのブール属性になります:

option = BeautifulSoup('<option selected=""></option>').option
print(option.encode(formatter="html"))
# b'<option selected=""></option>'

print(option.encode(formatter="html5"))
# b'<option selected></option>'

(この動作は Beautiful Soup 4.10.0 から追加されました。)

formatter=None を指定すると、Beautiful Soup は出力時に文字列を全く修正しません。これが最も高速なオプションですが、以下の例のように無効な HTML/XML を生成する可能性があります:

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

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

フォーマッターオブジェクト

より高度な出力制御が必要な場合、Beautiful Soupのフォーマッタークラスの一つをインスタンス化し、それを formatter として渡すことができます。

class bs4.HTMLFormatter

HTMLドキュメントのフォーマットルールをカスタマイズするために使用されます。

以下は、文字列を大文字に変換するフォーマッターの例です。これは、テキストノードや属性値内に出現する文字列をすべて大文字に変換します。:

from bs4.formatter import HTMLFormatter
def uppercase(str):
    return str.upper()

formatter = HTMLFormatter(uppercase)

print(soup.prettify(formatter=formatter))
# <p>
#  IL A DIT <<SACRÉ BLEU!>>
# </p>

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

次は、整形出力時のインデントを増加させるフォーマッターの例です。:

formatter = HTMLFormatter(indent=8)
print(link_soup.a.prettify(formatter=formatter))
# <a href="http://example.com/?foo=val1&bar=val2">
#         A link
# </a>
class bs4.XMLFormatter

XMLドキュメントのフォーマットルールをカスタマイズするために使用されます。

独自フォーマッターの作成

HTMLFormatter または XMLFormatter をサブクラス化することで、出力に対するさらに細かい制御が可能になります。例えば、Beautiful Soupはデフォルトでタグ内の属性をアルファベット順に並べ替えます:

attr_soup = BeautifulSoup(b'<p z="1" m="2" a="3"></p>', 'html.parser')
print(attr_soup.p.encode())
# <p a="3" m="2" z="1"></p>

この並べ替えを無効にするには、 Formatter.attributes() メソッドをサブクラス化します。このメソッドは、どの属性を出力するか、またどの順序で出力するかを制御します。この実装では、「m」という名前の属性が存在する場合に、それをフィルタリングします。:

class UnsortedAttributes(HTMLFormatter):
    def attributes(self, tag):
        for k, v in tag.attrs.items():
            if k == 'm':
                continue
            yield k, v

print(attr_soup.p.encode(formatter=UnsortedAttributes()))
# <p z="1" a="3"></p>

最後の注意点として、 CData オブジェクトを作成する場合、その中のテキストは 正確にそのまま表示され、フォーマットはされません。 Beautiful Soupは、カスタム関数がすべての文字列をカウントするなど、独自のエンティティ置換関数を呼び出しますが、その返り値は無視されます。:

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

get_text()

ドキュメントやタグ内の人間が読めるテキストのみを取得したい場合は、 get_text() メソッドを使用できます。このメソッドは、ドキュメント内またはタグの下にあるすべてのテキストを、1つのUnicode文字列として返します。:

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

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

テキストの各部分を結合する際に使用する文字列を指定することもできます。:

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

テキストの各部分の先頭と末尾の空白を削除するように指定することもできます。:

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

ただし、その時点で .stripped_strings ジェネレータを使用して、テキストを自分で処理することを検討するかもしれません。:

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

Beautiful Soup バージョン 4.9.0 以降では、lxml や html.parser が使用されている場合、<script>、<style>、および <template> タグの内容は一般的に ‘テキスト’ と見なされません。これらのタグはページの人間が視覚的に見ることができるコンテンツの一部ではないためです。

Beautiful Soup バージョン 4.10.0 以降では、NavigableString オブジェクトに対して get_text()、.strings、または .stripped_strings を呼び出すことができます。この場合、オブジェクト自体を返すか、何も返さないかのいずれかです。したがって、これを行う唯一の理由は、混合リストを繰り返し処理する場合です。

使用するパーサーを指定

HTMLを解析するだけであれば、マークアップを BeautifulSoup コンストラクタに渡すだけで問題ないでしょう。Beautiful Soup が適切なパーサーを選択してデータを解析してくれます。しかし、使用するパーサーを変更したい場合、コンストラクタにいくつかの追加引数を渡すことができます。

BeautifulSoup コンストラクタの最初の引数は、解析したいマークアップを表す文字列やオープンされたファイルハンドルです。2番目の引数は、マークアップを どのように 解析したいかを指定します。

特に何も指定しない場合、インストールされている最も優れたHTMLパーサーが使用されます。Beautiful Soupは、lxmlのパーサーを最も優れたものとしてランク付けし、その次にhtml5lib、最後にPythonの組み込みパーサーを使用します。これを上書きして、以下のいずれかを指定することができます:

  • 解析したいマークアップの種類。現在サポートされているのは「html」、「xml」、および「html5」です。

  • 使用したいパーサーライブラリの名前。現在サポートされているオプションは「lxml」、「html5lib」、および「html.parser」(Pythonの組み込みHTMLパーサー)です。

パーサー(構文解析器)のインストール の章では、サポートされているパーサーの比較がされています。

適切なパーサーがインストールされていない場合、Beautiful Soup は指定を無視して別のパーサーを選択します。現在、サポートされているXMLパーサーはlxmlのみです。lxmlがインストールされていない場合、XMLパーサーを指定してもそのパーサーは使用されず、”lxml” を指定しても機能しません。

パーサー毎の違い

Beautiful Soup は、さまざまなパーサーに対して同じインターフェースを提供しますが、各パーサーにはそれぞれ違いがあります。異なるパーサーは、同じドキュメントから異なる解析木を作成します。最も大きな違いは、HTMLパーサーとXMLパーサーの間にあります。以下に、Pythonに付属するパーサーを使用して、HTMLとして解析された短いドキュメントの例を示します。:

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

スタンドアロンの <b/> タグは有効なHTMLではないため、html.parser はそれを <b></b> タグペアに変換します。

次に、同じドキュメントをXMLとして解析した例を示します(これを実行するには、lxmlがインストールされている必要があります)。スタンドアロンの <b/> タグがそのまま残され、ドキュメントが <html> タグ内に配置される代わりに、XML宣言が追加されている点に注目してください。:

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

HTMLパーサー間にも違いがあります。完璧に整形されたHTMLドキュメントをBeautiful Soupに渡した場合、これらの違いは重要ではありません。あるパーサーは他のパーサーよりも高速ですが、どのパーサーも元のHTMLドキュメントとまったく同じように見えるデータ構造を生成します。

しかし、ドキュメントが完璧に整形されていない場合、異なるパーサーが異なる結果を生成します。以下に、lxmlのHTMLパーサーを使用して解析された短い無効なドキュメントの例を示します。<a> タグが <body> および <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>

html5libは、浮遊している </p> タグを開く <p> タグとペアにします。さらに、html5libは空の <head> タグも追加しますが、lxmlはそれを省略します。

次に、Pythonの組み込みHTMLパーサーを使用して解析された同じドキュメントを示します。:

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

lxmlと同様に、このパーサーも閉じる </p> タグを無視しますが、html5libやlxmlとは異なり、<html> や <body> タグを追加して整形されたHTMLドキュメントを作成しようとはしません。

「<a></p>」というドキュメントは無効なため、これらの技術のいずれも「正しい」方法ではありません。html5libパーサーはHTML5標準の技術を使用しているため、最も「正しい」方法だと主張できるかもしれませんが、これらの技術はすべて正当です。

パーサー間の違いは、スクリプトに影響を与える可能性があります。スクリプトを他の人に配布する予定がある場合や、複数のマシンで実行する予定がある場合は、BeautifulSoup コンストラクタでパーサーを指定することをお勧めします。これにより、ユーザーがドキュメントをあなたとは異なる方法で解析する可能性が減少します。

エンコーディング

HTMLやXMLドキュメントは、ASCIIやUTF-8などの特定のエンコーディングで書かれています。しかし、そのドキュメントをBeautiful Soupに読み込むと、Unicodeに変換されていることがわかります。:

markup = "<h1>Sacr\xc3\xa9 bleu!</h1>"
soup = BeautifulSoup(markup, 'html.parser')
soup.h1
# <h1>Sacré bleu!</h1>
soup.h1.string
# '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, 'html.parser')
print(soup.h1)
# <h1>νεμω</h1>
print(soup.original_encoding)
# iso-8859-7

この問題を修正するには、正しい from_encoding を指定します。:

soup = BeautifulSoup(markup, 'html.parser', from_encoding="iso-8859-8")
print(soup.h1)
# <h1>םולש</h1>
print(soup.original_encoding)
# iso8859-8

正しいエンコーディングがわからない場合でも、Unicode, Dammitが間違った推測をしていることがわかっている場合は、その誤った推測を exclude_encodings として渡すことができます。:

soup = BeautifulSoup(markup, 'html.parser', exclude_encodings=["iso-8859-7"])
print(soup.h1)
# <h1>םולש</h1>
print(soup.original_encoding)
# WINDOWS-1255

Windows-1255は完全に正しいわけではありませんが、このエンコーディングはISO-8859-8の互換スーパーセットであるため、十分に近い結果が得られます。( exclude_encodings はBeautiful Soup 4.4.0で導入された新機能です。)

稀なケース(通常、UTF-8ドキュメントに完全に異なるエンコーディングで書かれたテキストが含まれている場合)では、Unicodeを取得する唯一の方法が、一部の文字を特別なUnicode文字「置換文字」(U+FFFD, �)に置き換えることになる場合があります。Unicode, Dammitがこれを行う必要がある場合、 UnicodeDammit または BeautifulSoup オブジェクトの .contains_replacement_characters 属性が True に設定されます。これにより、Unicode表現が元の表現と完全に一致しないことを示し、いくつかのデータが失われたことを示します。もしドキュメントに�が含まれているが、 .contains_replacement_charactersFalse である場合、その�は元々そこに存在していたものであり、欠落したデータを表していないことがわかります(この段落ではそうです)。

出力エンコーディング

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, 'html.parser')
print(soup.prettify())
# <html>
#  <head>
#   <meta content="text/html; charset=utf-8" http-equiv="Content-type" />
#  </head>
#  <body>
#   <p>
#    Sacré bleu!
#   </p>
#  </body>
# </html>

この例では、ドキュメントがUTF-8になったことを反映して、<meta>タグが書き換えられていることに注目してください。

UTF-8以外のエンコーディングを希望する場合は、 prettify() にエンコーディングを指定することができます。:

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

また、 BeautifulSoup オブジェクトやsoup内の任意の要素に対して、Pythonの文字列のようにencode()を呼び出すこともできます。:

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

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

指定したエンコーディングで表現できない文字は、数値のXMLエンティティ参照に変換されます。以下は、Unicodeキャラクター SNOWMAN を含むドキュメントです。:

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

雪だるまのキャラクターはUTF-8のドキュメントの一部になれます( ☃ のように表示されます)が、ISO-Latin-1やASCIIでは表現する方法がないため、これらのエンコーディングでは “&#9731” に変換されます。:

print(tag.encode("utf-8"))
# b'<b>\xe2\x98\x83</b>'

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

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

Unicode, Dammit

(訳注: Unicodeにしろよ、チクショウ)

Unicode, Dammitは、Beautiful Soupを使わずに単独で使用することもできます。データのエンコーディングが不明な場合に、それを単にUnicodeに変換したいときに便利です。以下はその使用例です。:

from bs4 import UnicodeDammit
dammit = UnicodeDammit(b"\xc2\xabSacr\xc3\xa9 bleu!\xc2\xbb")
print(dammit.unicode_markup)
# «Sacré bleu!»
dammit.original_encoding
# 'utf-8'

Unicode, Dammitの推測精度は、以下のPythonライブラリのいずれかをインストールすることで大幅に向上します: charset-normalizerchardet、または 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つの特別な機能があります。

Smart quotes

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
# '<p>I just &ldquo;love&rdquo; Microsoft Word&rsquo;s smart quotes</p>'

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

また、Microsoft のスマートクオートを ASCII のクオートに変換することもできます:

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

この機能は役立つかもしれませんが、Beautiful Soup はこれを使用しません。Beautiful Soup は、Microsoft のスマートクオートを他の文字と一緒に Unicode 文字に変換するデフォルトの動作を好みます:

UnicodeDammit(markup, ["windows-1252"]).unicode_markup
# '<p>I just “love” Microsoft Word’s smart quotes</p>'

不一致なエンコーディング

時々、ドキュメントはほとんどがUTF-8で書かれているが、Windows-1252の文字(再びMicrosoftのスマートクオートなど)が含まれていることがあります。これは、ウェブサイトが複数のソースからデータを含む場合に発生することがあります。こうしたドキュメントを純粋なUTF-8に変換するために、 UnicodeDammit.detwingle() を使用できます。以下はその簡単な例です:

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としてデコードすると意味不明な文字が出力されます。幸いにも、 UnicodeDammit.detwingle() を使えば、文字列を純粋なUTF-8に変換でき、Unicodeにデコードしてスノーマンと引用符を同時に表示することができます:

new_doc = UnicodeDammit.detwingle(doc)
print(new_doc.decode("utf8"))
# ☃☃☃“I like snowmen!”

UnicodeDammit.detwingle() は、UTF-8に埋め込まれたWindows-1252(またはその逆)の処理のみを行うことができますが、これが最も一般的なケースです。

なお、データを BeautifulSoupUnicodeDammit コンストラクタに渡す前に、 UnicodeDammit.detwingle() を呼び出す必要があることに注意してください。Beautiful Soupは、ドキュメントがどのようなものであれ単一のエンコーディングを持つと仮定します。UTF-8とWindows-1252の両方を含むドキュメントを渡すと、ドキュメント全体がWindows-1252と認識され、 ☃☃☃“I like snowmen!” のように表示されてしまう可能性があります。

UnicodeDammit.detwingle() は、Beautiful Soup 4.1.0で新しく導入された機能です。

行番号

html.parserhtml5lib パーサーは、各タグが元のドキュメントのどこにあったかを追跡することができます。この情報には、 Tag.sourceline (行番号)と Tag.sourcepos (行内の開始タグの位置)としてアクセスできます:

markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
soup = BeautifulSoup(markup, 'html.parser')
for tag in soup.find_all('p'):
    print(repr((tag.sourceline, tag.sourcepos, tag.string)))
# (1, 0, 'Paragraph 1')
# (3, 4, 'Paragraph 2')

sourcelinesourcepos の意味は、パーサーによって若干異なることに注意してください。html.parserでは、これらの数字は最初の小なり記号の位置を示しています。html5libでは、これらの数字は最後の大なり記号の位置を示しています:

soup = BeautifulSoup(markup, 'html5lib')
for tag in soup.find_all('p'):
    print(repr((tag.sourceline, tag.sourcepos, tag.string)))
# (2, 0, 'Paragraph 1')
# (3, 6, 'Paragraph 2')

この機能を無効にするには、BeautifulSoup コンストラクタに store_line_numbers=False を渡します:

markup = "<p\n>Paragraph 1</p>\n    <p>Paragraph 2</p>"
soup = BeautifulSoup(markup, 'html.parser', store_line_numbers=False)
print(soup.p.sourceline)
# None

この機能は Beautiful Soup 4.8.1で新しく導入され、lxmlに基づくパーサーはこの機能をサポートしていません。

オブジェクトの等価性

Beautiful Soupでは、2つの NavigableString または Tag オブジェクトが同じHTMLまたはXMLマークアップを表している場合、それらは等しいと見なされます。この例では、異なるオブジェクトツリー内に存在していても、どちらも “<b>pizza</b>” というマークアップを持つため、2つの <b> タグは等しいと扱われます:

markup = "<p>I want <b>pizza</b> and more <b>pizza</b>!</p>"
soup = BeautifulSoup(markup, 'html.parser')
first_b, second_b = soup.find_all('b')
print(first_b == second_b)
# True

print(first_b.previous_element == second_b.previous_element)
# False

もし2つの変数が正確に同じオブジェクトを参照しているかを確認したい場合は、 is を使用します:

print(first_b is second_b)
# False

Beautiful Soupオブジェクトのコピー

任意の Tag または NavigableString をコピーするには、 copy.copy() を使用できます:

import copy
p_copy = copy.copy(soup.p)
print(p_copy)
# <p>I want <b>pizza</b> and more <b>pizza</b>!</p>

コピーは元のものと等しいと見なされますが、それは元のものと同じマークアップを表しているためです。ただし、同じオブジェクトではありません:

print(soup.p == p_copy)
# True

print(soup.p is p_copy)
# False

唯一の違いは、コピーが元のBeautiful Soupオブジェクトツリーから完全に切り離されていることです。これは、まるで extract() が呼び出されたかのように動作します:

print(p_copy.parent)
# None

これは、異なる2つの Tag オブジェクトが同じ場所に同時に存在できないためです。

パーサーの高度なカスタマイズ

Beautiful Soupでは、パーサーがHTMLやXMLをどのように処理するかをカスタマイズするためのいくつかの方法が提供されています。このセクションでは、最も一般的に使用されるカスタマイズ手法について説明します。

ドキュメントの一部だけを解析する

たとえば、Beautiful Soup を使ってドキュメントの <a> タグだけを見たいとします。ドキュメント全体を解析してから <a> タグを探すのは、時間とメモリの無駄です。最初から <a> タグ以外の部分を無視する方がはるかに高速です。 SoupStrainer クラスを使うと、読み込むドキュメントのうち、解析する部分を選ぶことができます。 SoupStrainer を作成し、それを BeautifulSoup コンストラクタに parse_only 引数として渡すだけです。

(注意: この機能は html5lib パーサーを使用している場合は動作しません。html5lib を使用すると、ドキュメント全体が必ず解析されます。これは、html5lib が作業中に解析木を絶えず再構成するためで、ドキュメントの一部が実際に解析木に含まれていないと、クラッシュする可能性があるからです。混乱を避けるために、以下の例では Beautiful Soup に Python の組み込みパーサーを使用させています。)

class bs4.SoupStrainer

SoupStrainer クラスは、 解析木を検索 の典型的なメソッドと同じ引数を取ります。具体的には、name, attrs, string, **kwargs です。以下に3つの SoupStrainer オブジェクトの例を示します:

from bs4 import SoupStrainer

only_a_tags = SoupStrainer("a")

only_tags_with_id_link2 = SoupStrainer(id="link2")

def is_short_string(string):
    return string is not None and len(string) < 10

only_short_strings = SoupStrainer(string=is_short_string)

再び “Three sisters” ドキュメントを使って、これら3つの SoupStrainer オブジェクトで文書を解析した結果がどうなるかを見てみましょう:

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>
"""

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, 'html.parser')
soup.find_all(only_short_strings)
# ['\n\n', '\n\n', 'Elsie', ',\n', 'Lacie', ' and\n', 'Tillie',
#  '\n\n', '...', '\n']

複数の値を持つ属性のカスタマイズ

HTMLドキュメントでは、 class のような属性は複数の値を持つリストとして扱われ、 id のような属性は単一の値を持つものとして扱われます。これは、HTMLの仕様でこれらの属性が異なる扱いをされるためです。

markup = ‘<a class=”cls1 cls2” id=”id1 id2”>’ soup = BeautifulSoup(markup, ‘html.parser’) soup.a[‘class’] # [‘cls1’, ‘cls2’] soup.a[‘id’] # ‘id1 id2’

これをオフにするには、multi_valued_attributes=None を渡します。そうすると、すべての属性が単一の値を持つようになります。

soup = BeautifulSoup(markup, ‘html.parser’, multi_valued_attributes=None) soup.a[‘class’] # ‘cls1 cls2’ soup.a[‘id’] # ‘id1 id2’

この動作をかなりカスタマイズすることも可能です。そのためには、 multi_valued_attributes に辞書を渡します。これが必要な場合は、HTMLTreeBuilder.DEFAULT_CDATA_LIST_ATTRIBUTES を確認してください。これは、Beautiful Soup がデフォルトで使用する設定であり、HTML の仕様に基づいています。

(この機能は Beautiful Soup 4.8.0 で新たに追加されました。)

重複した属性の処理

html.parser パーサーを使用する際、on_duplicate_attribute コンストラクタ引数を使って、タグが同じ属性を複数回定義した場合に Beautiful Soup がどのように処理するかをカスタマイズできます。:

markup = '<a href="http://url1/" href="http://url2/">'

デフォルトの動作は、最後に見つかった値をタグに使用することです。:

soup = BeautifulSoup(markup, 'html.parser')
soup.a['href']
# http://url2/

soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute='replace')
soup.a['href']
# http://url2/

on_duplicate_attribute='ignore' を使用すると、 最初に 見つかった値を使用し、残りを無視するように指定できます。

soup = BeautifulSoup(markup, ‘html.parser’, on_duplicate_attribute=’ignore’) soup.a[‘href’] # http://url1/

(lxml および html5lib は常にこの方法で処理します。この動作は Beautiful Soup 内からは変更できません。)

さらにカスタマイズが必要な場合は、重複する値ごとに呼び出される関数を渡すことができます。:

def accumulate(attributes_so_far, key, value):
    if not isinstance(attributes_so_far[key], list):
        attributes_so_far[key] = [attributes_so_far[key]]
    attributes_so_far[key].append(value)

soup = BeautifulSoup(markup, 'html.parser', on_duplicate_attribute=accumulate)
soup.a['href']
# ["http://url1/", "http://url2/"]

(この機能は Beautiful Soup 4.9.1 で新たに追加されました。)

カスタムサブクラスのインスタンス化

パーサーがタグや文字列について Beautiful Soup に伝えるとき、通常、Beautiful Soup はその情報を保持するために Tag または NavigableString オブジェクトをインスタンス化します。しかし、デフォルトの動作に代わり、 TagNavigableStringサブクラス をインスタンス化するように Beautiful Soup に指示することができます。これらのサブクラスはカスタムの動作を持つものとして定義できます。:

from bs4 import Tag, NavigableString
class MyTag(Tag):
    pass


class MyString(NavigableString):
    pass


markup = "<div>some text</div>"
soup = BeautifulSoup(markup, 'html.parser')
isinstance(soup.div, MyTag)
# False
isinstance(soup.div.string, MyString)
# False

my_classes = { Tag: MyTag, NavigableString: MyString }
soup = BeautifulSoup(markup, 'html.parser', element_classes=my_classes)
isinstance(soup.div, MyTag)
# True
isinstance(soup.div.string, MyString)
# True

この機能は、Beautiful Soup をテストフレームワークに組み込む際に便利です。

(この機能は Beautiful Soup 4.8.1 で新たに追加されました。)

トラブルシューティング

diagnose()

もし、Beautiful Soup がドキュメントに対して行っている処理を理解するのに苦労している場合は、ドキュメントを diagnose() 関数に渡してみてください。(この機能は Beautiful Soup 4.2.0 で新たに追加されました。)Beautiful Soup は、さまざまなパーサーがドキュメントをどのように処理するかを示すレポートを出力し、使用可能なパーサーが不足している場合はそれを教えてくれます。:

from bs4.diagnose import diagnose
with open("bad.html") as fp:
    data = fp.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つはクラッシュで、ドキュメントをBeautiful Soupに渡すと例外が発生し、通常は HTMLParser.HTMLParseError が発生する場合です。もう1つは、予期しない動作で、Beautiful Soupの解析木が、作成元のドキュメントと大きく異なる場合です。

これらの問題のほとんどは、Beautiful Soup自体の問題ではないことが多いです。これは、Beautiful Soupが非常に優れたソフトウェアであるからではなく、Beautiful Soup自体には解析コードが含まれていないからです。代わりに、外部のパーサーに依存しています。特定のドキュメントで1つのパーサーがうまく機能しない場合、最善の解決策は別のパーサーを試すことです。詳細およびパーサーの比較については、 パーサー(構文解析器)のインストール を参照してください。

最も一般的な解析エラーは、 HTMLParser.HTMLParseError: malformed start tagHTMLParser.HTMLParseError: bad end tag です。これらはどちらもPythonの組み込みHTMLパーサーライブラリによって生成されますが、解決策は lxml または html5lib をインストールすること です。

予期しない動作の最も一般的なタイプは、ドキュメント内にあることがわかっているタグが見つからない場合です。入力時にそのタグが見えていたのに、find_all()[] を返したり、 find()None を返したりすることがあります。これもまた、Pythonの組み込みHTMLパーサーの一般的な問題であり、時には理解できないタグをスキップすることがあります。この場合も、最善の解決策は lxml または html5lib をインストールすること です。

バージョン不一致の問題

  • SyntaxError: Invalid syntax``( ``ROOT_TAG_NAME = '[document]' 行にて発生): Python 3で、Python 2用の古いBeautiful Soupをコード変換せずに実行した場合に発生します。

  • ImportError: No module named HTMLParser - Python 3で、Python 2用の古いBeautiful Soupを実行した場合に発生します。

  • ImportError: No module named html.parser - Python 2で、Python 3用のBeautiful Soupを実行した場合に発生します。

  • ImportError: No module named BeautifulSoup - Beautiful Soup 3のコードを、BS3がインストールされていないシステムで実行した場合に発生します。または、Beautiful Soup 4のコードを記述する際に、パッケージ名が bs4 に変更されたことを知らない場合に発生します。

  • ImportError: No module named bs4 - Beautiful Soup 4のコードを、BS4がインストールされていないシステムで実行した場合に発生します。

XMLの解析

デフォルトでは、Beautiful Soupは文書をHTMLとして解析します。文書をXMLとして解析するには、 BeautifulSoup コンストラクタの2番目の引数に “xml” を渡します:

soup = BeautifulSoup(markup, "xml")

この操作を行うには、lxmlをインストールしておく必要があります

他のパーサーに関する問題

  • スクリプトがあるコンピュータでは動作するが、別のコンピュータでは動作しない場合、あるいは、ある仮想環境では動作するが別の仮想環境では動作しない、または仮想環境の外では動作するが中では動作しない場合、それはおそらく、2つの環境で使用可能なパーサーライブラリが異なるためです。たとえば、lxmlがインストールされているコンピュータでスクリプトを開発し、その後、html5libしかインストールされていないコンピュータで実行しようとした場合などです。この問題がなぜ重要かについては パーサー毎の違い を参照し、 BeautifulSoup コンストラクタに特定のパーサーライブラリを指定することで問題を修正してください。

  • HTMLタグや属性は大文字小文字を区別しない ため、3つのHTMLパーサーはすべて、タグと属性名を小文字に変換します。つまり、マークアップ <TAG></TAG> は <tag></tag> に変換されます。大文字小文字を混ぜたタグや属性を保持したい場合は、文書を XMLとして解析する 必要があります。

その他の問題

  • UnicodeEncodeError: 'charmap' codec can't encode character '\xfoo' in position bar (または他の UnicodeEncodeError ) - この問題は主に2つの状況で発生します。1つは、コンソールが表示方法を知らないUnicode文字を出力しようとしたときです。(この問題については Python wikiのこのページ を参照してください。)もう1つは、ファイルに書き込む際、デフォルトのエンコーディングでサポートされていないUnicode文字を渡したときです。この場合、最も簡単な解決策は、Unicode文字列を u.encode("utf8") を使って明示的にUTF-8にエンコードすることです。

  • KeyError: [attr] - タグが attr 属性を定義していない場合に、 tag['attr'] にアクセスしようとして発生します。最も一般的なエラーは KeyError: 'href'KeyError: 'class' です。 attr が定義されているか不明な場合は、Pythonの辞書と同様に tag.get('attr') を使用してください。

  • AttributeError: 'ResultSet' object has no attribute 'foo' - これは通常、 find_all() が単一のタグや文字列を返すと期待していた場合に発生します。しかし、 find_all() はタグや文字列の リスト`( ``ResultSet` オブジェクト)を返します。このリストを反復処理して、それぞれの .foo を確認する必要があります。または、本当に1つの結果だけが欲しい場合は、find_all() の代わりに find() を使用する必要があります。

  • AttributeError: 'NoneType' object has no attribute 'foo' - これは通常、 find() を呼び出してから、その結果の .foo 属性にアクセスしようとしたときに発生します。しかし、find() が何も見つけられなかった場合、タグや文字列の代わりに None を返します。なぜ find() が何も返さないのかを調べる必要があります。

  • AttributeError: 'NavigableString' object has no attribute 'foo' - これは通常、文字列をタグとして扱おうとしたときに発生します。リストを反復処理していて、その中にタグしかないと思っている場合に発生しますが、実際にはタグと文字列の両方が含まれています。

パフォーマンスの向上

Beautiful Soupは、その上に構築されているパーサーほど高速ではありません。もし応答時間が重要である場合、時間単位でコンピュータの使用料を支払っている場合、またはプログラマの時間よりもコンピュータの時間の方が価値があるような状況がある場合は、Beautiful Soupを使うのをやめて、 lxml を直接使用することをお勧めします。

とはいえ、Beautiful Soupのパフォーマンスを向上させるための方法もあります。もしlxmlを基盤として使用していないのであれば、 今すぐ始める のが良いでしょう。Beautiful Soupは、html.parserやhtml5libを使うよりも、lxmlを使ってドキュメントを解析する方がはるかに高速です。

エンコーディングの検出を大幅に高速化するには、 cchardet ライブラリをインストールすると良いでしょう。

ドキュメントの一部だけを解析する ことは、ドキュメントの解析自体の時間を大幅に短縮するわけではありませんが、メモリの節約にはなり、ドキュメントの 検索 もより高速になります。

このドキュメントの翻訳について

Beautiful Soupのドキュメントの新しい翻訳は大いに歓迎されます。翻訳は、Beautiful Soup本体およびその英語版ドキュメントと同様に、MITライセンスの下で公開される必要があります。

翻訳をBeautiful Soupのメインコードベースに取り込み、公式ウェブサイトに掲載するには、以下の2つの方法があります:

  1. Beautiful Soupリポジトリのブランチを作成し、翻訳を追加して、ソースコードの変更提案と同じようにメインブランチへのマージを提案します。

  2. Beautiful Soupのディスカッショングループにメッセージを送り、翻訳へのリンクを添付するか、翻訳そのものをメッセージに添付します。

翻訳の際には、中国語やブラジルポルトガル語の翻訳を参考にしてください。特に、ドキュメントのHTML版ではなく、ソースファイル doc/source/index.rst を翻訳してください。これにより、HTML以外のさまざまな形式でドキュメントを公開できるようになります。

Beautiful Soup 3

Beautiful Soup 3は、以前のリリースシリーズであり、現在は積極的に開発されていません。現在、主要なLinuxディストリビューションにパッケージとして提供されています:

$ apt-get install python-beautifulsoup

また、PyPiを通じて BeautifulSoup として公開されています。:

$ easy_install BeautifulSoup

$ pip install BeautifulSoup

さらに、 Beautiful Soup 3.2.0のtarballをダウンロードすることもできます

もし easy_install beautifulsoupeasy_install BeautifulSoup を実行したが、コードが動作しない場合、誤ってBeautiful Soup 3をインストールしてしまった可能性があります。その場合は、 easy_install beautifulsoup4 を実行する必要があります。

Beautiful Soup 3のドキュメントはオンラインにアーカイブされています

BS4へのコード移行

Beautiful Soup 3で書かれたコードは、ほとんどの場合、簡単な変更でBeautiful Soup 4に対応させることができます。必要なのは、パッケージ名を BeautifulSoup から bs4 に変更することだけです。例えば、このようにします:

from BeautifulSoup import BeautifulSoup

このように変更します:

from bs4 import BeautifulSoup
  • ImportError のエラー “No module named BeautifulSoup” が発生した場合、問題は、Beautiful Soup 3のコードを実行しようとしているが、インストールされているのはBeautiful Soup 4だけということです。

  • ImportError のエラー “No module named bs4” が発生した場合、問題は、Beautiful Soup 4のコードを実行しようとしているが、インストールされているのはBeautiful Soup 3だけということです。

BS4は基本的にBS3との互換性がありますが、 PEP8 に準拠するために多くのメソッドが非推奨となり、新しい名前に変更されています。また、他にも多くの名前変更や変更点があり、そのいくつかは後方互換性を破るものです。

ここでは、BS3のコードや慣習をBS4に変換するために知っておくべきことを説明します:

パーサーの利用

Beautiful Soup 3は、Pythonの SGMLParser モジュールを使用していましたが、このモジュールはPython 3.0で非推奨となり、削除されました。Beautiful Soup 4はデフォルトで html.parser を使用しますが、代わりにlxmlやhtml5libをプラグインとして利用することもできます。パーサーの比較については パーサー(構文解析器)のインストール を参照してください。

html.parserSGMLParser とは異なるパーサーであるため、同じマークアップに対してBeautiful Soup 3とは異なる解析木を生成する場合があります。さらに、 html.parser をlxmlやhtml5libに置き換えると、解析木がさらに変わることもあります。このような場合、新しい解析木に対応するために、スクレイピングコードを更新する必要があります。

メソッド名の変更など

以下のメソッド名が変更されました:

  • 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

  • getText -> get_text

  • nextSibling -> next_sibling

  • previousSibling -> previous_sibling

同様の理由で、Beautiful Soupのコンストラクタの引数もいくつか名前が変更されました:

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

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

Python 3との互換性のために1つのメソッドが変更されました:

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

より正確な用語を使用するために1つの属性が名前を変更されました:

  • Tag.isSelfClosing -> Tag.is_empty_element

Pythonで特別な意味を持つ言葉の使用を避けるために、3つの属性が名前を変更されました。これらの変更は他の変更と異なり、 後方互換性がありません。 BS3でこれらの属性を使用していた場合、BS4でコードが動かなくなりますので、変更が必要です。

  • UnicodeDammit.unicode -> UnicodeDammit.unicode_markup

  • Tag.next -> Tag.next_element

  • Tag.previous -> Tag.previous_element

以下のメソッドはBeautiful Soup 2のAPIから引き継がれたもので、2006年以降非推奨となっており、使用すべきではありません:

  • Tag.fetchNextSiblings

  • Tag.fetchPreviousSiblings

  • Tag.fetchPrevious

  • Tag.fetchPreviousSiblings

  • Tag.fetchParents

  • Tag.findChild

  • Tag.findChildren

ジェネレーターの変更

ジェネレーターにはPEP8に準拠した名前が付けられ、プロパティに変換されました:

  • childGenerator() -> children

  • nextGenerator() -> next_elements

  • nextSiblingGenerator() -> next_siblings

  • previousGenerator() -> previous_elements

  • previousSiblingGenerator() -> previous_siblings

  • recursiveChildGenerator() -> descendants

  • parentGenerator() -> parents

そのため、以下のコードを:

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

次のように書けるようになりました:

for parent in tag.parents:
    ...

(ただし、古いコードも依然として動作します。)

以前、いくつかのジェネレーターは終了後に None を返し、その後停止していましたが、これはバグでした。現在、ジェネレーターは単に停止します。

新しいジェネレーターが2つ追加されました。.strings と .stripped_strings です。 `.strings はNavigableStringオブジェクトを、 .stripped_strings は余分な空白を削除したPythonの文字列を返します。

XML

BeautifulStoneSoup クラスはXML解析のために使用されなくなりました。XMLを解析するには、 BeautifulSoup コンストラクタの第2引数に “xml” を渡します。同じ理由で、 BeautifulSoup コンストラクタはもう isHTML 引数を認識しません。

Beautiful Soupの空要素XMLタグの扱いが改善されました。以前は、XMLを解析する際にどのタグが空要素タグであるかを明示的に指定する必要がありましたが、 selfClosingTags 引数はもう認識されなくなりました。代わりに、Beautiful Soupは任意の空タグを空要素タグと見なします。空要素タグに子要素を追加すると、それは空要素タグではなくなります。

エンティティ

受信したHTMLやXMLのエンティティは常に対応するUnicode文字に変換されます。Beautiful Soup 3では、エンティティを処理するための重複した方法がいくつかありましたが、これらは削除されました。 BeautifulSoup コンストラクタは、もう smartQuotesToconvertEntities 引数を認識しません。( Unicode, Dammit には smart_quotes_to がまだありますが、デフォルトではスマートクォートをUnicodeに変換するようになりました。) HTML_ENTITIES, XML_ENTITIES, XHTML_ENTITIES といった定数は削除されました。これらは、すべてのエンティティをUnicode文字に変換する機能(すべてのエンティティをUnicode文字に変換するわけではない)を設定するためのものでしたが、この機能はもはや存在しません。

Unicode文字をHTMLエンティティに戻して出力したい場合は、それらをUTF-8文字に変換するのではなく、出力フォーマッタ を使用する必要があります。

その他

Tag.string は再帰的に動作するようになりました。もしタグAが単一のタグBのみを含み、それ以外に何も含んでいない場合、A.string は B.string と同じになります。(以前はNoneでした。)

class のような 複数の値を持つ属性 には、値として文字列ではなく文字列のリストが含まれます。これにより、CSSクラスで検索する方法が影響を受ける可能性があります。

Tag オブジェクトは、 __hash__ メソッドを実装しており、同じマークアップを生成する2つの Tag オブジェクトは等しいと見なされます。これにより、 Tag オブジェクトを辞書やセットに格納する際のスクリプトの動作が変わる可能性があります。

find* メソッドに string タグ固有の引数( name など)を同時に渡すと、Beautiful Soup はタグ固有の条件に一致し、その Tag.stringstring の値と一致するタグを検索します。文字列自体 は検索されません。以前は、Beautiful Soup はタグ固有の引数を無視し、文字列のみを検索していました。

BeautifulSoup コンストラクタは、もう markupMassage 引数を認識しません。マークアップを正しく処理するのはパーサーの責任です。

ほとんど使用されない代替パーサークラス(例えば ICantBelieveItsBeautifulSoupBeautifulSOAP )は削除されました。あいまいなマークアップの処理方法は、今後はパーサーに委ねられます。

prettify() メソッドは、これまでのバイト文字列ではなく、Unicode文字列を返すようになりました。