PythonでWebスクレイピングを行う際、多くのエンジニアが愛用しているライブラリがBeautiful Soupです。
HTML要素を抽出する手法はいくつか存在しますが、中でもCSSセレクタを利用したselectメソッドは非常に強力です。
特定の属性を持つ要素をピンポイントで取得したい場合、完全一致だけでなく部分一致の指定が必要になる場面が多々あります。
この記事では、Beautiful SoupのCSSセレクタにおいて、属性の前方一致、後方一致、および中間一致を活用する方法を詳しく解説します。
Beautiful SoupにおけるCSSセレクタの基本
Beautiful Soupには、要素を探すための主なメソッドとしてfindやfind_all、そしてselectがあります。
selectメソッドは、Webフロントエンドの開発でお馴染みのCSSセレクタ記法をそのまま利用できる点が最大の特徴です。
複雑な階層構造を持つHTMLから目的のデータを抜き出す場合、CSSセレクタを使うとコードが簡潔になり、可読性が向上します。
特に、IDやクラス名が動的に生成される現代的なWebサイトでは、属性の一部だけを頼りに要素を特定する技術が不可欠です。
Beautiful Soupは標準でCSSセレクタをサポートしており、内部的にsoupsieveというライブラリを利用して高度なマッチングを実現しています。
まずは、属性を指定する基本的なセレクタの書き方をおさらいしましょう。
通常、属性の完全一致を狙う場合は、[属性名="値"]という形式で記述します。
しかし、スクレイピングの実務では、「特定の文字列で始まるID」や「特定の拡張子を持つリンク」を抽出したいケースが非常に多いです。
属性の部分一致を実現する3つの演算子
CSSセレクタには、属性値のパターンにマッチさせるための特殊な演算子が用意されています。
Beautiful Soupのselectメソッドでも、これらの演算子をそのまま使用することが可能です。
主要な演算子として、「^=」「$=」「*=」の3種類を覚えることが重要です。
これらを使いこなすことで、正規表現を別途インポートすることなく、スマートに条件分岐を記述できます。
以下の表は、それぞれの演算子がどのようなマッチングを行うかをまとめたものです。
| 演算子 | 一致の種類 | 説明 |
|---|---|---|
^= | 前方一致 | 指定した文字列で属性値が始まる要素にマッチします。 |
$= | 後方一致 | 指定した文字列で属性値が終わる要素にマッチします。 |
*= | 中間一致(包含) | 指定した文字列が属性値のどこかに含まれる要素にマッチします。 |
~= | 単語一致 | スペース区切りの単語リストの中に指定した文字列が含まれる場合にマッチします。 |
これらの演算子を組み合わせることで、スクレイピングの柔軟性は飛躍的に高まります。
次項からは、それぞれの具体的な使い方をコード例とともに見ていきましょう。
前方一致(^=)で抽出する
前方一致は、属性値の先頭が特定の文字列であることを条件にします。
例えば、広告バナーや動的な要素に対して「ad-123」「ad-456」のように、共通の接頭辞がついている場合に有効です。
以下のサンプルコードでは、IDが「post-」で始まる要素のみを抽出しています。
from bs4 import BeautifulSoup
html_doc = """
<div>
<p id="post-101">記事1の内容</p>
<p id="post-102">記事2の内容</p>
<p id="sidebar-info">サイドバーの情報</p>
</div>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
# IDが "post-" で始まるpタグをすべて取得
posts = soup.select('p[id^="post-"]')
for post in posts:
print(f"見つかった要素: {post.text}")
見つかった要素: 記事1の内容
見つかった要素: 記事2の内容
このように、p[id^="post-"]と記述するだけで、余計な条件判定を書かずに済みます。
前方一致は、カテゴリ分けされたクラス名やIDをまとめて取得する際に非常に重宝します。
後方一致($=)で抽出する
後方一致は、属性値の末尾が特定の文字列であることを条件にします。
この手法が最も活躍するのは、リンクのリンク先(href属性)からファイル形式を特定したいときです。
例えば、ページ内にあるPDFファイルへのリンクだけをすべてリストアップしたい場合に便利です。
from bs4 import BeautifulSoup
html_content = """
<ul>
<li><a href="report2024.pdf">2024年報告書</a></li>
<li><a href="image.jpg">写真</a></li>
<li><a href="summary.pdf">概要PDF</a></li>
<li><a href="index.html">トップページ</a></li>
</ul>
"""
soup = BeautifulSoup(html_content, 'html.parser')
# href属性が ".pdf" で終わるaタグを取得
pdf_links = soup.select('a[href$=".pdf"]')
for link in pdf_links:
print(f"PDFファイルを発見: {link.get('href')}")
PDFファイルを発見: report2024.pdf
PDFファイルを発見: summary.pdf
a[href$=".pdf"]という指定により、画像ファイルやHTMLファイルを自動的に除外して抽出できています。
もしこれがfind_allであれば、ラムダ式や正規表現を組み合わせて記述する必要がありますが、CSSセレクタなら一行で完結します。
中間一致(*=)で抽出する
中間一致は、属性値の中のどこかに特定の文字列が含まれていればマッチします。
条件が最も緩いため、非常に汎用性が高い演算子です。
URLの中に特定のキーワードが含まれているリンクを探したり、長いクラス名の一部をフックにしたりする場合に役立ちます。
from bs4 import BeautifulSoup
html_snippet = """
<div class="container">
<div class="item-primary-box">主要なアイテム</div>
<div class="item-secondary-card">補助的なアイテム</div>
<div class="navigation-bar">メニュー</div>
</div>
"""
soup = BeautifulSoup(html_snippet, 'html.parser')
# クラス名に "item" という文字列が含まれるdivタグを取得
items = soup.select('div[class*="item"]')
for item in items:
print(f"抽出されたクラス: {item['class']}")
抽出されたクラス: ['item-primary-box']
抽出されたクラス: ['item-secondary-card']
注意点として、class*="item"は「item-primary」だけでなく「menu-item」などにもマッチします。
特定の文字列を含んでいるかどうかという広い条件で探したい場合には、この中間一致が最適です。
実践的なテクニック:複数の属性や要素と組み合わせる
Beautiful Soupのselectメソッドの真価は、これらの部分一致演算子を他のセレクタと組み合わせたときに発揮されます。
より実戦に近い、複雑な条件指定の方法を解説します。
複数の条件を組み合わせる(AND条件)
「特定のクラスを持ち、かつ特定のURL形式を持つリンク」といった条件も簡単に記述できます。
セレクタを繋げて書くことで、絞り込みを強化することが可能です。
# 「external」というクラスを持ち、かつ「https」で始まるリンクを抽出
results = soup.select('a.external[href^="https"]')
このように記述することで、内部リンク(相対パス)や、クラスが設定されていない外部リンクを除外できます。
スクレイピングの精度を高めるためには、タグ名だけでなく属性セレクタを併用することが推奨されます。
複数の候補から抽出する(OR条件)
「.jpgで終わる、または.pngで終わる」といったOR条件を指定したい場合は、カンマ区切りを利用します。
# jpg または png の画像リンクを取得
images = soup.select('a[href$=".jpg"], a[href$=".png"]')
これにより、1回のselect呼び出しで複数のパターンに合致する要素をリストとして取得できます。
Beautiful Soupでの注意点:クラス名の扱い
Beautiful Soupでは、HTMLのclass属性を特別な「マルチバリュー属性」として扱います。
通常、Beautiful Soupでtag['class']にアクセスすると、文字列ではなくリストが返ってきます。
しかし、CSSセレクタの属性一致([class*="..."])を使用する場合、Beautiful Soupはクラス属性を一つの結合された文字列として解釈して判定を行います。
このため、class="btn primary"という要素に対して、[class^="btn"]はマッチしますが、[class^="primary"]はマッチしません。
もし空白区切りの単語として特定のクラスが含まれているかを確認したい場合は、~=演算子を使用するのが適切です。
[class~="primary"]と記述すれば、クラスの並び順に関係なく、「primary」という独立したクラスが含まれているかを判定できます。
クラス名の部分一致を行う際は、そのクラスが単一なのか、複数設定されている可能性があるのかを事前に確認しておきましょう。
なぜfind_allよりselectの方が好まれるのか
Beautiful Soupを使い始めたばかりの頃は、find_all(attrs={"class": re.compile("...")})のような書き方を教わることが多いかもしれません。
しかし、現代のPythonスクレイピングにおいてselectが推奨される理由は、その圧倒的な簡潔さにあります。
正規表現(reモジュール)をインポートせずに複雑なパターンマッチができるのは、コードの管理上大きなメリットです。
また、CSSセレクタはブラウザのデベロッパーツールで直接コピーして試すことができるため、開発サイクルが早くなります。
パフォーマンスの面でも、通常のスクレイピング用途であればselectがボトルネックになることはほとんどありません。
可読性が高く、メンテナンスしやすいコードを書くためには、CSSセレクタによる部分一致を第一選択肢にすべきです。
まとめ
Beautiful Soupのselectメソッドを用いた属性の部分一致抽出について解説してきました。
前方一致(^=)、後方一致($=)、中間一致(*=)の3つの演算子をマスターするだけで、Webスクレイピングの効率は劇的に向上します。
動的なIDの制御や、特定の拡張子を持つファイルの抽出、複雑なクラス名の中からのデータ特定など、活用シーンは多岐にわたります。
まずは小さなスクリプトからこれらの演算子を取り入れ、セレクタの記述を簡略化してみてください。
柔軟で堅牢なスクレイピングコードを作成するために、CSSセレクタの表現力を最大限に活用しましょう。
今回の内容を参考に、ぜひ日々のデータ収集業務を効率化させてください。
