PythonでWebスクレイピングを行う際、HTMLの階層構造を効率的にたどることは非常に重要です。
BeautifulSoupライブラリには、特定の要素から下位の要素へとアクセスするための便利なプロパティがいくつか用意されています。
本記事では、その中でも特に強力な「descendants」属性に焦点を当て、その仕組みと活用方法を詳しく解説します。
HTML要素を再帰的に取得し、複雑な構造から必要な情報を抜き出すテクニックを学びましょう。
descendants属性とは?子孫要素の概念を理解する
BeautifulSoupにおけるdescendants属性は、特定のタグ内にあるすべての要素(子孫要素)を再帰的に取得するためのジェネレータです。
HTMLのツリー構造において、「子要素(children)」が直下の階層のみを指すのに対し、「子孫要素(descendants)」はそのさらに下の階層にあるすべてのタグや文字列を含みます。
例えば、<div>の中に<p>があり、その中に<span>がある場合、<div>にとって<p>は子要素であり、<p>と<span>の両方が子孫要素となります。
この属性を使用することで、ネスト(入れ子)がどれほど深くても、一つのループで全要素を順番に走査することが可能になります。
複雑なウェブサイトの解析において、特定のコンテナ内にあるテキストやリンクを漏れなく抽出したい場合に非常に役立ちます。
HTMLツリーにおける階層関係
HTMLはドキュメント・オブジェクト・モデル(DOM)と呼ばれるツリー構造で構成されています。
親要素から見て直接ぶら下がっている要素を子要素(Children)と呼びます。
子要素の中にさらに含まれる要素も含めたすべての下位要素を子孫要素(Descendants)と呼びます。
スクレイピングの実務では、この「直近の子」だけを見るか、「末端まですべて」を見るかの使い分けが重要になります。
children属性とdescendants属性の決定的な違い
BeautifulSoupで要素を走査する際、最も混同しやすいのがchildrenとdescendantsの違いです。
children属性は、指定した要素の「直下」にある要素のみをリストのような形式で返します。
一方でdescendants属性は、「直下」だけでなく、その先の孫要素、曾孫要素までをすべて掘り下げて取得します。
以下のコード例で、その挙動の差を確認してみましょう。
from bs4 import BeautifulSoup
# サンプルのHTML構造
html_content = """
<div id="parent">
<p>第一階層のパラグラフ<span>第二階層のスパン</span></p>
<ul>
<li>アイテム1</li>
<li>アイテム2</li>
</ul>
</div>
"""
soup = BeautifulSoup(html_content, 'html.parser')
parent_div = soup.find(id="parent")
# children(子要素)のカウント
print(f"childrenの数: {len(list(parent_div.children))}")
# descendants(子孫要素)のカウント
print(f"descendantsの数: {len(list(parent_div.descendants))}")
childrenの数: 5
descendantsの数: 13
実行結果を見ると、取得される要素の数に大きな開きがあることがわかります。
数値が予想より多く感じるのは、BeautifulSoupが改行や空白も「NavigableString」として一つの要素としてカウントするためです。
childrenはdiv直下のp、ul、およびその間の改行文字のみを拾います。
対してdescendantsは、pの中のspanや、ulの中の各li、さらにはそれらが保持するテキストまですべてを列挙します。
使い分けの比較表
どちらを使うべきか迷った際は、以下の表を参考にしてください。
| 属性名 | 取得範囲 | 主な用途 |
|---|---|---|
| children | 直下の要素のみ | リストの項目(li)やテーブルの行(tr)など、構造が決まった直下要素を処理する場合 |
| descendants | すべての配下要素 | 複雑なネスト構造の中から特定の条件に合う要素をすべて探し出したい場合 |
descendantsを使って子孫要素をループ処理する方法
descendantsはジェネレータを返すため、forループに直接指定して効率的に要素を走査できます。
すべての要素を順番に処理する基本的な実装を見ていきましょう。
# すべての子孫要素を表示する
for element in parent_div.descendants:
# 要素の型を確認(タグか文字列か)
print(f"要素: {repr(element)}")
このループでは、タグ(Tagオブジェクト)だけでなく、テキスト部分(NavigableStringオブジェクト)も個別に取得されます。
スクレイピングの実務においては、テキストだけを除外したい、あるいは特定のタグだけを処理したいというケースがほとんどです。
特定のタグ(Tag型)のみを抽出する
ループ内でbs4.element.Tagインスタンスかどうかを判定することで、純粋なHTMLタグのみを操作できます。
以下のコードは、子孫要素の中からタグだけを抽出してその名前を表示する例です。
from bs4 import Tag
for element in parent_div.descendants:
if isinstance(element, Tag):
print(f"Tag見つかりました: {element.name}")
このように条件分岐を加えることで、不要な空白文字や改行を除去しながら目的のデータにアクセスできます。
実践的なユースケース:複雑な情報の抽出
実際のWebサイトでは、情報が多層のdivやsectionに隠れていることがよくあります。
descendantsを使用すれば、それらの階層を一つずつ手動で辿ることなく、一気に内部をスキャンできます。
再帰的なテキスト抽出
特定のエリアに含まれるすべてのテキストを、構造を無視してリスト化したい場合に便利です。
texts = []
for element in parent_div.descendants:
if not isinstance(element, Tag):
# 空白文字を除去して中身がある場合のみ追加
text = element.strip()
if text:
texts.append(text)
print(texts)
['第一階層のパラグラフ', '第二階層のスパン', 'アイテム1', 'アイテム2']
.get_text()メソッドを使えば同様のことができますが、descendantsによるループ処理は「テキストがどの要素に含まれているか」を判定しながら処理できるため、より柔軟な制御が可能です。
descendants利用時の注意点とパフォーマンス
非常に大規模なHTMLファイルを扱う場合、descendantsの使用には注意が必要です。
descendantsは再帰的にすべての要素を走査するため、DOMツリーが巨大な場合、ループの回数が膨大になり処理時間が長くなる可能性があります。
また、要素を書き換えながらループを回すと、予期しない挙動を引き起こすことがあるため、変更を伴う場合はリストに一度格納してから処理するのが安全です。
特定のIDやクラス名が既に判明している場合は、descendantsで全走査するよりも、find_allメソッドで直接ターゲットを絞り込む方が効率的です。
あくまで「構造が不規則で、すべての階層を調べる必要があるとき」に活用するのがベストな戦略です。
まとめ
BeautifulSoupのdescendants属性は、HTMLの深層構造までを一度に走査できる非常に強力なツールです。
children属性が「点」で要素を捉えるのに対し、descendants属性は「面(深さ全体)」で要素を捉えるイメージです。
ループ処理とisinstanceによるフィルタリングを組み合わせることで、複雑なサイトからも効率的にデータを抽出できるようになります。
スクレイピングの対象となるサイトの構造に応じて、これら2つの属性を適切に使い分けましょう。
今回紹介した基本をマスターすれば、ネストの深いモダンなWebサイトの解析もスムーズに行えるようになるはずです。
