Python PR

【Python】Beautiful Soupを使ってHTMLやXMLなどのデータを抽出する

記事内に商品プロモーションを含む場合があります

この記事では、Beautiful Soupを使ったデータの抽出を解説します。

環境
  • Beautiful Soup 4
  • Python 2.7、または Python 3.2以降

LinkBeautiful Soup: We called him Tortoise because he taught us.

インストール

Beautiful Soupは標準モジュールではないのでインストールが必要です。以下のコマンドをターミナルで実行することでインストールできます。

pip install beautifulsoup4

解析に使用するパーサーもインストールします。lxmlがお勧めされています。

pip install lxml

スープの作成

Beautiful Soupを使用するには、まず以下のようにbs4.BeautifulSoupをインポートし、

from bs4 import BeautifulSoup

BeautifulSoup()にHTMLドキュメントを渡してインスタンスを生成します。

soup = BeautifulSoup(doc, "lxml")

指定するドキュメントは文字列でもファイルオブジェクトでも可能です。

# 文字列を指定
doc = '''<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <title>ブラウザタブに表示される文字</title>
</head>
<body>
 <h1>ページタイトル</h1>
 <p>あい<b>うえ</b>お</p>
</body>
</html>'''

soup = BeautifulSoup(doc, "lxml")

# ファイルを指定
f = open('BeautifulSoup/index.html')
soup = BeautifulSoup(f, "lxml")

使用するパーサーは明示的に指定しなくても適したものが選択されますが、実行時に以下のような警告が表示されます。

/Python/BeautifulSoup/main.py:21: GuessedAtParserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("lxml"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.

The code that caused this warning is on line 21 of the file /Python/BeautifulSoup/main.py. To get rid of this warning, pass the additional argument 'features="lxml"' to the BeautifulSoup constructor.

  soup = BeautifulSoup(f)

別の環境で実行した際に異なった挙動をすることがあるので明示的に指定した方が安全です。

扱うオブジェクト

Beautiful Soupでは、以下の4つのオブジェクトを扱います。

  • BeautifulSoup
  • Tag
  • NavigableString
  • Comment

BeautifulSoup

スープの作成で生成したオブジェクト。解析したドキュメントがツリー状に収められている。

# スープの生成
soup = BeautifulSoup('<html>あいうえお</html>', "lxml")

print(type(soup))
# <class 'bs4.BeautifulSoup'>

Tag

Tagは、ドキュメント内のXMLやHTMLのタグに対応したオブジェクトです。

# スープの生成
soup = BeautifulSoup('<p class="underline">あいうえお</p>', "lxml")

# タグをタグ名で取得
tag = soup.p

# タグのタイプ
print(type(tag))  # <class 'bs4.element.Tag'>

名前

name属性でタグ名を取得できます。

# タグをタグ名で取得
tag = soup.p
print(tag.name)  # p

タグを書き換えることもできます。

tag.name = "b"
print(tag)
# <b class="underline">あいうえお</b>

属性

変数名の後ろの[]内に属性名を文字列で指定することで属性を取得できる。

print(tag['class'])
# ['underline']

NavigableString

タグに挟まれている文字列は、NavigableStringというクラスで表現されます。タグの内側の文字列はstring属性で取得できます。

print(tag.string)
# あいうえお

print(type(tag.string))
# <class 'bs4.element.NavigableString'>

replace_with()を使うことで文字列を置換可能。

tag.string.replace_with("abc")
print(tag)
# <p class="underline">abc</p>

抽出した文字列をBeautiful Soup外で使いたい場合はただの文字列に変換する。

string = tag.string
print(type(string))
# <class 'bs4.element.NavigableString'>

s = str(string)
print(type(s))
# <class 'str'>

Commentなど

特別な書式で表現される CDataProcessingInstructionDeclarationDoctypeComment クラスは、NavigableStringのサブクラスとして定義されています。

soup = BeautifulSoup('<p><!--コメント--></p>', "lxml")

comment = soup.p.string
print(comment, type(comment))
# コメント <class 'bs4.element.Comment'>

パースツリーの探索

以下のようなコードを用いてパースツリーを探索する方法を解説します。

from bs4 import BeautifulSoup

doc = '''<!DOCTYPE html>
<html lang="ja">
<head>
 <meta charset="UTF-8">
 <title>ブラウザタブに表示される文字</title>
</head>
<body>
 <h1>ページタイトル</h1>
 <p>あい<b>うえ</b>お</p>
</body>
</html>'''

soup = BeautifulSoup(doc, "lxml")

子要素の取得

様々な方法で子要素を取得することができます。

タグの取得

取得したいタグを属性名として指定することで最初に見つけたそのタグの要素を取得できます。

print(soup.body)
# <body>
# <h1>ページタイトル</h1>
# <p>あい<b>うえ</b>お</p>
# </body>

タグ名を繋げて子要素を取得することが可能です。

print(soup.body.p)
# <p>あい<b>うえ</b>お</p>

子要素をリストで取得

contents属性で子要素をリストで取得できます。

print(soup.body.contents)
# ['\n', <h1>ページタイトル</h1>, '\n', <p>あい<b>うえ</b>お</p>, '\n']

タグに囲まれていない要素(コンテンツ)は、NavigableStringというPythonのUnicode型のような型となるので注意してください。

p = soup.body.contents[3]
print(p.contents)
# ['あい', <b>うえ</b>, 'お']

子要素をイテレーターとして取得

childrenジェネレーターで子要素をイテレーターとして取得できます。

for child in soup.body.children:
    print(child, end='')
#
# <h1>ページタイトル</h1>
# <p>あい<b>うえ</b>お</p>

全ての子要素を再帰的に取得する

descendants属性でタグの全ての子要素を再帰的に取得することができます。

for child in soup.p.descendants:
    print(child)
# あい
# <b>うえ</b>
# うえ
# お

要素の内容の取得

子要素を1つしか持っていない、かつ その子要素がNavigableStringオブジェクトの場合、string属性で要素の内容を取得可能です。

print(soup.h1.string)
# ページタイトル

子要素が複数存在する場合はNoneが返される。

print(soup.p.string)
# None

全ての要素の内容の取得

タグの全ての要素の内容を取得するにはstringsジェネレーターを使います。

for content in soup.strings:
    print(content, end='')
#
#
#
# ブラウザタブに表示される文字
#
#
# ページタイトル
# あいうえお
#

余分な空白や改行が邪魔な場合は、stripped_stringsジェネレーターを使うと良い。

for content in soup.stripped_strings:
    print(content)
# ブラウザタブに表示される文字
# ページタイトル
# あい
# うえ
# お

親要素の取得

様々な方法で親要素を取得することができます。

親要素の取得

parent属性で親要素を取得することができます。

print(soup.title.parent)
# <head>
# <meta charset="utf-8"/>
# <title>ブラウザタブに表示される文字</title>
# </head>

<html>タグの様なトップレベルのタグの親はBeautifulSoupオブジェクトとなります。

print(type(soup.html))
# <class 'bs4.element.Tag'>

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

BeautifulSoupオブジェクトのparentNoneとなります。

print(soup.parent)
# None

全ての親要素を取得

全ての親要素を取得するにはparentsジェネレーターを使います。以下のコードではname属性を用いて取得した親要素のタグ名のみを出力しています。

for parent in soup.title.parents:
    if parent is None:
        print(parent)
    else:
        print(parent.name)
# head
# html
# [document]

兄弟要素の取得

様々な方法で兄弟要素を取得することができます。

兄弟要素の取得

next_sibling属性で同じレベルの次の要素を取得できる。ただし、改行も1つの要素として取得されるので注意してください。

print(repr(soup.h1.next_sibling))
# '\n'

print(soup.h1.next_sibling.next_sibling)
# <p>あい<b>うえ</b>お</p>

previous_sibling属性で同じレベルの前の要素を取得できる。こちらも改行が1つの要素として取得される。

print(repr(soup.p.previous_sibling))
# '\n'

print(soup.p.previous_sibling.previous_sibling)
# <h1>ページタイトル</h1>

パースツリーの検索

Beautiful Soupに定義されているメソッドを使って要素を検索することができます。結構な数のメソッドが定義されていますが引数はほとんど同じです。

ここからは以下のようなコードを使っていきます。

from bs4 import BeautifulSoup

doc = '''<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>ブラウザタブに表示される文字</title>
    </head>
    <body>
        <h1 id="page_title">ページタイトル</h1>
        <p class="red">あいうえお</p>
        <p class="blue">かきくけこ</p>
        <h2 id="toc1">見出し1</h2>
        <p class="red">さし<span class="bold">すせ</span>そ</p>
        <h2 id="toc2">見出し2</h2>
        <p>たちつてと</p>
        <h2 id="toc3">見出し3</h2>
        <p>なにぬねの</p>
    </body>
</html>'''

# スープの生成
soup = BeautifulSoup(doc, "lxml")

メソッド

Beautiful Soupには以下のようなメソッドが定義されています。

子要素の検索

find_all()メソッドは、子要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

find_all(name, attrs, recursive, text, limit, **kwargs)

find()メソッドは、子要素の中から引数で指定した条件に合致する最初の要素を返します。

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

親要素の検索

find_parents()メソッドは、親要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

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

find_parent()メソッドは、親要素の中から引数で指定した条件に合致する最初の要素を返します。

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

後方の兄弟要素の検索

find_next_siblings()メソッドは、後方にある兄弟要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

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

find_next_sibling()メソッドは、後方にある兄弟要素の中から引数で指定した条件に合致する最初の要素を返します。

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

前方の兄弟要素の検索

find_previous_siblings()メソッドは、前方にある兄弟要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

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

find_previous_sibling()メソッドは、前方にある兄弟要素の中から引数で指定した条件に合致する最初の要素を返します。

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

後方の要素

find_all_next()メソッドは、後方にある要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

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

find_next()メソッドは、後方にある要素の中から引数で指定した条件に合致する最初の要素を返します。

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

前方の要素

find_all_previous()メソッドは、前方にある要素の中から引数で指定した条件に合致する全ての要素を検索し、リストとして返します。

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

find_previous()メソッドは、前方にある要素の中から引数で指定した条件に合致する最初の要素を返します。

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

フィルターの種類

渡せるフィルターは以下の通りです。

文字列

検索メソッドに文字列を渡すと一致するタグを返します。

# 文字列
print(soup.find_all('h2'))
# [<h2 id="toc1">見出し1</h2>, <h2 id="toc2">見出し2</h2>, <h2 id="toc3">見出し3</h2>]

正規表現

正規表現で条件を指定することもできます。以下では、「h」が入っているタグとマッチします。

# 正規表現
import re
for tag in soup.find_all(re.compile('h')):
    print(tag.name)
# html
# head
# h1
# h2
# h2
# h2

リスト

複数の条件を指定したい場合はリストで指定できます。

# リスト
print(soup.find_all(['h1', 'h2']))
# [<h1 id="page_title">ページタイトル</h1>, <h2 id="toc1">見出し1</h2>, <h2 id="toc2">見出し2</h2>, <h2 id="toc3">見出し3</h2>]

True

Trueを指定すると全ての要素にマッチする。

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

関数

定義した関数を指定することができます。指定する関数は引数に要素を取り、マッチさせたい時にTrue、そうでない時にFalseを返す必要があります。

def method(tag):
    # class属性を持つp要素
    return tag.has_attr('class') and tag.name == "p"

print(soup.find_all(method))
# [<p class="red">あいうえお</p>, <p class="blue">かきくけこ</p>, <p class="red">さし<span class="bold">すせ</span>そ</p>]

引数

検索メソッドに定義されている引数について見ていきましょう!

name引数

name引数に文字列を指定することでその名前のタグを検索できます。

# h2要素の取得
print(soup.find_all('h2'))
# [<h2 id="toc1">見出し1</h2>, <h2 id="toc2">見出し2</h2>, <h2 id="toc3">見出し3</h2>]

キーワード引数

キーワード引数を指定することで属性を検索することができる。

# id = "toc2" の要素
print(soup.find(id="toc2"))
# <h2 id="toc2">見出し2</h2>

キーワード引数の値としてTrueを指定することでその属性に値が入っている全てのタグを検索します。

# id = "何らかの値" の要素
print(soup.find_all(id=True))
# [<h1 id="page_title">ページタイトル</h1>, <h2 id="toc1">見出し1</h2>, <h2 id="toc2">見出し2</h2>, <h2 id="toc3">見出し3</h2>]

classで検索したい場合はclass_を指定します。

# class = "red" の要素
print(soup.find_all(class_="red"))
# [<p class="red">あいうえお</p>, <p class="red">さし<span class="bold">すせ</span>そ</p>]

また、attrs引数に辞書として指定することも可能です。

# class = "red" の要素
print(soup.find_all(attrs={"class": "red"}))
# [<p class="red">あいうえお</p>, <p class="red">さし<span class="bold">すせ</span>そ</p>]

string引数

string引数に文字列を指定することでタグに挟まれている文字列を検索することができます。

# class = "red" の要素
print(soup.find_all(string="かきくけこ"))
# ['かきくけこ']

タグとして受け取りたい場合は、タグ名を指定すると良い。

tag = soup.find_all("p", string="かきくけこ")
print(type(tag[0]))
# <class 'bs4.element.Tag'>

limit引数

find_all()などの検索結果を全てを取得するメソッドにはlimitという引数が定義されている。limit引数に整数を指定することで取得する要素の最大数を設定できる。

# 検索結果を2個だけ取得する
print(soup.find_all("p", limit=2))
# [<p class="red">あいうえお</p>, <p class="blue">かきくけこ</p>]

recursive引数

find_all()find()メソッドは、直下の子要素だけでなく、さらに深い子要素(子要素の子要素など)まで検索するようになっています。

直下の子要素のみ検索したい場合はrecursive引数にFalseを指定します。

# 直下より深い子要素も検索
print(soup.find_all("h1"))
# [<h1 id="page_title">ページタイトル</h1>]

# 直下の子要素のみ検索
print(soup.find_all("h1", recursive=False))
# []

その他

ショートカット

find_all()はよく使用されるメソッドなのでショートカットが用意されています。Beautiful Soup、またはTagを関数のように呼び出すことでfind_all()メソッドとして実行できます。

soup.find_all("p")
soup("p")

CSSセレクタ

TagオブジェクトやBeautifulSoupオブジェクトに定義されているselect()メソッドを使うことでCSSセレクタを使って要素を取得すことができます。

以下のコードでは、h1の次のp要素のみ取得します。

print(soup.select("h1 + p"))
# [<p class="red">あいうえお</p>]

まとめ

この記事では、BeautifulSoupの使い方を解説しました。

軽く解説しようと思ったら1万文字ぐらいのボリュームになってしまいました。機能は多いですが難しいことはないので是非是非使ってみてください。

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ