Pynote

Python、機械学習、画像処理について

スクレイピング - Beautiful Soup の find(), find_all() を使った要素の検索方法 まとめ

関連記事

pynote.hatenablog.com

ツリー構造の操作

以下の HTML をサンプルとして使用する。

sample.html

<html>
<head>
    <title>レストランメニュー</title>
</head>
<body>
    <h1>本日の<b>おすすめ</b>メニュー</h1>
    <ul class="menu">
        <li>さんまの塩焼き</li>
        <li>ハンバーグ定食</li>
        <li>田舎風<i>ミネストローネ</i></li>
    </ul>
    <p>メニューは日替わりです。</p>
    <hr>
    <p>サンプル画像</p>
    <p>
        <img src="sample.png" alt="サンプル画像"></p>
    <p>お気に入りに<a href="sample.com">Web site</a>を登録してください。</p>
</body>
</html>
import re
from bs4 import BeautifulSoup

with open('sample.html', encoding='utf8') as f:
    html = f.read()
    # DOM ツリーを簡単にするため、行頭の空白と改行は削除する。
    html = re.sub(r'^\s+', '', html, flags = re.MULTILINE).replace('\n', '')

soup = BeautifulSoup(html, 'lxml')

HTML の DOM ツリーは以下のようになる。

root (BeautifulSoup)
└── html (Tag)
    ├── head (Tag)
    │   └── title (Tag)
    │       └── 'レストランメニュー' (NavigableString)
    └── body (Tag)
        ├── h1 (Tag)
        │   ├── '本日の' (NavigableString)
        │   ├── b (Tag)
        │   │   └── 'おすすめ' (NavigableString)
        │   └── 'メニュー' (NavigableString)
        ├── ul (Tag)
        │   ├── li (Tag)
        │   │   └── 'さんまの塩焼き' (NavigableString)
        │   ├── li (Tag)
        │   │   └── 'ハンバーグ定食' (NavigableString)
        │   └── li (Tag)
        │       ├── '田舎風' (NavigableString)
        │       └── i (Tag)
        │           └── 'ミネストローネ' (NavigableString)
        ├── p (Tag)
        │   └── 'メニューは日替わりです。' (NavigableString)
        ├── hr (Tag)
        ├── p (Tag)
        │   └── 'サンプル画像' (NavigableString)
        ├── p (Tag)
        │   └── img (Tag)
        └── p (Tag)
            ├── 'お気に入りに' (NavigableString)
            ├── a (Tag)
            │   └── 'Web site' (NavigableString)
            └── 'を登録してください。' (NavigableString)

find_all()、find()

基本的な使い方

find_all() は、呼び出したノードの子孫ノードを検索し、指定した条件に合致するすべての要素を 'bs4.element.ResultSet' オブジェクトで返す。
このオブジェクトは各要素が bs4.element.Tag オブジェクトであるリスト [タグ1, タグ2, ...] であると考えてよい。

ret = soup.find_all('li')
print(ret)
# [<li>さんまの塩焼き</li>, <li>ハンバーグ定食</li>, <li>田舎風<i>ミネストローネ</i></li>]

# スライスなどインデックス操作もリスト同様に行える。
print(ret[0])  # <li>さんまの塩焼き</li>

print(ret[1:])  # [<li>ハンバーグ定食</li>, <li>田舎風<i>ミネストローネ</i></li>]

条件に合致する要素が1つしかなかった場合でも返り値は要素数が1のリストになる。

ret = soup.find_all('b')
print(ret)  # [<b>おすすめ</b>]

一方、find() は find_all() と引数の指定方法は同じだが、条件に合致する1番最初に見つかった要素を返す。

ret = soup.find('li')
print(ret)  # <li>さんまの塩焼き</li>

指定した名前の要素を取得する。

ret = soup.find_all('b')
print(ret) # [<b>おすすめ</b>]

ret = soup.find_all('p')
print(ret)
# [<p>メニューは日替わりです。</p>,
#  <p>サンプル画像</p>,
#  <p><img alt="サンプル画像" src="sample.png"/></p>,
#  <p>お気に入りに<a href="sample.com">Web site</a>を登録してください。</p>]

要素名をリストで複数指定できる。

ret = soup.find_all(['b', 'li'])
print(ret)
# [<b>おすすめ</b>, <li>さんまの塩焼き</li>, <li>ハンバーグ定食</li>, <li>田舎風<i>ミネストローネ</i></li>]

正規表現に合致する名前の要素を取得する。

import re

# 名前が h から始まる要素を取得する。
for tag in soup.find_all(re.compile("^h")):
    print(tag.name, end=' ')
# html head h1 hr

すべての要素を取得する。

True を指定することで、NavigableString 以外のすべての要素を取得する。

# NavigableString を除くすべての要素を取得する。
for tag in soup.find_all(True):
    print(tag.name, end=' ')
# html head title body h1 b ul li li li i p hr p p img p a 

要素名の条件を定義する関数を使用する。

html = '''
<p class="a hoge">A</p>
<p class="hoge">B</p>
<p class="hoge" id="fuga">C</p>
<p class="a hoge fizz">D</p>
<p>E</p>
<p>F</p>
'''.replace('\n', '')

soup = BeautifulSoup(html, 'lxml')
# 属性 class を持ち、属性 id は持たない要素の場合は True、
# そうでない場合は False を返す。
def has_class_but_no_id(elem):
    return elem.has_attr('class') and not elem.has_attr('id')

for elem in soup.find_all(has_class_but_no_id):
    print(elem)
# <p class="a hoge">A</p>
# <p class="hoge">B</p>
# <p class="a hoge fizz">D</p>

指定した属性を持つ要素を取得する。

引数に 属性名=値 を指定することで、その属性を持つ要素を取得する。
ただし、属性 class を指定する際、class は Python予約語なので、代わりに class_ を使用する。

# class='hoge' を持つ要素を取得する。
for tag in soup.find_all('p', class_='hoge'):
    print(tag)
# <p class="a hoge">A</p>
# <p class="hoge">B</p>
# <p class="hoge" id="fuga">C</p>
# <p class="a hoge fizz">D</p>

複数の属性を指定する。

# 属性 class='hoge' 及び id='fuga' を持つ要素を取得する。
for tag in soup.find_all('p', class_='hoge', id='fuga'):
    print(tag)
# <p class="hoge" id="fuga">C</p>

正規表現で指定する。

# class の属性値が h から始まる要素を取得する。
for tag in soup.find_all('p', class_=re.compile(r'^h')):
    print(tag)
# <p class="a hoge">A</p>
# <p class="hoge">B</p>
# <p class="hoge" id="fuga">C</p>
# <p class="a hoge fizz">D</p>

属性値の指定を True とすることで、その属性を持つ要素を取得する。

# 属性 class を持つ要素を取得する。
for tag in soup.find_all(class_=True):
    print(tag)
# <p class="a hoge">A</p>
# <p class="hoge">B</p>
# <p class="hoge" id="fuga">C</p>
# <p class="a hoge fizz">D</p>

属性名の条件を定義する関数を指定できる。

# 値が空でなく、長さが4である属性の場合は True、
# そうでない場合は False を返す。
def has_four_characters(class_name):
    return class_name is not None and len(class_name) == 4

for tag in soup.find_all(class_=has_four_characters):
    print(tag)
# <p class="a hoge">A</p>
# <p class="hoge">B</p>
# <p class="hoge" id="fuga">C</p>
# <p class="a hoge fizz">D</p>

attrs 引数でも属性の条件を指定できる。

# 属性 class='hoge' 及び id='fuga' を持つ要素を取得する。
for tag in soup.find_all('p', attrs={'class': 'hoge', 'id': 'fuga'}):
    print(tag)
# <p class="hoge" id="fuga">C</p>

指定した値を持つ要素を取得する。

string 引数のみ指定した場合は、指定した値をもつ NavigableString を返す。
name 引数で要素名も指定した場合、指定した値を .string 属性に持つ要素を返す。

# 値が 'Apple' である NavigableString を取得する。
ret = soup.find_all(string='Apple')
print(ret)  # ['Apple']

# 値が 'Apple' で名前が 'p' である要素を取得する。
ret = soup.find_all('p', string='Apple')
print(ret)  # [<p class="a hoge">Apple</p>]
html = '''
<p class="a hoge">A</p>
<p class="hoge">B</p>
<p class="hoge" id="fuga">C</p>
<p class="a hoge fizz">D</p>
<p>E</p>
<p>F</p>
'''.replace('\n', '')

soup = BeautifulSoup(html, 'lxml')

# 値が 'Apple' である要素を取得する。
ret = soup.find_all('p', string='Apple')
print(ret)  # [<p class="a hoge">Apple</p>]

# 値が 'Apple' または 'Banana' である要素を取得する。
ret = soup.find_all('p', string=['Apple', 'Banana'])
print(ret)
# [<p class="a hoge">Apple</p>,
#  <p class="hoge">Banana</p>]

# 値が 'P' で始まる要素を取得する。
ret = soup.find_all('p', string=re.compile(r'^P'))
print(ret)
# [<p class="hoge" id="fuga">Plum</p>,
#  <p class="a hoge fizz">Peach</p>]

# 値の長さが4である要素を取得する。
def has_4_length_string(value):
    return len(value) == 4

ret = soup.find_all('p', string=has_4_length_string)
print(ret)  # [<p class="hoge" id="fuga">Plum</p>]

返り値の要素数の上限を指定する。

limit 引数で上限を設定できる。

print(soup.find_all('p', limit=3))
# [<p class="a hoge">A</p>, <p class="hoge">B</p>, <p class="hoge" id="fuga">C</p>]

再帰的に検索する深さを指定する。

デフォルトでは、find_all() を呼び出した要素の子孫ノードをすべて検索するが、recursive=False を指定した場合はその子のみ検索対象とする。

ret = soup.find_all('h1')
print(ret) # [<h1>本日の<b>おすすめ</b>メニュー</h1>]

ret = soup.find_all('h1', recursive=False)
print(ret) # []

タグオブジェクトの呼び出しメソッド

bs4.BeautifulSoup オブジェクトまたは bs4.element.Tag オブジェクトの呼び出しメソッドは、find_all() と同じ意味になる。

ret = soup.find_all('h1')
print(ret) # [<h1>本日の<b>おすすめ</b>メニュー</h1>]

ret = soup('h1')
print(ret) # [<h1>本日の<b>おすすめ</b>メニュー</h1>]

ret = soup.html('h1')
print(ret) # [<h1>本日の<b>おすすめ</b>メニュー</h1>]

find_all() の類似メソッド

  • find_parents(): 呼び出した要素の先祖要素を検索し、見つかった要素の一覧を返す。
  • find_parent(): 呼び出した要素の先祖要素を検索し、最初に見つかった要素を返す。
  • find_next_siblings(): 呼び出した要素より後の兄弟要素を検索し、見つかった要素の一覧を返す。
  • find_next_sibling(): 呼び出した要素より後の兄弟要素を検索し、最初に見つかった要素を返す。
  • find_previous_siblings(): 呼び出した要素より前の兄弟要素を検索し、見つかった要素の一覧を返す。
  • find_previous_sibling(): 呼び出した要素より前の兄弟要素を検索し、最初に見つかった要素を返す。
  • find_all_next(): HTML 上で呼び出した要素より後にある要素を検索し、見つかった要素の一覧を返す。
  • find_next(): HTML 上で呼び出した要素より後にある要素を検索し、最初に見つかった要素を返す。
  • find_all_previous(): HTML 上で呼び出した要素より前にある要素を検索し、見つかった要素の一覧を返す。
  • find_previous(): HTML 上で呼び出した要素より前にある要素を検索し、最初に見つかった要素を返す。