Webスクレイピングを行う際、HTML要素のclass属性が動的に変化したり、複雑な文字列が組み合わさっていたりするケースが増えています。
Beautiful Soupの標準的な機能である完全一致検索だけでは、こうした複雑な構造を持つWebサイトから効率的にデータを抽出することは困難です。
モダンなフロントエンドフレームワークで生成されたサイトでは、class名の一部がランダムな値になったり、特定の規則性を持った接尾辞が付与されたりすることが一般的だからです。
この記事では、Beautiful Soupを用いてclass属性を部分一致や正規表現、さらに動的な条件で抽出する具体的な手法を詳しく紹介します。
Beautiful Soupにおける基本的なclass指定の限界
Beautiful Soupで要素を取得する際、最も一般的な方法はfindやfind_allメソッドにclass_引数を渡す手法です。
この手法は、指定した文字列が完全に一致するクラスを持つ要素を探す場合には非常に有効です。
しかし、対象のHTMLが「item-12345」や「button_active_01」といった動的なクラス名を持っている場合、完全一致ではヒットしません。
このような動的な要素を柔軟に捉えるためには、部分一致やパターンマッチングのテクニックを習得する必要があります。
正規表現を利用したclass属性の部分一致抽出
Beautiful Soupのメソッドには、文字列の代わりにPythonの標準ライブラリであるre(正規表現)モジュールを渡すことができます。
正規表現を利用することで、「特定の文字から始まる」「特定の文字で終わる」「特定の単語を含む」といった柔軟な検索が可能になります。
re.compileによるパターンマッチング
まずは、正規表現オブジェクトを生成するre.compileを使用した例を見てみましょう。
以下のコードは、クラス名に「card-」という文字列が含まれている全てのdiv要素を抽出する例です。
import re
from bs4 import BeautifulSoup
html = """
<div class="card-wrapper">コンテンツ1</div>
<div class="card-container">コンテンツ2</div>
<div class="main-content">コンテンツ3</div>
"""
soup = BeautifulSoup(html, 'html.parser')
# class名に "card-" が含まれる要素を抽出
elements = soup.find_all("div", class_=re.compile("card-"))
for el in elements:
print(el.text)
コンテンツ1
コンテンツ2
このように、正規表現を使うことで部分的に一致するクラス名を持つ要素を一括で取得できます。
前方一致・後方一致の指定方法
正規表現の記号を活用することで、より厳密な条件指定も可能です。
クラス名の先頭が特定の文字列であるか、あるいは末尾が特定の数値であるかといった条件を使い分けましょう。
| 条件 | 正規表現のパターン | 説明 |
|---|---|---|
| 前方一致 | re.compile("^prefix-") | 「prefix-」で始まるクラスを抽出します。 |
| 後方一致 | re.compile("-suffix$") | 「-suffix」で終わるクラスを抽出します。 |
| 部分一致(任意) | re.compile("keyword") | 「keyword」を含むクラスを抽出します。 |
この表にあるパターンをfind_allに適用することで、サイトの更新によってクラス名が微妙に変化しても安定して抽出を続けることができます。
CSSセレクタによる属性フィルタリング
Beautiful Soupには、selectメソッドを使用してCSSセレクタ形式で要素を探す機能も備わっています。
実はCSSセレクタ自体に、属性の部分一致を判定する強力な構文が含まれています。
正規表現をインポートせずに、直感的な記法で部分一致を記述できるのが大きなメリットです。
属性セレクタの特殊記号
CSSセレクタでは、属性名の後に特定の記号を付加することで、マッチングのルールを変更できます。
以下のコード例では、selectメソッドを用いて複数の条件で要素を抽出しています。
# クラス名が "test" で始まる要素 (前方一致)
starts_with = soup.select('div[class^="test"]')
# クラス名が "active" で終わる要素 (後方一致)
ends_with = soup.select('div[class$="active"]')
# クラス名に "module" が含まれる要素 (部分一致)
contains = soup.select('div[class*="module"]')
これらの記法は、フロントエンド開発者がスタイルを当てる際に使用する標準的なCSSと同じであるため、直感的に理解しやすいのが特徴です。
特に「特定の命名規則に従った動的クラス」をターゲットにする場合に非常に強力なツールとなります。
Lambda(ラムダ式)を用いた高度な動的抽出
さらに複雑な条件でクラスを抽出したい場合は、関数(ラムダ式)を引数に渡す手法が有効です。
Beautiful Soupは、引数に関数が指定された場合、その関数にタグオブジェクトを渡し、戻り値がTrueとなる要素のみを返します。
複数の条件を組み合わせたフィルタリング
例えば、「クラス名に特定の文字を含み、かつ特定の文字は含まない」といった条件や、「クラスの文字数が一定以上」といった条件を自由に記述できます。
# クラス名の長さが10文字以上で、かつ "btn" を含む要素を抽出
elements = soup.find_all(lambda tag: tag.name == "div" and
tag.has_attr('class') and
any("btn" in c and len(c) > 10 for c in tag.get('class')))
for el in elements:
print(f"見つかった要素: {el.text}")
Beautiful Soupにおいて、class属性はリスト形式で保持される点に注意してください。
そのため、tag.get('class')で取得したリストに対して、any()やリスト内包表記を用いることで、各クラス名に対して詳細なチェックを行うことが可能です。
ラムダ式を活用することで、定型的なメソッドでは対応できない独自のロジックをスクレイピングに組み込むことができます。
実戦:動的なクラス名を持つWebサイトの攻略
最近のWebサイト、特にReactやVue.jsなどで構築されたサイトでは、ビルド時にクラス名がheader_3f2a1のようにハッシュ化されることがあります。
このようなサイトをスクレイピングする場合、不変な部分である「header_」のみを頼りに要素を特定する必要があります。
実践的なコード例
以下のコードは、ハッシュ値が付加されたクラス名から、必要なデータのみを抜き出す実践的なアプローチです。
html_snippet = """
<section class="container">
<div class="product_item_a8f92">商品A</div>
<div class="product_item_b7e11">商品B</div>
<div class="footer_info">フッター</div>
</section>
"""
soup = BeautifulSoup(html_snippet, 'html.parser')
# 「product_item_」から始まるクラスを持つ要素をすべて抽出
products = soup.find_all(class_=re.compile(r"^product_item_"))
for product in products:
print(f"取得データ: {product.get_text().strip()}")
取得データ: 商品A
取得データ: 商品B
このように、変化しないプレフィックス(接頭辞)を特定し、正規表現の^(先頭一致)を組み合わせるのが最も確実な対策となります。
まとめ
Beautiful Soupで動的なクラス名や複雑な属性を扱うためには、複数のアプローチを使い分けることが重要です。
単純なキーワード一致であればre.compileによる正規表現が手軽で強力です。
一方で、直感的な記述を好む場合やフロントエンドの知識がある場合は、CSSセレクタの属性検索記法が最適でしょう。
さらに複雑な論理条件が必要な場面では、ラムダ式を用いたカスタムフィルタ関数を作成することで、ほぼ全ての動的要素に対応可能です。
ターゲットとするWebサイトの構造に合わせて、これらのテクニックを柔軟に組み合わせてみてください。
効率的な要素抽出は、スクレイピングコードの保守性を高め、サイトの構造変更に強い堅牢なプログラム作成に繋がります。
