Webスクレイピングにおいて、HTMLの中から特定の文字列が含まれる要素を効率よく見つけ出す技術は非常に重要です。
PythonのライブラリであるBeautiful Soupは、直感的な操作で複雑なHTML構造から目的のテキストを検索する機能を豊富に備えています。
本記事では、単純な文字列検索から正規表現、さらには関数を用いた高度な抽出手法までを体系的に解説します。
Beautiful Soupにおけるテキスト検索の基礎
Beautiful Soupでテキスト内容を基に要素を検索する場合、主にfind()やfind_all()メソッドを使用します。
これらのメソッドには、タグ名だけでなく、要素内のテキストを条件として指定するための引数が用意されています。
かつてのバージョンではtextという引数が使われていましたが、現在の最新バージョンではstring引数を使用することが推奨されています。
まずは、HTML内のテキストと完全に一致する要素を探す基本的なコードを確認しましょう。
from bs4 import BeautifulSoup
# 解析対象のHTMLサンプル
html_content = """
<div>
<p>こんにちは、Pythonの世界へ!</p>
<p>スクレイピングの学習を始めましょう。</p>
<span>Pythonは非常に便利です。</span>
</div>
"""
# BeautifulSoupオブジェクトの作成
soup = BeautifulSoup(html_content, 'html.parser')
# 特定のテキストを持つ要素を検索
element = soup.find(string="こんにちは、Pythonの世界へ!")
# 取得した結果の確認
print(f"取得したテキスト: {element}")
print(f"親要素のタグ名: {element.parent.name}")
取得したテキスト: こんにちは、Pythonの世界へ!
親要素のタグ名: p
このように、string引数に文字列を渡すと、その内容を保持するNavigableStringオブジェクトが返されます。
特定のタグに限定して検索したい場合は、第一引数にタグ名を指定し、string引数を併用します。
例えば、soup.find("p", string="...")と記述することで、pタグの中にあるテキストのみを対象にできます。
正規表現を用いた部分一致検索のテクニック
実際のWebサイトでは、テキストが完全に一致することは稀であり、多くの場合で部分一致による検索が必要となります。
Beautiful Soupで部分一致検索を実現するには、Python標準のreモジュールを併用します。
re.compile()を使用して正規表現パターンを作成し、それをstring引数に渡すことで柔軟な検索が可能になります。
特定のキーワードを含むすべての要素を抽出する例を見てみましょう。
import re
from bs4 import BeautifulSoup
html_content = """
<ul>
<li>Apple (在庫あり)</li>
<li>Banana (在庫なし)</li>
<li>Orange (在庫あり)</li>
</ul>
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 「在庫あり」という文字列を含む要素をすべて検索
in_stock_items = soup.find_all(string=re.compile("在庫あり"))
for item in in_stock_items:
print(f"マッチしたテキスト: {item.strip()}")
マッチしたテキスト: Apple (在庫あり)
マッチしたテキスト: Orange (在庫あり)
正規表現を利用することで、「特定の文字で始まる」「特定の文字で終わる」といった高度な条件も簡単に指定できます。
例えば、re.compile("^Python")とすれば、文頭がPythonで始まる要素のみに絞り込めます。
大文字と小文字を区別せずに検索したい場合は、re.compile("python", re.IGNORECASE)のようにフラグを指定してください。
Lambda関数を活用した高度なカスタム検索
より複雑なロジックでテキストを検索したい場合は、Lambda関数(無名関数)を引数に渡す方法が有効です。
関数の引数には各要素(Tagオブジェクト)が渡され、その関数がTrueを返した要素が抽出対象となります。
文字数による制限や、複数の条件を組み合わせた検索を行う際にこの手法は威力を発揮します。
from bs4 import BeautifulSoup
html_content = """
<div>
<p class="content">これは短い文章です。</p>
<p class="content">これは非常に長くて詳細な説明が含まれている文章の例です。</p>
<p class="footer">短いコピー</p>
</div>
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 「文章」という文字を含み、かつ15文字以上のテキストを持つpタグを検索
long_paragraphs = soup.find_all(lambda tag:
tag.name == "p" and
"文章" in tag.get_text() and
len(tag.get_text()) > 15
)
for tag in long_paragraphs:
print(f"条件に合うタグのテキスト: {tag.get_text()}")
条件に合うタグのテキスト: これは非常に長くて詳細な説明が含まれている文章の例です。
この手法の利点は、tag.get_text()を用いることで、子要素を含めたすべてのテキスト内容を評価対象にできる点にあります。
string引数では子要素を持つタグのテキスト取得が難しい場合がありますが、関数指定であればその制約を回避できます。
テキスト検索における主な手法の比較
目的に応じて最適な検索手法を選択できるよう、各アプローチの特徴を整理しました。
| 手法 | メリット | デメリット |
|---|---|---|
| 完全一致 (string=”…”) | 記述がシンプルで分かりやすい。 | 1文字でも異なるとヒットしない。 |
| 正規表現 (re.compile) | 部分一致やパターン検索が可能。 | 正規表現の知識が必要になる。 |
| Lambda関数 | 複雑な条件(文字数、属性など)を併用できる。 | コードがやや複雑になり、可読性が下がる。 |
| CSSセレクタ (select) | 複雑な階層構造を効率よく指定できる。 | テキスト内容そのものを直接条件にできない。 |
基本的には正規表現を使いこなし、どうしても解決できない複雑なフィルタリングが必要な場合にのみLambda関数を検討するのが良いでしょう。
空白文字や改行コードへの対処法
WebサイトのHTMLには、目に見えない改行コードや連続したスペースが含まれていることがよくあります。
これらが原因で、期待したテキスト検索が失敗するケースが多々あります。
Beautiful Soupで検索を行う前に、テキストの正規化(クリーニング)を考慮することが重要です。
string引数で検索する場合、前後の空白は厳密に区別されるため、正規表現の\s*(任意の空白)を活用することをお勧めします。
import re
from bs4 import BeautifulSoup
html_content = "<p> 最新のニュース \n</p>"
soup = BeautifulSoup(html_content, 'html.parser')
# 普通の完全一致では失敗する可能性がある
# 正規表現で前後の空白を許容して検索
pattern = re.compile(r"\s*最新のニュース\s*")
result = soup.find(string=pattern)
if result:
print(f"抽出成功: {result.strip()}")
抽出成功: 最新のニュース
また、get_text(strip=True)メソッドを使用すると、要素内のテキストから前後の空白を自動的に削除して取得できます。
データを収集するフェーズでは、このstrip=Trueを積極的に活用してデータを綺麗に保ちましょう。
テキストを起点に周囲の要素を特定する方法
特定のテキストを見つけることがゴールではなく、そのテキストの隣にあるデータや親要素を取得したいケースが多々あります。
Beautiful Soupでは、検索で見つけたテキスト(NavigableString)からDOMツリーを遡ったり下ったりすることが可能です。
例えば、「商品名」というテキストが含まれるセルの隣にある「価格」を取得するようなシナリオです。
from bs4 import BeautifulSoup
html_content = """
<table>
<tr><td>商品A</td><td>1,000円</td></tr>
<tr><td>商品B</td><td>2,500円</td></tr>
</table>
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 「商品B」というテキストを持つ要素を探す
target_text = soup.find(string="商品B")
# その親要素(td)の次の兄弟要素(td)を取得
price_td = target_text.find_parent("td").find_next_sibling("td")
print(f"商品名: {target_text}")
print(f"価格: {price_td.get_text()}")
商品名: 商品B
価格: 2,500円
このように、find_parent()やfind_next_sibling()を組み合わせることで、構造化されていないデータからも目的の値を抽出できます。
テキスト検索は、DOM探索を開始するための強力な「アンカー(錨)」として機能します。
大規模なページでの検索パフォーマンス最適化
解析対象のHTMLが非常に巨大な場合、find_all()による全文検索は処理時間を要することがあります。
効率を上げるためには、検索範囲をあらかじめ絞り込む手法が有効です。
まずIDやクラス名で大枠のコンテナ要素を特定し、その範囲内に対してテキスト検索を実行します。
# 効率的な検索の例
# まずは特定のIDを持つエリアに絞り込む
main_content = soup.find(id="main-article-area")
# そのエリア内だけでテキスト検索を実行
if main_content:
target_elements = main_content.find_all(string=re.compile("キーワード"))
また、Beautiful Soupのパーサーとしてlxmlを使用することも、速度向上のための一般的な手法です。
lxmlはC言語で記述されており、標準のhtml.parserよりも高速に動作します。
さらに、特定のタグだけを解析対象にするSoupStrainerクラスを利用すれば、メモリ使用量を抑えつつ高速な処理が可能になります。
実戦的な抽出例:ニュース見出しのフィルタリング
学んだ技術を応用して、特定のトピックに関連するニュース見出しだけを抽出する実用的なコードを作成してみましょう。
ここでは「AI」または「人工知能」という言葉を含む記事のタイトルとURLを取得する想定です。
import re
from bs4 import BeautifulSoup
# 仮想のニュースサイトHTML
news_html = """
<div class="news-list">
<article>
<h2><a href="/news/01">最新のAI技術が医療を変える</a></h2>
<p>2026年の人工知能市場予測について。</p>
</article>
<article>
<h2><a href="/news/02">週末の天気予報</a></h2>
<p>全国的に晴天が続く見込みです。</p>
</article>
<article>
<h2><a href="/news/03">次世代人工知能の倫理的課題</a></h2>
<p>AI開発におけるガイドラインが策定されました。</p>
</article>
</div>
"""
soup = BeautifulSoup(news_html, 'html.parser')
# 「AI」または「人工知能」を含むh2タグ内のaタグを検索
# 正規表現の | (OR) を使用
keywords = re.compile("AI|人工知能")
# テキスト内容に基づいて検索し、aタグそのものを取得
# string引数はNavigableStringを返すが、ここでは親のaタグを見つけたい
relevant_articles = soup.find_all("a", string=keywords)
print("--- 抽出された記事一覧 ---")
for a_tag in relevant_articles:
title = a_tag.get_text()
link = a_tag.get("href")
print(f"タイトル: {title} / URL: {link}")
--- 抽出された記事一覧 ---
タイトル: 最新のAI技術が医療を変える / URL: /news/01
タイトル: 次世代人工知能の倫理的課題 / URL: /news/03
このように、特定のキーワードに基づいて必要な情報だけをフィルタリングする処理は、データ収集の自動化において中核となる技術です。
リスト内包表記などと組み合わせることで、さらに簡潔なコードで記述することも可能です。
テキスト検索時に注意すべき落とし穴
Beautiful Soupのstring引数を使用する際、対象の要素が複数の子要素(例えば途中に<br>タグや<span>タグがあるなど)を持っていると、期待通りに動作しないことがあります。
これは、要素内にタグが混じると、その要素の.string属性がNoneを返してしまうという仕様に起因します。
このようなケースでは、string引数の代わりに、先に述べたLambda関数内で.get_text()を使用するか、正規表現で全体をカバーする工夫が必要です。
また、動的にJavaScriptで生成されるテキストは、Beautiful Soup単体では取得できません。
その場合は、SeleniumやPlaywrightといったブラウザ操作ライブラリと組み合わせて、レンダリング後のHTMLをBeautiful Soupに渡す手順を踏んでください。
まとめ
Beautiful Soupを使用したテキスト検索は、Webスクレイピングの効率を飛躍的に高める技術です。
単なる完全一致検索にとどまらず、正規表現やLambda関数を駆使することで、複雑なHTML構造からでも正確に目的の情報を取り出すことができます。
検索対象のテキストに空白や改行が含まれる可能性を常に考慮し、正規化を行う習慣を身につけましょう。
また、テキストを起点に親要素や兄弟要素を探索するテクニックは、表形式のデータやリスト構造の解析において非常に強力な武器となります。
本記事で紹介した様々な手法を、実際のスクレイピングプロジェクトの要件に合わせて使い分けてみてください。
正確かつ効率的なデータ抽出が可能になれば、その後のデータ分析や活用もよりスムーズに進むはずです。
