Pythonを使用したウェブスクレイピングにおいて、特定の要素を起点にしてその「隣にある要素」を取得したい場面は頻繁に発生します。
例えば、見出しタグの直後にある段落を取得したり、表形式のデータで特定のラベルに対応する値を取得したりする場合です。
BeautifulSoupライブラリには、このような操作を直感的に行うためのnext_siblingというプロパティが用意されています。
しかし、初心者がnext_siblingを使用する際に必ずと言っていいほど直面するのが、「期待した要素ではなく、空白や改行が取得されてしまう」という問題です。
本記事では、BeautifulSoupにおける要素の階層構造の仕組みを紐解きながら、next_siblingの正しい使い方と、空白ノードを回避して確実に目的のデータを取得するための代替手法について詳しく解説します。
BeautifulSoupとnext_siblingの基本知識
BeautifulSoupでHTMLを解析すると、解析されたデータは「解析ツリー(Parse Tree)」と呼ばれる階層構造で管理されます。
このツリー構造において、同じ親要素を持つ要素同士を「兄弟(Sibling)」と呼びます。
兄弟関係とは何か
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は、ターゲットとなる要素の直後にあるノードを取得するためのプロパティです。
基本的な使い方は非常にシンプルで、要素オブジェクトに対してドット記法でアクセスするだけです。
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内の改行やインデント(空白)にあります。
NavigableStringという落とし穴
BeautifulSoupは、HTML内のタグだけでなく、タグとタグの間にある文字列も「ノード」として扱います。
これをNavigableStringと呼びます。
一般的なHTMLは、人間が読みやすいように改行やスペースで整えられています。
<ul>
<li id="item1">アイテム1</li>
<li id="item2">アイテム2</li>
</ul>
一見すると、id="item1"の次の兄弟はid="item2"のように見えます。
しかし、BeautifulSoupの内部的なツリー構造では、id="item1"の直後には「改行とインデント(\n )」というテキストノードが存在しているのです。
空白ノードを取得してしまう例
以下のコードを実行して、実際にどのような挙動になるか確認してみましょう。
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()というメソッドが用意されています。
これは「次の兄弟の中で、指定した条件に合致する最初のタグ」を検索するメソッドです。
条件を指定せずに呼び出すと、直後のテキストノード(空白など)を自動的にスキップし、最初に見つかった「タグ」を返してくれます。
# 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オブジェクトかどうかを判定することで、空白ノードを除外できます。
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(値)を取得したいケースが多くあります。
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)内の特定の行の次を取得
表の中で、特定のキーワードを含む行を見つけ、その次の行にあるデータを取得する例です。
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_element | HTMLソース内で次に現れる要素 | 深さ優先探索。子要素があればその中へ入る。 |
例えば、<div><p>テキスト</p></div> という構造で、divに対して操作を行う場合:
div.next_siblingは、divタグが終わった後の次の要素を探します。div.next_elementは、divの直下にあるpタグを探します。
「隣の要素」を取得したいという目的であれば、基本的にはnext_sibling(あるいはfind_next_sibling)を選択するのが正解です。
スクレイピングの効率を高めるためのアドバイス
next_siblingを活用する際は、「取得した要素がNoneでないか」を常にチェックするガード句を入れることをお勧めします。
スクレイピング対象のサイトがリニューアルされたり、ページによって構造が異なっていたりする場合、エラーでプログラムが停止するのを防ぐためです。
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()でも、隣接兄弟結合子(+)を使用することで同様の操作が可能です。
# 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構造からでも狙ったデータをピンポイントで取得できるようになります。
構造化されていないデータを意味のある情報へと変換するスクレイピングにおいて、兄弟要素の操作は必須のスキルです。
ぜひ日々の開発に取り入れてみてください。
