PythonライブラリBeautiful Soup 4 (bs4) の徹底解説:Webスクレイピング入門から応用まで

Python

HTML/XMLを簡単にパースして、必要な情報を抽出しよう!

Webページから特定の情報を自動で収集したいと思ったことはありませんか?例えば、ニュースサイトから最新記事のタイトルだけを抜き出したり、ECサイトから商品の価格情報を取得したり…。そんな時に活躍するのがWebスクレイピングという技術です。

そして、PythonでWebスクレイピングを行う際に、非常に人気があり広く使われているライブラリが Beautiful Soup 4 (bs4) です。この記事では、Beautiful Soup 4の基本的な使い方から、少し応用的なテクニックまで、具体例を交えながら詳しく解説していきます。😊

Beautiful Soupは、HTMLやXMLといったマークアップ言語で書かれた文書を解析(パース)し、Pythonのオブジェクトとして扱いやすくしてくれるライブラリです。複雑なHTML構造の中から、目的のデータを簡単に、そしてPythonらしい直感的な方法で取り出すことができます。

Beautiful Soup 4 のインストール

Beautiful Soup 4はPythonの標準ライブラリではないため、使用する前にインストールする必要があります。Pythonのパッケージ管理ツールであるpipを使うのが最も簡単です。

ターミナル(Windowsの場合はコマンドプロンプト)を開き、以下のコマンドを実行してください。

pip install beautifulsoup4

これでBeautiful Soup 4本体のインストールは完了です。✅

ただし、Beautiful SoupはあくまでHTML/XMLの構造を解析しやすくするためのインターフェースを提供するライブラリであり、実際に解析を行うパーサーは別途必要になります。Beautiful Soupは複数のパーサーを利用できますが、代表的なものと、そのインストールコマンドは以下の通りです。

  • html.parser: Pythonの標準ライブラリに含まれています。追加インストールは不要ですが、性能は中程度です。
  • lxml: 非常に高速で、かつ柔軟な解析が可能です。多くの場合、これが推奨されますが、C言語で書かれたライブラリに依存するため、環境によってはインストールに追加の手順が必要になる場合があります。
    pip install lxml
  • html5lib: Webブラウザと同様の非常に正確な解析を行いますが、動作は比較的遅いです。HTML5の仕様に厳密に従って解析したい場合に有用です。
    pip install html5lib

どのパーサーを使うかは、Beautiful Soupのオブジェクトを作成する際に指定します。特に理由がなければ、高速なlxmlのインストールをおすすめします。

💡 Webページの内容を取得するには?
Beautiful SoupはHTML/XMLの「解析」を行うライブラリです。Webページの内容(HTMLソースコード)をインターネットから取得するには、別途requestsのようなHTTP通信ライブラリが必要になります。requestsもpipで簡単にインストールできます。

pip install requests
この記事の例でも、requestsライブラリと組み合わせて使用します。

基本的な使い方

Beautiful Soupを使う基本的な流れは以下のようになります。

  1. 必要なライブラリ(`BeautifulSoup`と、通常は`requests`)をインポートする。
  2. `requests`を使ってWebページのHTMLを取得する。
  3. 取得したHTMLと使用するパーサーを指定して`BeautifulSoup`オブジェクトを作成する。
  4. 作成したオブジェクトのメソッドを使って、目的のデータを検索・抽出する。

実際のコードを見てみましょう。ここでは、簡単なHTML文字列を解析する例を示します。

from bs4 import BeautifulSoup

# 解析したいHTML文字列
html_doc = """
<html><head><title>テストページ</title></head>
<body>
<p class="title"><b>これはタイトルです</b></p>

<p class="story">これは段落1です。
<a href="http://example.com/link1" class="sister" id="link1">リンク1</a>、
<a href="http://example.com/link2" class="sister" id="link2">リンク2</a> そして
<a href="http://example.com/link3" class="sister" id="link3">リンク3</a>。
</p>

<p class="story">...</p>
</body>
</html>
"""

# BeautifulSoupオブジェクトを作成
# 第1引数にHTML文字列、第2引数に使用するパーサーを指定
soup = BeautifulSoup(html_doc, 'html.parser') # ここでは標準のhtml.parserを使用

# titleタグの内容を取得
print(f"タイトル: {soup.title.string}")

# 最初のpタグの内容を取得
print(f"最初のPタグ: {soup.p}")

# 最初のpタグのclass属性を取得
print(f"最初のPタグのクラス: {soup.p['class']}") # ['title']

# 最初のaタグを取得
print(f"最初のAタグ: {soup.a}")

# 全てのaタグを取得 (ResultSetというリストのようなオブジェクトが返る)
all_a_tags = soup.find_all('a')
print(f"全てのAタグの数: {len(all_a_tags)}") # 3

# 3番目のaタグのテキストを取得
print(f"3番目のAタグのテキスト: {all_a_tags[2].string}") # リンク3

# id="link2"の要素を取得
link2 = soup.find(id="link2")
print(f"IDがlink2のタグ: {link2}")

# href属性を取得
print(f"IDがlink2のhref: {link2['href']}") # http://example.com/link2

このように、`BeautifulSoup`オブジェクトを作成した後は、`.タグ名`で最初の該当タグにアクセスしたり、`find()`や`find_all()`といったメソッドで条件に合うタグを検索したりできます。属性値は辞書のように`[‘属性名’]`でアクセスできます。

BeautifulSoupオブジェクトの作成

`BeautifulSoup`オブジェクトは、通常、第一引数にHTML/XMLの文字列、第二引数にパーサーの種類を示す文字列を渡して作成します。

from bs4 import BeautifulSoup
import requests

# WebページからHTMLを取得
url = 'http://example.com' # 実際のURLに置き換えてください
try:
    response = requests.get(url)
    response.raise_for_status() # ステータスコードが200以外なら例外を発生させる
    response.encoding = response.apparent_encoding # 文字化け対策

    # BeautifulSoupオブジェクトを作成 (lxmlパーサーを使用)
    soup = BeautifulSoup(response.text, 'lxml')

    # これで soup オブジェクトを使って解析ができる
    # print(soup.prettify()) # 整形されたHTMLを出力

except requests.exceptions.RequestException as e:
    print(f"リクエストエラー: {e}")
except Exception as e:
    print(f"エラーが発生しました: {e}")

# ローカルファイルから読み込む場合
# with open("example.html", "r", encoding="utf-8") as f:
#     soup_from_file = BeautifulSoup(f, 'lxml')

`requests.get()`で取得したレスポンスの`.text`属性にHTML文字列が含まれています。`response.encoding = response.apparent_encoding`は、文字化けを防ぐためのおまじないです(必ずしも必要ではありませんが、日本語サイトなどを扱う場合に有効なことがあります)。

ファイルからHTMLを読み込む場合は、`open()`でファイルオブジェクトを開き、それを`BeautifulSoup`に渡します。

ツリーの基本的なナビゲーション

Beautiful SoupはHTML/XMLを解析し、タグや文字列などをPythonオブジェクトとして表現したツリー構造を内部に構築します。このツリーを移動(ナビゲート)するための属性が用意されています。

  • `.contents`: タグの子要素をリストとして取得します。文字列も要素として含まれます。
  • `.children`: `.contents`と同様ですが、リストではなくイテレータを返します。メモリ効率が良いです。
  • `.descendants`: タグの子孫要素(子、孫、曾孫…)すべてを再帰的に取得するイテレータを返します。
  • `.string`: タグが内部に持つテキスト(NavigableStringオブジェクト)を直接取得します。ただし、タグ内に他のタグが含まれていたり、テキストが複数に分かれている場合は`None`を返します。
  • `.strings`: タグ内部のテキストを(子孫要素も含め)すべて取得するイテレータを返します。
  • `.stripped_strings`: `.strings`と同様ですが、各テキストの前後の空白文字を除去して取得します。
  • `.parent`: タグの親要素を取得します。
  • `.parents`: タグの祖先要素(親、祖父母、曽祖父母…)を順に取得するイテレータを返します。
  • `.next_sibling`: タグの次の兄弟要素(同じ階層のすぐ隣の要素)を取得します。空白文字や改行も要素として扱われる点に注意が必要です。
  • `.previous_sibling`: タグの前の兄弟要素を取得します。
  • `.next_siblings`: タグの後の兄弟要素すべてを取得するイテレータを返します。
  • `.previous_siblings`: タグの前の兄弟要素すべてを取得するイテレータを返します。
  • `.next_element`: パースされた順序で、次の要素(タグまたは文字列)を取得します。
  • `.previous_element`: パースされた順序で、前の要素を取得します。

これらの属性を組み合わせることで、HTMLツリー内を柔軟に移動できますが、実際には次に説明する検索メソッドを使う方が便利なことが多いです。

ツリーの検索:find_all() と select()

Beautiful Soupで最もよく使うのが、特定の条件に合う要素を検索する機能です。主に`find_all()`メソッドと`select()`メソッドが使われます。

find_all() メソッド

`find_all()`は、指定した条件に一致するすべての要素をリスト(正確には`bs4.element.ResultSet`オブジェクト)として返します。一致するものがなければ空のリストを返します。

様々な条件を指定できます。

from bs4 import BeautifulSoup

html_doc = """
<html><head><title>検索テスト</title></head>
<body>
  <h1 id="main-title" class="heading">主要タイトル</h1>
  <p class="content-text">これは本文です。</p>
  <div class="items">
    <a href="/item/1" class="item-link active">アイテム1</a>
    <a href="/item/2" class="item-link">アイテム2</a>
    <span class="item-link inactive">アイテム3 (リンク切れ)</span>
  </div>
  <!-- コメント -->
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'lxml')

# 1. タグ名で検索
h1_tags = soup.find_all('h1')
print(f"H1タグ: {h1_tags}") # [<h1 class="heading" id="main-title">主要タイトル</h1>]

a_tags = soup.find_all('a')
print(f"Aタグの数: {len(a_tags)}") # 2

# 2. 属性で検索 (キーワード引数を使用)
# class属性で検索する場合、Pythonの予約語'class'と衝突するため、末尾にアンダースコアをつけて'class_'とします。
item_links = soup.find_all(class_='item-link')
print(f"class='item-link'のタグ数: {len(item_links)}") # 3 (aタグ2つとspanタグ1つ)

active_items = soup.find_all(class_='active')
print(f"class='active'のタグ: {active_items}") # [<a class="item-link active" href="/item/1">アイテム1</a>]

# 複数のクラスを持つ要素を検索 (スペース区切りで指定)
active_item_links = soup.find_all(class_='item-link active')
print(f"class='item-link active'のタグ: {active_item_links}") # [<a class="item-link active" href="/item/1">アイテム1</a>]

# id属性で検索
main_title = soup.find_all(id='main-title')
print(f"id='main-title'のタグ: {main_title}") # [<h1 class="heading" id="main-title">主要タイトル</h1>]

# href属性を持つaタグを検索
links_with_href = soup.find_all('a', href=True)
print(f"hrefを持つAタグ数: {len(links_with_href)}") # 2

# 特定のhref属性値を持つaタグを検索
item1_link = soup.find_all('a', href='/item/1')
print(f"href='/item/1'のAタグ: {item1_link}") # [<a class="item-link active" href="/item/1">アイテム1</a>]

# 3. 属性で検索 (attrs引数を使用)
# キーワード引数で指定できない属性名(例: 'data-foo')や、より複雑な条件で検索したい場合にattrs辞書を使います。
# 例: soup.find_all(attrs={'data-custom': 'value'})

# 4. テキスト内容で検索
# 完全一致
title_text = soup.find_all(string='主要タイトル')
print(f"テキストが'主要タイトル'の要素: {title_text}") # ['主要タイトル'] (NavigableStringオブジェクトが返る)

# 部分一致 (正規表現を使用)
import re
items_containing_text = soup.find_all(string=re.compile('アイテム'))
print(f"テキストに'アイテム'を含む要素: {items_containing_text}") # ['アイテム1', 'アイテム2', 'アイテム3 (リンク切れ)']

# 5. 複数の条件を組み合わせる
# classが'item-link'で、かつhref属性を持つaタグ
real_item_links = soup.find_all('a', class_='item-link', href=True)
print(f"class='item-link'でhrefを持つAタグ数: {len(real_item_links)}") # 2

# 6. 検索範囲を制限する (`recursive=False`)
# find_all はデフォルトで子孫要素全てを検索しますが、`recursive=False` を指定すると直接の子要素のみを検索します。
div_tag = soup.find('div', class_='items')
direct_children_links = div_tag.find_all('a', recursive=False) # divの直接の子であるaタグ
print(f"div直下のAタグ数: {len(direct_children_links)}") # 2 (spanは直接の子だがaタグではない)

# 7. 取得数を制限する (`limit`引数)
first_item_link = soup.find_all('a', class_='item-link', limit=1)
print(f"最初のitem-link: {first_item_link}") # [<a class="item-link active" href="/item/1">アイテム1</a>]

# 補足: find() メソッド
# find_all() がリストを返すのに対し、find() は条件に最初に一致した**単一の**要素を返します。
# 一致する要素がない場合は None を返します。
# find() は find_all(limit=1) とほぼ同等ですが、返す型が異なります(要素自身 or None)。
first_a = soup.find('a')
print(f"findで見つけた最初のAタグ: {first_a}")
non_existent = soup.find('table')
print(f"存在しないtableタグ: {non_existent}") # None

`find_all()`(および`find()`)は非常に柔軟で、タグ名、属性、テキスト内容など、様々な組み合わせで要素を検索できます。

select() メソッド (CSSセレクタ)

もう一つの強力な検索方法が`select()`メソッドです。これはCSSセレクタを使って要素を検索します。Web開発に慣れている方には、こちらの方が直感的かもしれません。

`select()`は、セレクタに一致するすべての要素をリストとして返します。一致するものがなければ空のリストを返します。

`select_one()`というメソッドもあり、これはセレクタに最初に一致した単一の要素を返します(一致しない場合は`None`)。`find()`と`find_all()`の関係に似ています。

from bs4 import BeautifulSoup

html_doc = """
<html><head><title>セレクタテスト</title></head>
<body>
  <div id="main">
    <h1 class="title main-title">メインタイトル</h1>
    <p class="content">段落1</p>
    <ul class="items">
      <li class="item"><a href="/page1">項目1</a></li>
      <li class="item special"><a href="/page2">項目2 (特別)</a></li>
      <li class="item"><span>項目3 (リンクなし)</span></li>
    </ul>
  </div>
  <div id="sub">
    <p class="content">段落2</p>
  </div>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'lxml')

# 1. タグ名で選択
p_tags = soup.select('p')
print(f"Pタグの数: {len(p_tags)}") # 2

# 2. クラス名で選択 (ドット`.`を使用)
content_elements = soup.select('.content')
print(f"class='content'の要素数: {len(content_elements)}") # 2

special_items = soup.select('.special')
print(f"class='special'の要素: {special_items}") # [<li class="item special">...</li>]

# 複数のクラスを持つ要素 (ドットを繋げる)
special_item_li = soup.select('li.item.special')
print(f"li.item.special: {special_item_li}") # [<li class="item special">...</li>]

# 3. IDで選択 (ハッシュ `#`を使用)
main_div = soup.select('#main')
print(f"id='main'の要素: {main_div}") # [<div id="main">...</div>]
# IDは通常一意なので、select_oneを使う方が適している場合が多い
main_div_one = soup.select_one('#main')
print(f"select_one('#main'): {main_div_one.name}") # div

# 4. 子孫要素を選択 (スペース区切り)
# id='main' の div の中にある全ての a タグ
links_in_main = soup.select('#main a')
print(f"#main 内のAタグ数: {len(links_in_main)}") # 2

# 5. 直接の子要素を選択 ('>' を使用)
# class='items' の ul の直接の子である li タグ
list_items = soup.select('ul.items > li')
print(f"ul.items 直下のliタグ数: {len(list_items)}") # 3

# 6. 属性で選択 ([属性名=値] を使用)
link_to_page1 = soup.select('a[href="/page1"]')
print(f"href='/page1'のAタグ: {link_to_page1}") # [<a href="/page1">項目1</a>]

# 属性の存在確認 ([属性名])
elements_with_class = soup.select('[class]')
print(f"class属性を持つ要素数: {len(elements_with_class)}") # 7 (h1, p, ul, li*3, p)

# 7. 複数のセレクタを組み合わせる (カンマ `,` 区切り)
h1_and_special_li = soup.select('h1, li.special')
print(f"h1 または li.special の要素数: {len(h1_and_special_li)}") # 2

# 8. select_one() の使用例
first_link = soup.select_one('a') # 最初に現れるaタグ
print(f"最初のAタグ (select_one): {first_link}")

CSSセレクタは非常に表現力が高く、複雑な条件も簡潔に記述できることが多いです。

find_all() と select() の使い分け

`find_all()`と`select()`はどちらも強力な検索メソッドですが、以下のような特徴があります。

  • find_all():
    • Pythonの引数として条件を指定するため、プログラム的に条件を動的に生成しやすい。
    • `string`引数や正規表現を使ってテキスト内容での検索が直接的に行える。
    • `recursive=False` で検索範囲を直接の子要素に限定できる。
  • select():
    • CSSセレクタに慣れていれば、複雑な階層構造や属性条件を簡潔に記述できる。
    • 子孫セレクタ (` `)、子セレクタ (`>`)、隣接兄弟セレクタ (`+`)、一般兄弟セレクタ (`~`) など、CSS標準の多様な関係性セレクタが使える。
    • 属性セレクタ(`[attr^=val]`, `[attr$=val]`, `[attr*=val]`など)も利用可能。

どちらを使うかは好みや状況によりますが、単純なタグ名やクラス名での検索ならどちらでも大差ありません。複雑な階層関係やCSS特有のセレクタを使いたい場合は`select()`が便利です。一方、テキスト内容での検索や、プログラムで条件を組み立てたい場合は`find_all()`の方が適していることがあります。📝

データの抽出

目的のタグ(要素)を見つけたら、次はその中から具体的な情報(テキストや属性値)を抽出します。

from bs4 import BeautifulSoup

html_doc = """
<div id="product-1" class="item" data-category="electronics">
  <h2 class="name">スマートフォン</h2>
  <p class="price">価格: <span>¥50,000</span> (税込)</p>
  <a href="/products/1" class="details-link">詳細を見る</a>
</div>
"""
soup = BeautifulSoup(html_doc, 'lxml')

product_div = soup.find('div', id='product-1')

# 1. タグ名を取得
print(f"タグ名: {product_div.name}") # div

# 2. 属性値を取得
# 辞書アクセス (属性が存在しないとKeyError)
product_id = product_div['id']
print(f"ID属性: {product_id}") # product-1

# get()メソッド (属性が存在しなくてもエラーにならない、デフォルト値を指定可能)
category = product_div.get('data-category')
print(f"data-category属性: {category}") # electronics
non_existent_attr = product_div.get('data-unknown', 'デフォルト値')
print(f"存在しない属性: {non_existent_attr}") # デフォルト値

# class属性 (リストが返る)
classes = product_div.get('class')
print(f"Class属性: {classes}") # ['item']

# 全ての属性を辞書として取得
all_attrs = product_div.attrs
print(f"全属性: {all_attrs}") # {'id': 'product-1', 'class': ['item'], 'data-category': 'electronics'}

# 3. テキスト内容を取得
# .string (タグ直下のテキストのみ、子タグがあるとNone)
h2_tag = product_div.find('h2')
product_name_string = h2_tag.string
print(f"製品名 (.string): {product_name_string}") # スマートフォン

price_p_tag = product_div.find('p', class_='price')
price_string = price_p_tag.string # pタグ内にはspanタグとテキストが混在するためNoneになる
print(f"価格 (.string): {price_string}") # None

# .get_text() (子孫要素のテキストも含めて結合して取得)
product_name_get_text = h2_tag.get_text()
print(f"製品名 (.get_text()): {product_name_get_text}") # スマートフォン

price_get_text = price_p_tag.get_text()
print(f"価格 (.get_text()): {price_get_text}") # 価格: ¥50,000 (税込)

# .get_text() のオプション
# separator: テキスト要素間の区切り文字を指定
price_get_text_sep = price_p_tag.get_text(separator='|')
print(f"価格 (.get_text(separator='|')): {price_get_text_sep}") # 価格: |¥50,000| (税込)

# strip=True: 各テキストの前後の空白を除去してから結合
price_get_text_strip = price_p_tag.get_text(strip=True)
print(f"価格 (.get_text(strip=True)): {price_get_text_strip}") # 価格:¥50,000(税込)

# .strings (子孫要素のテキストを個別に取得するイテレータ)
print("価格 (.strings):")
for s in price_p_tag.strings:
    print(f"- '{s}'")
# 出力:
# - '価格: '
# - '¥50,000'
# - ' (税込)'

# .stripped_strings (空白除去版の.strings)
print("価格 (.stripped_strings):")
for s in price_p_tag.stripped_strings:
    print(f"- '{s}'")
# 出力:
# - '価格:'
# - '¥50,000'
# - '(税込)'

# リンクのURLを取得
details_link = product_div.find('a', class_='details-link')
link_url = details_link['href']
link_text = details_link.get_text()
print(f"詳細リンクURL: {link_url}") # /products/1
print(f"詳細リンクテキスト: {link_text}") # 詳細を見る
  • 属性値の取得には、辞書アクセス `[‘attr_name’]` か `get(‘attr_name’)` メソッドを使います。`get()`の方が属性が存在しない場合にエラーにならないため、より安全です。
  • テキスト内容の取得には主に`.string`と`.get_text()`を使います。
    • `.string`: タグ直下のテキストのみを取得したい場合に便利ですが、子タグや複数のテキストノードがあると`None`になります。
    • `.get_text()`: タグとその子孫要素すべてのテキストを連結して取得します。最もよく使われる方法です。`separator`や`strip`引数で整形も可能です。
    • `.strings` / `.stripped_strings`: テキストを個別に処理したい場合にイテレータとして取得できます。

これらの方法を使い分けることで、HTMLから必要な情報を正確に抽出できます。

パーサーの選択と比較

Beautiful Soupは、HTML/XMLを実際に解析するパーサーを選択できます。どのパーサーを使うかによって、解析速度、壊れたHTMLへの寛容度、依存ライブラリなどが異なります。

主なパーサーの特徴を比較してみましょう。

パーサー Beautiful Soupでの指定 特徴 速度 寛容度 (壊れたHTML) 依存ライブラリ 解析タイプ
html.parser 'html.parser' Python標準ライブラリ。追加インストール不要。 中程度 比較的寛容 (Python 3.2以降) なし HTML
lxml (HTML) 'lxml' 非常に高速。柔軟性が高い。推奨されることが多い。 非常に速い 🚀 寛容 lxml (C依存) HTML
lxml (XML) 'lxml-xml' または 'xml' 非常に高速。XMLの解析に特化。 非常に速い 🚀 厳格 (XML仕様準拠) lxml (C依存) XML
html5lib 'html5lib' Webブラウザと同等の解析。HTML5準拠。正確性が高い。 遅い🐢 非常に寛容 html5lib HTML

どのパーサーを選ぶべきか?

  • 一般的なWebスクレイピング (HTML): まずは高速で柔軟な `lxml` を試すのが良いでしょう。インストールが必要ですが、多くの場合で最良の選択肢となります。
  • 依存関係を増やしたくない場合: Python標準の `html.parser` を使います。性能は`lxml`に劣りますが、十分な場合も多いです。
  • 非常に壊れたHTMLを扱う場合、またはブラウザの解釈と完全に一致させたい場合: `html5lib` を検討します。ただし、速度が遅い点に注意が必要です。
  • XMLファイルを解析する場合: `lxml-xml` (またはエイリアスの`’xml’`) を使用します。

パーサーを指定せずに`BeautifulSoup`オブジェクトを作成した場合、環境に`lxml`がインストールされていれば`lxml`が、なければ`html.parser`が自動的に選択されることが多いですが、挙動を明確にするためにも、常にパーサーを明示的に指定することをおすすめします。

実践的な例:ニュースサイトの見出しを取得

簡単な例として、架空のニュースサイトから最新ニュースの見出しとリンクを取得するコードを書いてみましょう。(※注意:これは架空のHTML構造に対する例です。実際のサイトでは構造が異なるため、適宜調整が必要です。)

架空のHTML構造 (想定)

<!-- news_site.html -->
<div class="news-list">
  <article class="news-item">
    <h2 class="news-title"><a href="/news/101">今日のビッグニュース!</a></h2>
    <p class="news-summary">驚きの出来事がありました...</p>
  </article>
  <article class="news-item">
    <h2 class="news-title"><a href="/news/102">技術的な進歩について</a></h2>
    <p class="news-summary">新しい技術が発表されました...</p>
  </article>
  <article class="news-item">
    <h2 class="news-title"><a href="/news/103">スポーツの結果速報</a></h2>
    <p class="news-summary">昨日の試合結果は...</p>
  </article>
</div>

Pythonコード

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin # URLを結合するために使用

# 架空のニュースサイトURL (実際にはrequestsで取得する想定)
# この例ではローカルファイルを読むことにします
# target_url = "http://example-news.com"
# try:
#     response = requests.get(target_url)
#     response.raise_for_status()
#     response.encoding = response.apparent_encoding
#     html_content = response.text
# except requests.exceptions.RequestException as e:
#     print(f"Error fetching URL {target_url}: {e}")
#     exit()

# --- ローカルファイルから読み込む場合 ---
try:
    with open("news_site.html", "r", encoding="utf-8") as f:
        html_content = f.read()
except FileNotFoundError:
    print("Error: news_site.htmlが見つかりません。上記のHTMLをファイルとして保存してください。")
    exit()
# ------------------------------------

soup = BeautifulSoup(html_content, 'lxml')

# ベースURL (相対URLを絶対URLに変換するため)
base_url = "http://example-news.com" # 架空

news_list = []

# class="news-item" の article タグをすべて取得
news_articles = soup.find_all('article', class_='news-item')

if not news_articles:
    print("ニュース記事が見つかりませんでした。HTML構造を確認してください。")
else:
    print(f"{len(news_articles)}件のニュースが見つかりました。")
    print("-" * 30)

    for article in news_articles:
        # 各記事の中から h2 タグ (クラス名 news-title) を探す
        title_tag = article.find('h2', class_='news-title')
        if title_tag:
            # h2 タグの中の a タグを探す
            a_tag = title_tag.find('a')
            if a_tag:
                # a タグのテキスト(見出し)と href 属性(リンク)を取得
                title = a_tag.get_text(strip=True)
                relative_link = a_tag.get('href')

                # href属性が存在し、空でないことを確認
                if relative_link:
                    # 相対URLを絶対URLに変換
                    absolute_link = urljoin(base_url, relative_link)
                    news_list.append({'title': title, 'link': absolute_link})
                else:
                    print(f"警告: タイトル '{title}' に有効なリンクが見つかりません。")
            else:
                 print(f"警告: クラス 'news-title' のh2タグ内にaタグが見つかりません: {title_tag}")
        else:
            print(f"警告: クラス 'news-item' のarticle内にクラス 'news-title' のh2タグが見つかりません: {article}")

# 結果を表示
if news_list:
    print("\n取得したニュース一覧:")
    for item in news_list:
        print(f"  タイトル: {item['title']}")
        print(f"  リンク: {item['link']}")
        print("-" * 20)
else:
    print("有効なニュース情報が取得できませんでした。")

このコードは、まず`news-item`クラスを持つ`article`タグをすべて探し出します。次に、各`article`タグの中から`news-title`クラスを持つ`h2`タグを探し、さらにその中の`a`タグからテキスト(見出し)と`href`属性(リンク)を取得しています。`urljoin`を使って、相対URLを絶対URLに変換している点もポイントです。

実際のWebサイトでは、HTML構造がもっと複雑だったり、JavaScriptによって動的にコンテンツが生成されたりすることがあります。その場合は、ブラウザの開発者ツール(F12キーなどで起動)を使ってHTML構造をよく観察し、目的のデータが含まれるタグやクラス、IDを特定する必要があります。

注意点とヒント

  • 文字化け対策: `requests`で取得したページの文字エンコーディングが正しく認識されない場合、文字化けが発生することがあります。`response.encoding = response.apparent_encoding`や、サイトのHTMLヘッダで指定されているエンコーディング(例: `response.encoding = ‘utf-8’`)を明示的に指定することで解決できる場合があります。Beautiful Soup自身もエンコーディングの自動検出機能を持っています。
  • 動的コンテンツの限界: Beautiful Soupは、あくまで取得した時点でのHTML/XMLを解析します。JavaScriptによって後から動的に読み込まれたり、変更されたりするコンテンツは、そのままでは取得・解析できません。そのようなサイトをスクレイピングするには、SeleniumやPlaywrightといったブラウザ自動操作ライブラリを併用する必要があります。
  • エラーハンドリング: `find()`や`select_one()`は要素が見つからない場合に`None`を返します。`None`に対して属性アクセス(例: `None[‘href’]`)やメソッド呼び出し(例: `None.get_text()`)を行うとエラーになります。要素が存在するかどうかをチェックするか、`try-except`ブロックを使ってエラーを適切に処理するようにしましょう。属性アクセスも`get()`メソッドを使う方が安全です。
  • robots.txtの尊重: Webサイトによっては、`robots.txt`ファイルでクローラー(スクレイピングプログラムを含む)のアクセスルールを定めています。このルールを無視して過度なアクセスを行うと、サイトに負荷をかけたり、アクセスをブロックされたりする可能性があります。スクレイピングを行う前に必ず`robots.txt`(通常は `http://ドメイン名/robots.txt` にあります)を確認し、ルールを遵守しましょう。
  • 利用規約の確認: サイトによっては利用規約でスクレイピングを禁止している場合があります。必ず利用規約を確認し、許可されている範囲で利用しましょう。
  • 倫理的な考慮: スクレイピングは強力な技術ですが、対象サイトに負荷をかけすぎないよう、アクセス間隔を空ける(例: `time.sleep(1)`を入れる)などの配慮が必要です。個人情報や著作権で保護されたコンテンツの取得・利用は法的に問題となる可能性があります。常識と倫理観を持って利用することが重要です。

まとめ

Beautiful Soup 4は、PythonでWebスクレイピングを行うための非常に強力で使いやすいライブラリです。この記事では、その基本的な使い方から、要素の検索(`find_all`, `select`)、データの抽出(`.string`, `.get_text`, 属性アクセス)、パーサーの選択、そして実践的な例と注意点について解説しました。

Beautiful Soupを使えば、Web上に存在する膨大な情報の中から、必要なデータを効率的に収集・活用することが可能になります。ぜひ、この記事を参考に、Beautiful Soupを使ったWebスクレイピングに挑戦してみてください!💪

さらに詳しい情報や高度な使い方については、Beautiful Soup 公式ドキュメント(英語)や、日本語訳ドキュメントも参照すると良いでしょう。

コメント

タイトルとURLをコピーしました