Webスクレイピングにおいて、特定の要素を起点にしてその親要素や先祖要素へと遡って情報を取得する手法は非常に重要です。
例えば、表(テーブル)の中にある特定のテキストを見つけ、そのテキストが含まれる行(trタグ)全体を操作したい場合や、ネストされた複雑なレイアウトから目的のコンテナ要素を特定したい場合に、親方向への移動が必要になります。
PythonのスクレイピングライブラリであるBeautifulSoupには、要素を遡るための直感的なメソッドや属性が複数用意されています。
本記事では、.parent、.parents、さらにはフィルタリングが可能な.find_parent()などの使い分けについて、具体的なコード例を交えながら詳しく解説します。
BeautifulSoupにおけるツリー構造と親方向の移動
HTMLドキュメントは、タグが階層状に重なったツリー構造として表現されます。
ある要素を基準にしたとき、その一つ上の階層にある要素を「親要素」、さらにその上にあるすべての要素を「先祖要素」と呼びます。
スクレイピングの実務では、子要素(例えば特定のアイコンやキーワード)を先に特定し、そこから遡って親要素が持つIDやクラス情報を取得するケースが多々あります。
BeautifulSoupでは、DOMツリーを下方向(子・孫)に辿るだけでなく、上方向(親・先祖)へも自由自在に移動できる仕組みが備わっています。
まずは、親方向へ遡るための代表的な4つのアプローチを整理しましょう。
| 属性・メソッド | 取得対象 | 特徴 |
|---|---|---|
.parent | 直近の親要素(1つ) | 属性としてアクセスし、すぐ上の親を返す |
.parents | すべての先祖要素 | ジェネレータ形式で、最上位まで順に遡る |
.find_parent() | 条件に合う直近の親 | 条件(タグ名やクラス)を指定して検索できる |
.find_parents() | 条件に合うすべての先祖 | 条件に一致する先祖をリスト形式で取得する |
これらの違いを理解し、適切に使い分けることで、効率的で壊れにくいスクレイピングコードを記述できるようになります。
直近の親要素を取得する .parent 属性
もっともシンプルに親要素へアクセスする方法が、.parent 属性を使用する方法です。
これは、指定した要素の真上の階層にある要素を1つだけ返します。
.parent の基本的な使い方
以下のサンプルコードでは、リストアイテム(li)からその親であるリストタグ(ul)を取得します。
from bs4 import BeautifulSoup
html_content = """
<div class="container">
<ul id="target-list">
<li id="item-1">Python</li>
<li id="item-2">BeautifulSoup</li>
</ul>
</div>
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 特定の要素(liタグ)を取得
child_element = soup.find(id="item-1")
# .parent で親要素(ulタグ)を取得
parent_element = child_element.parent
print(f"子要素のID: {child_element.get('id')}")
print(f"親要素のタグ名: {parent_element.name}")
print(f"親要素のID: {parent_element.get('id')}")
子要素のID: item-1
親要素のタグ名: ul
親要素のID: target-list
注意点:NavigableString と [document]
.parent を使用する際に留意すべき点が2つあります。
1つ目は、要素内のテキスト部分(NavigableString)の親も、そのテキストを囲んでいるタグになるという点です。
2つ目は、HTMLの最上位要素である <html> の親は [document] オブジェクトになり、さらに [document] の親は None になる点です。
ループ処理などで親を遡り続ける場合は、None に到達してエラーが発生しないよう、条件分岐を行う必要があります。
すべての先祖要素を遡る .parents 属性
直近の親だけでなく、さらにその上の要素(祖父要素など)をすべて辿りたい場合には、.parents を使用します。
.parents はイテレータ(ジェネレータ)を返します。
そのため、for 文などで順番に要素を取り出す処理に適しています。
.parents による再帰的な取得
以下の例では、特定のリンクタグから最上位の html タグまでを順番に表示します。
from bs4 import BeautifulSoup
html_doc = """
<html>
<body>
<div class="wrapper">
<main class="content">
<p><a href="#" id="start-node">Click here</a></p>
</main>
</div>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
link = soup.find(id="start-node")
# すべての先祖要素を順番に表示
print("--- 先祖要素を遡ります ---")
for ancestor in link.parents:
if ancestor is not None:
# [document]は名前が[document]になるため、タグ名のみを表示
print(f"Tag: {ancestor.name}")
--- 先祖要素を遡ります ---
Tag: p
Tag: main
Tag: content
Tag: div
Tag: body
Tag: html
Tag: [document]
このように、.parents を使うと、起点となる要素から外側に向かって順番に階層を探索することができます。
特定のクラス名を持つ先祖要素が見つかるまで遡る、といったロジックを実装する際に非常に便利です。
条件を指定して親を探す .find_parent() メソッド
.parent は機械的に1つ上の要素を返しますが、実務では「数段上のどこかにある、特定のクラスを持った div タグが欲しい」というケースが多いでしょう。
そのような場合には、.find_parent() メソッドが最適です。
このメソッドは、soup.find() と同様の引数(タグ名、属性、正規表現、関数など)を受け取り、条件に合致する最も近い先祖要素を1つ返します。
find_parent() の実践的な例
例えば、複雑な広告枠や記事リストの中で、特定の「ボタン」からその記事全体の「カード型コンテナ」を取得したい場合に役立ちます。
from bs4 import BeautifulSoup
html_complex = """
<article class="post-card" id="post-101">
<div class="post-header">
<h2>記事タイトル</h2>
</div>
<div class="post-body">
<p>記事の概要文...</p>
<div class="button-wrapper">
<button class="btn-detail">詳細を見る</button>
</div>
</div>
</article>
"""
soup = BeautifulSoup(html_complex, 'html.parser')
button = soup.find("button", class_="btn-detail")
# buttonの数段上にある article.post-card を直接探す
article_container = button.find_parent("article", class_="post-card")
if article_container:
print(f"コンテナのID: {article_container.get('id')}")
print(f"コンテナのタグ: {article_container.name}")
コンテナのID: post-101
コンテナのタグ: article
もし .parent だけで同じことをしようとすると、button.parent.parent.parent のように記述しなければならず、HTML構造が少し変わっただけでコードが壊れてしまいます。
.find_parent() を使うことで、構造の変化に強い、堅牢なスクレイピングが可能になります。
条件に合うすべての先祖を取得する .find_parents()
.find_parent() が1つだけ(最も近い要素)を返すのに対し、.find_parents() は条件に合うすべての先祖要素をリストとして返します。
これは、入れ子構造になっているカテゴリリストや、特定の属性を持つ親要素をすべて抽出したい場合に利用されます。
from bs4 import BeautifulSoup
html_nested = """
<div class="category-tree" data-level="root">
<div class="category-item" data-level="1">
<div class="category-item" data-level="2">
<span id="target-span">現在のカテゴリ</span>
</div>
</div>
</div>
"""
soup = BeautifulSoup(html_nested, 'html.parser')
span = soup.find(id="target-span")
# すべての "category-item" クラスを持つ親を遡って取得
ancestors = span.find_parents("div", class_="category-item")
print(f"見つかった先祖要素の数: {len(ancestors)}")
for idx, parent in enumerate(ancestors):
print(f"{idx + 1}番目の親のレベル: {parent.get('data-level')}")
見つかった先祖要素の数: 2
1番目の親のレベル: 2
2番目の親のレベル: 1
実践例:テーブルデータから親要素を特定して情報を抜き出す
スクレイピングの現場で非常によくある課題が、「特定のキーワードが含まれるセル(td)を見つけ、その行(tr)にある他のデータを取得する」というものです。
ここでは、社員名簿のようなテーブルから、「名前」をキーにして「部署名」を取得するコードを作成してみましょう。
from bs4 import BeautifulSoup
table_html = """
<table border="1">
<thead>
<tr><th>ID</th><th>名前</th><th>部署</th></tr>
</thead>
<tbody>
<tr><td>001</td><td>山田 太郎</td><td>営業部</td></tr>
<tr><td>002</td><td>佐藤 花子</td><td>開発部</td></tr>
<tr><td>003</td><td>鈴木 一郎</td><td>人事部</td></tr>
</tbody>
</table>
"""
soup = BeautifulSoup(table_html, 'html.parser')
def get_department_by_name(target_name):
# まずは名前が書かれているセル(td)を探す
name_cell = soup.find("td", string=target_name)
if not name_cell:
return "見つかりませんでした"
# 1. tdの親である tr を取得
target_row = name_cell.parent
# 2. そのtrの中にある3番目のtd(部署)を取得
# find_all("td") でリスト化してアクセス
cells = target_row.find_all("td")
department = cells[2].get_text()
return department
# 実行
search_name = "佐藤 花子"
result = get_department_by_name(search_name)
print(f"{search_name}さんの部署: {result}")
佐藤 花子さんの部署: 開発部
この手法のポイントは、まずユニークな値(この場合は氏名)を起点にすることです。
その後の name_cell.parent によって「その人が含まれる行全体」にアクセス範囲を広げ、再び子要素を探すことで、行内の他の情報を安全に取得できます。
逆引き:どちらを使うべきか迷った時のガイドライン
「親に遡る」ための方法が複数あるため、どれを使うべきか迷うことがあるかもしれません。
以下のガイドラインを参考にしてください。
1. 1つ上の親であることが確実な場合
HTMLの構造がシンプルで、span のすぐ上が必ず div である、といった場合は .parent を使用するのが最も高速でコードも簡潔です。
2. ターゲットの親要素が何段階上にあるか不明な場合
例えば「何らかのタグに囲まれているが、間に strong や em が挟まっているかもしれない」という場合は、.find_parent("div") を使いましょう。
これにより、間のタグを無視して目的のタグが見つかるまで遡ってくれます。
3. 特定の条件を満たす要素まで遡り続けたい場合
「id 属性を持っている要素に到達するまで遡りたい」といった動的な条件の場合は、.parents を使ってループを回し、if 文で判定を行う手法が有効です。
4. BeautifulSoup4の新機能やモダンな書き方
Pythonの最新バージョン(2026年時点)においても、BeautifulSoup4の基本的なAPIは安定していますが、cssセレクタを用いた .select() メソッドは親方向への探索(例:CSSの:has()擬似クラスのような挙動)には直接対応していません。
そのため、親方向への移動に関しては、依然として .parent 系メソッドが最強のツールです。
注意すべき落とし穴とエラー回避
親要素を辿る際に発生しやすいトラブルとその対策について解説します。
NoneType エラー
もっとも頻発するのが「親を探したが見つからなかった(Noneを返した)」にもかかわらず、その結果に対してさらにメソッドを呼び出してしまうケースです。
# 危険なコード
# target が見つからなかった場合、target.parent でエラーになる
# また、target.parent が None の場合、さらに .parent を呼ぶと AttributeError
target = soup.find("div", class_="non-existent")
grand_parent = target.parent.parent
これを防ぐためには、以下のように存在チェックを行うのが鉄則です。
target = soup.find("div", class_="non-existent")
if target and target.parent:
parent = target.parent
# 処理を継続
NavigableString(空白・改行)の親
BeautifulSoupでHTMLをパースした際、タグとタグの間の改行や空白が NavigableString として扱われることがあります。
特定の要素の子要素から親を辿る際、自分が今「タグ」を扱っているのか「文字列オブジェクト」を扱っているのかを意識することは、意図しない挙動を防ぐために重要です。
基本的にタグの親はタグですが、ルート付近では注意が必要です。
まとめ
PythonのBeautifulSoupを利用したスクレイピングにおいて、親要素へ遡る操作は、情報のコンテキストを正確に把握するために不可欠です。
- .parent:直近の親を素早く取得。
- .parents:先祖要素をルートまで順番に走査。
- .find_parent():特定の条件に合う先祖要素をピンポイントで取得。
これらを駆使することで、HTML構造が複雑なサイトであっても、目的のデータに最短ルートでアクセスできるようになります。
特に、キーワードを起点にその周辺データ(行全体やコンテナ全体)を抜き出す手法は、実務での利用頻度が非常に高いため、ぜひマスターしておきましょう。
スクレイピングを行う際は、常に「起点となる要素」から「目的のデータが含まれる親要素」までの距離と条件を冷静に分析し、もっとも変化に強い(壊れにくい)メソッドを選択することを心がけてください。
