閉じる

BeautifulSoupのnext_siblingで次の要素を取得する方法|空白ノードの回避策まで解説

Pythonを使用したウェブスクレイピングにおいて、特定の要素を起点にしてその「隣にある要素」を取得したい場面は頻繁に発生します。

例えば、見出しタグの直後にある段落を取得したり、表形式のデータで特定のラベルに対応する値を取得したりする場合です。

BeautifulSoupライブラリには、このような操作を直感的に行うためのnext_siblingというプロパティが用意されています。

しかし、初心者がnext_siblingを使用する際に必ずと言っていいほど直面するのが、「期待した要素ではなく、空白や改行が取得されてしまう」という問題です。

本記事では、BeautifulSoupにおける要素の階層構造の仕組みを紐解きながら、next_siblingの正しい使い方と、空白ノードを回避して確実に目的のデータを取得するための代替手法について詳しく解説します。

BeautifulSoupとnext_siblingの基本知識

BeautifulSoupでHTMLを解析すると、解析されたデータは「解析ツリー(Parse Tree)」と呼ばれる階層構造で管理されます。

このツリー構造において、同じ親要素を持つ要素同士を「兄弟(Sibling)」と呼びます。

兄弟関係とは何か

HTMLの構造において、兄弟関係を理解することは非常に重要です。

以下のコード例を見てみましょう。

HTML
<div>
    <p id="first">一番目の段落</p>
    <p id="second">二番目の段落</p>
    <p id="third">三番目の段落</p>
</div>

この例では、div要素が親であり、その中にある3つのp要素はすべて同じ階層に位置する兄弟です。

id="first"の要素から見て、id="second"は「次の兄弟(next sibling)」であり、逆にid="second"から見てid="first"は「前の兄弟(previous sibling)」となります。

next_siblingプロパティの役割

next_siblingは、ターゲットとなる要素の直後にあるノードを取得するためのプロパティです。

基本的な使い方は非常にシンプルで、要素オブジェクトに対してドット記法でアクセスするだけです。

Python
from bs4 import BeautifulSoup

html_content = '<div><p id="a">A</p><p id="b">B</p></div>'
soup = BeautifulSoup(html_content, 'html.parser')

first_p = soup.find('p', id='a')
next_node = first_p.next_sibling

print(f"現在の要素: {first_p.text}")
print(f"次の要素: {next_node.text}")
実行結果
現在の要素: A
次の要素: B

この例のように、HTMLソースに改行やスペースが含まれていない場合は、期待通りに次のタグを取得できます。

しかし、実際のウェブサイトのソースコードでこのような書き方をされていることは稀であり、ここからが注意が必要なポイントとなります。

next_siblingがうまく動かない原因:空白ノードの正体

多くの開発者が「next_siblingを使っても、空の結果しか返ってこない」という壁にぶつかります。

その原因は、HTML内の改行やインデント(空白)にあります。

BeautifulSoupは、HTML内のタグだけでなく、タグとタグの間にある文字列も「ノード」として扱います。

これをNavigableStringと呼びます。

一般的なHTMLは、人間が読みやすいように改行やスペースで整えられています。

HTML
<ul>
    <li id="item1">アイテム1</li>
    <li id="item2">アイテム2</li>
</ul>

一見すると、id="item1"の次の兄弟はid="item2"のように見えます。

しかし、BeautifulSoupの内部的なツリー構造では、id="item1"の直後には「改行とインデント(\n )」というテキストノードが存在しているのです。

空白ノードを取得してしまう例

以下のコードを実行して、実際にどのような挙動になるか確認してみましょう。

Python
from bs4 import BeautifulSoup

html = """
<ul>
    <li id="target">ターゲット</li>
    <li>次の要素のはず</li>
</ul>
"""
soup = BeautifulSoup(html, 'html.parser')
target = soup.find(id='target')

# next_siblingで次の要素を取得しようとする
result = target.next_sibling

print(f"取得された結果の型: {type(result)}")
print(f"取得された結果の内容: {repr(result)}")
実行結果
取得された結果の型: <class 'bs4.element.NavigableString'>
取得された結果の内容: '\n    '

このように、next_siblingはタグではなく、その直後の改行コードを取得してしまいます。

これが原因で、「.textを呼び出しても何も表示されない」といったトラブルが発生するのです。

次の要素(タグ)を確実に取得する3つの回避策

空白ノード問題を回避し、確実に「次のタグ要素」を取得するためには、以下の3つのアプローチが有効です。

1. find_next_sibling() メソッドを使用する(推奨)

BeautifulSoupには、単なるプロパティとしてのnext_sibling以外に、find_next_sibling()というメソッドが用意されています。

これは「次の兄弟の中で、指定した条件に合致する最初のタグ」を検索するメソッドです。

条件を指定せずに呼び出すと、直後のテキストノード(空白など)を自動的にスキップし、最初に見つかった「タグ」を返してくれます。

Python
# find_next_sibling() の使用例
target = soup.find(id='target')
next_tag = target.find_next_sibling()

print(f"取得されたタグ: {next_tag}")
print(f"テキスト内容: {next_tag.get_text()}")
実行結果
取得されたタグ: <li>次の要素のはず</li>
テキスト内容: 次の要素のはず

このメソッドは、引数にタグ名(例:'p')やクラス名(例:class_='my-class')を指定して、より詳細なフィルタリングを行うことも可能です。

2. next_siblings でループ処理を行う

特定の要素以降にある複数の兄弟要素をすべて取得したい場合は、next_siblings(末尾にsが付く)を使用します。

これはイテレータを返すため、for文などで順番に処理できます。

その際、Tagオブジェクトかどうかを判定することで、空白ノードを除外できます。

Python
from bs4 import BeautifulSoup, Tag

# next_siblings で全ての兄弟をチェック
for sibling in target.next_siblings:
    if isinstance(sibling, Tag):
        print(f"タグを発見: {sibling.name}")

3. next_sibling を2回重ねる(非推奨)

構造が完全に固定されており、必ず「タグ、改行、タグ」の順に並んでいることが保証されている場合に限り、target.next_sibling.next_siblingとすることで次のタグにアクセスできます。

しかし、HTMLのわずかな変更で壊れやすいため、実務では避けるべき手法です。

実践:具体的なHTML構造からのデータ抽出例

ここからは、実際のスクレイピングでよく遭遇するHTML構造を例に、具体的な抽出コードを見ていきましょう。

定義リスト(dl, dt, dd)からの抽出

製品スペックや用語集などでよく使われるdlタグの構造では、dt(項目名)の直後にあるdd(値)を取得したいケースが多くあります。

Python
html_dl = """
<dl>
    <dt>メーカー</dt>
    <dd>Python株式会社</dd>
    <dt>発売日</dt>
    <dd>2026年5月</dd>
</dl>
"""
soup_dl = BeautifulSoup(html_dl, 'html.parser')

# 「メーカー」の値を抽出する
label = soup_dl.find('dt', string='メーカー')
value = label.find_next_sibling('dd')

print(f"{label.text}: {value.text}")
実行結果
メーカー: Python株式会社

テーブル(tr)内の特定の行の次を取得

表の中で、特定のキーワードを含む行を見つけ、その次の行にあるデータを取得する例です。

Python
html_table = """
<table>
    <tr class="header"><th>項目</th><th>内容</th></tr>
    <tr id="row-target"><td>ステータス</td><td>稼働中</td></tr>
    <tr><td>備考</td><td>特記事項なし</td></tr>
</table>
"""
soup_table = BeautifulSoup(html_table, 'html.parser')
current_row = soup_table.find('tr', id='row-target')

# 次の行を取得
next_row = current_row.find_next_sibling('tr')
print(f"次の行のデータ: {next_row.td.text}")
実行結果
次の行のデータ: 備考

next_sibling と next_element の違いを正しく理解する

BeautifulSoupには似たような名前のプロパティとしてnext_elementが存在します。

これらは混同されやすいですが、探索のアルゴリズムが全く異なります

以下の表に違いをまとめました。

プロパティ名探索対象特徴
next_sibling同じ親を持つ次の要素横方向の移動。兄弟関係を維持する。
next_elementHTMLソース内で次に現れる要素深さ優先探索。子要素があればその中へ入る。

例えば、<div><p>テキスト</p></div> という構造で、divに対して操作を行う場合:

  • div.next_sibling は、divタグが終わった後の次の要素を探します。
  • div.next_element は、divの直下にあるpタグを探します。

「隣の要素」を取得したいという目的であれば、基本的にはnext_sibling(あるいはfind_next_sibling)を選択するのが正解です。

スクレイピングの効率を高めるためのアドバイス

next_siblingを活用する際は、「取得した要素がNoneでないか」を常にチェックするガード句を入れることをお勧めします。

スクレイピング対象のサイトがリニューアルされたり、ページによって構造が異なっていたりする場合、エラーでプログラムが停止するのを防ぐためです。

Python
target = soup.find('h2', string='重要なお知らせ')
if target:
    content = target.find_next_sibling('p')
    if content:
        print(content.get_text())
    else:
        print("次の段落が見つかりません。")
else:
    print("ターゲットの見出しが見つかりません。")

また、CSSセレクタを用いたselect_one()でも、隣接兄弟結合子(+)を使用することで同様の操作が可能です。

Python
# CSSセレクタでの例
# h2の直後のpを取得
content = soup.select_one('h2.title + p')

特定の状況においては、find_next_sibling()よりもCSSセレクタの方が記述が簡潔になる場合もあります。

状況に応じて使い分けられるようになると、スクレイピングのスキルは一段と向上します。

まとめ

BeautifulSoupのnext_siblingは、HTMLの兄弟関係を利用してデータを効率的に抽出するための強力な武器になります。

しかし、ソースコード上の改行や空白がNavigableStringとしてノードに含まれるため、単純なプロパティアクセスだけでは失敗することが多々あります。

確実性を求めるのであれば、以下のポイントを意識してください。

  • 単純なnext_siblingではなく、find_next_sibling()を優先的に使用する。
  • 複数の要素を連続して処理する場合は、next_siblingsと型チェックを組み合わせる。
  • 階層を深く潜ってしまうnext_elementとの違いを正しく理解する。

これらのテクニックを駆使することで、複雑なHTML構造からでも狙ったデータをピンポイントで取得できるようになります。

構造化されていないデータを意味のある情報へと変換するスクレイピングにおいて、兄弟要素の操作は必須のスキルです。

ぜひ日々の開発に取り入れてみてください。

URLをコピーしました!