Python BeautifulSoup完全ガイド – HTMLパース・Webスクレイピングの決定版

 

BeautifulSoupは、PythonでHTMLやXMLを解析するための最も人気の高いライブラリです。直感的なAPIと強力な検索機能により、Webスクレイピングやデータ抽出タスクを簡単に実行できます。この記事では、BeautifulSoupの基本的な使い方から高度なテクニックまで、実践的なサンプルコードとともに詳しく解説します。

BeautifulSoupとは

BeautifulSoupは、HTMLやXMLドキュメントを解析してPythonオブジェクトとして操作できるライブラリです。壊れたHTMLでも柔軟に処理でき、要素の検索、抽出、編集を直感的に行えます。

主な特徴

  • 柔軟なHTMLパース: 不正なHTMLでも適切に処理
  • 直感的なAPI: Pythonらしい分かりやすい記法
  • 強力な検索機能: CSS セレクタや正規表現をサポート
  • 複数パーサー対応: lxml、html.parser、html5libに対応

インストールと基本設定

ライブラリのインストール

pip install beautifulsoup4 lxml requests

基本的なインポート

from bs4 import BeautifulSoup
import requests

基本的な使い方

HTMLの読み込み

html = "<html><body><h1>Hello World</h1></body></html>"
soup = BeautifulSoup(html, 'html.parser')
print(soup.prettify())

Webページの取得と解析

url = "https://quotes.toscrape.com/"
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
print(soup.title.text)  # Quotes to Scrape

ファイルからの読み込み

with open('sample.html', 'r', encoding='utf-8') as file:
    soup = BeautifulSoup(file, 'html.parser')

パーサーの選択

html.parser(標準)

soup = BeautifulSoup(html, 'html.parser')
# 利点: 標準ライブラリ、高速
# 欠点: 古いPythonでは機能制限

lxml(推奨)

soup = BeautifulSoup(html, 'lxml')
# 利点: 非常に高速、豊富な機能
# 欠点: 外部依存

html5lib(最も厳密)

soup = BeautifulSoup(html, 'html5lib')
# 利点: ブラウザと同等の解析
# 欠点: 低速

要素の検索

find() – 最初の要素を取得

soup = BeautifulSoup('<div class="content"><p>段落1</p><p>段落2</p></div>', 'html.parser')
first_p = soup.find('p')
print(first_p.text)  # 段落1

find_all() – すべての要素を取得

all_p = soup.find_all('p')
for p in all_p:
    print(p.text)  # 段落1, 段落2

属性での検索

html = '<div class="main"><span id="title">タイトル</span></div>'
soup = BeautifulSoup(html, 'html.parser')

# クラスで検索
main_div = soup.find('div', class_='main')
# IDで検索
title_span = soup.find('span', id='title')
print(title_span.text)  # タイトル

複数属性での検索

html = '<a href="https://example.com" class="external" target="_blank">リンク</a>'
soup = BeautifulSoup(html, 'html.parser')
link = soup.find('a', {'class': 'external', 'target': '_blank'})
print(link['href'])  # https://example.com

CSSセレクタの使用

基本的なセレクタ

html = '<div class="container"><p class="text">内容1</p><p class="text">内容2</p></div>'
soup = BeautifulSoup(html, 'html.parser')

# クラスセレクタ
texts = soup.select('.text')
print([t.text for t in texts])  # ['内容1', '内容2']

子要素・子孫要素の選択

html = '<div><ul><li>項目1</li><li>項目2</li></ul></div>'
soup = BeautifulSoup(html, 'html.parser')

# 子孫セレクタ
items = soup.select('div li')
# 直接子セレクタ
items = soup.select('ul > li')
print([item.text for item in items])  # ['項目1', '項目2']

属性セレクタ

html = '<input type="text" name="username"><input type="password" name="password">'
soup = BeautifulSoup(html, 'html.parser')

# 属性値での選択
text_input = soup.select('input[type="text"]')[0]
print(text_input['name'])  # username

疑似セレクタ

html = '<ul><li>1番目</li><li>2番目</li><li>3番目</li></ul>'
soup = BeautifulSoup(html, 'html.parser')

first_item = soup.select('li:first-child')[0]
last_item = soup.select('li:last-child')[0]
print(first_item.text, last_item.text)  # 1番目 3番目

属性とテキストの取得

属性の取得

html = '<a href="https://example.com" title="例サイト">リンク</a>'
soup = BeautifulSoup(html, 'html.parser')
link = soup.find('a')

print(link['href'])     # https://example.com
print(link.get('title'))  # 例サイト
print(link.get('class', 'なし'))  # なし(デフォルト値)

テキストの取得

html = '<div>外側<p>内側<span>最内側</span></p></div>'
soup = BeautifulSoup(html, 'html.parser')
div = soup.find('div')

print(div.text)          # 外側内側最内側
print(div.get_text())    # 外側内側最内側
print(div.get_text(separator=' '))  # 外側 内側最内側

HTMLタグを含むテキスト

html = '<p>これは<strong>重要</strong>な文章です</p>'
soup = BeautifulSoup(html, 'html.parser')
p = soup.find('p')

print(str(p))            # <p>これは<strong>重要</strong>な文章です</p>
print(p.decode_contents())  # これは<strong>重要</strong>な文章です

ナビゲーション

親要素・子要素の取得

html = '<div><p>段落<span>スパン</span></p></div>'
soup = BeautifulSoup(html, 'html.parser')
span = soup.find('span')

print(span.parent.name)    # p
print(span.parent.parent.name)  # div

兄弟要素の取得

html = '<div><h1>タイトル</h1><p>段落1</p><p>段落2</p></div>'
soup = BeautifulSoup(html, 'html.parser')
h1 = soup.find('h1')

next_sibling = h1.find_next_sibling()
print(next_sibling.text)  # 段落1

子要素の反復処理

html = '<ul><li>項目1</li><li>項目2</li><li>項目3</li></ul>'
soup = BeautifulSoup(html, 'html.parser')
ul = soup.find('ul')

for child in ul.children:
    if child.name == 'li':
        print(child.text)  # 項目1, 項目2, 項目3

実践的なスクレイピング例

1. ニュースサイトのヘッドライン抽出

import requests
from bs4 import BeautifulSoup

def scrape_headlines(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    
    headlines = soup.find_all('h2', class_='headline')
    return [h.text.strip() for h in headlines]

# headlines = scrape_headlines('https://news.example.com')

2. テーブルデータの抽出

def extract_table_data(html):
    soup = BeautifulSoup(html, 'html.parser')
    table = soup.find('table')
    
    headers = [th.text.strip() for th in table.find('tr').find_all('th')]
    rows = []
    
    for tr in table.find_all('tr')[1:]:
        row = [td.text.strip() for td in tr.find_all('td')]
        rows.append(dict(zip(headers, row)))
    
    return rows

3. 商品情報の抽出

def scrape_product_info(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    
    product = {
        'name': soup.find('h1', class_='product-title').text.strip(),
        'price': soup.find('span', class_='price').text.strip(),
        'description': soup.find('div', class_='description').text.strip()
    }
    return product

4. フォームの情報抽出

def extract_form_fields(html):
    soup = BeautifulSoup(html, 'html.parser')
    form = soup.find('form')
    
    fields = []
    for input_tag in form.find_all(['input', 'select', 'textarea']):
        field_info = {
            'type': input_tag.get('type', input_tag.name),
            'name': input_tag.get('name'),
            'required': input_tag.has_attr('required')
        }
        fields.append(field_info)
    
    return fields

正規表現との組み合わせ

テキストパターンでの検索

import re

html = '<div>電話: 03-1234-5678</div><div>メール: test@example.com</div>'
soup = BeautifulSoup(html, 'html.parser')

# 正規表現でテキスト検索
phone_div = soup.find(text=re.compile(r'\d{2,3}-\d{4}-\d{4}'))
print(phone_div.strip())  # 電話: 03-1234-5678

属性値のパターンマッチ

html = '<a href="/page1">リンク1</a><a href="/page2">リンク2</a><a href="http://external.com">外部</a>'
soup = BeautifulSoup(html, 'html.parser')

# 内部リンクのみ抽出
internal_links = soup.find_all('a', href=re.compile(r'^/'))
print([link.text for link in internal_links])  # ['リンク1', 'リンク2']

カスタム関数での検索

def has_phone_number(tag):
    return tag.string and re.search(r'\d{3}-\d{4}-\d{4}', tag.string)

html = '<p>連絡先: 080-1234-5678</p><p>住所: 東京都</p>'
soup = BeautifulSoup(html, 'html.parser')
phone_tag = soup.find(has_phone_number)
print(phone_tag.text)  # 連絡先: 080-1234-5678

HTMLの編集と作成

要素の編集

html = '<p>古いテキスト</p>'
soup = BeautifulSoup(html, 'html.parser')
p = soup.find('p')

p.string = '新しいテキスト'
print(soup)  # <p>新しいテキスト</p>

属性の追加・変更

html = '<img src="old.jpg">'
soup = BeautifulSoup(html, 'html.parser')
img = soup.find('img')

img['src'] = 'new.jpg'
img['alt'] = '新しい画像'
print(soup)  # <img alt="新しい画像" src="new.jpg"/>

新要素の作成と追加

soup = BeautifulSoup('<div></div>', 'html.parser')
div = soup.find('div')

# 新要素の作成
new_p = soup.new_tag('p')
new_p.string = '新しい段落'

# 要素の追加
div.append(new_p)
print(soup)  # <div><p>新しい段落</p></div>

要素の削除

html = '<div><p>保持</p><p class="remove">削除対象</p></div>'
soup = BeautifulSoup(html, 'html.parser')

# 要素の削除
remove_p = soup.find('p', class_='remove')
remove_p.decompose()  # またはremove_p.extract()
print(soup)  # <div><p>保持</p></div>

パフォーマンス最適化

パーサーの選択による最適化

import time

html = '<html>' + '<p>段落</p>' * 1000 + '</html>'

# lxmlパーサー(最高速)
start = time.time()
soup_lxml = BeautifulSoup(html, 'lxml')
print(f"lxml: {time.time() - start:.4f}秒")

# html.parserパーサー
start = time.time()
soup_html = BeautifulSoup(html, 'html.parser')
print(f"html.parser: {time.time() - start:.4f}秒")

SoupStrainerによる部分解析

from bs4 import SoupStrainer

# テーブルタグのみ解析
only_tables = SoupStrainer('table')
soup = BeautifulSoup(large_html, 'lxml', parse_only=only_tables)

効率的な検索方法

# 効率的: 具体的なセレクタ
fast_result = soup.select('div.container > p.text')

# 非効率的: 曖昧な検索
slow_result = soup.find_all(lambda tag: tag.name == 'p' and 'text' in tag.get('class', []))

エラーハンドリング

要素が見つからない場合の処理

def safe_extract_text(soup, selector, default=''):
    element = soup.select_one(selector)
    return element.text.strip() if element else default

# 使用例
title = safe_extract_text(soup, 'h1.title', 'タイトル不明')

属性の安全な取得

def safe_get_attr(element, attr, default=''):
    return element.get(attr, default) if element else default

# 使用例
img = soup.find('img')
src = safe_get_attr(img, 'src', 'no-image.jpg')

例外処理の実装

from requests.exceptions import RequestException

def scrape_with_error_handling(url):
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        
        soup = BeautifulSoup(response.content, 'html.parser')
        title = soup.find('title')
        
        return title.text.strip() if title else 'タイトルなし'
        
    except RequestException as e:
        print(f"リクエストエラー: {e}")
        return None
    except Exception as e:
        print(f"解析エラー: {e}")
        return None

高度なテクニック

名前空間の処理(XML)

xml = '''<root xmlns:book="http://example.com/book">
    <book:title>Python入門</book:title>
    <book:author>著者名</book:author>
</root>'''

soup = BeautifulSoup(xml, 'lxml-xml')
title = soup.find('title')
print(title.text)  # Python入門

カスタムパーサーの作成

from bs4 import BeautifulSoup
from bs4.builder import TreeBuilder

class CustomTreeBuilder(TreeBuilder):
    def __init__(self):
        super().__init__()
        # カスタム処理の実装
    
    # 必要なメソッドをオーバーライド

メモリ効率的な大量データ処理

def process_large_html_iteratively(html_content):
    # 大きなHTMLを小さなチャンクに分割して処理
    chunk_size = 1000000  # 1MB chunks
    
    for i in range(0, len(html_content), chunk_size):
        chunk = html_content[i:i+chunk_size]
        soup = BeautifulSoup(chunk, 'lxml')
        
        # 必要なデータを抽出
        yield extract_data_from_chunk(soup)
        
        # メモリ解放
        soup.decompose()

実際のプロジェクト例

Webスクレイピングクラス

class WebScraper:
    def __init__(self, base_url, headers=None):
        self.base_url = base_url
        self.session = requests.Session()
        if headers:
            self.session.headers.update(headers)
    
    def get_soup(self, path=''):
        url = self.base_url + path
        response = self.session.get(url)
        return BeautifulSoup(response.content, 'lxml')
    
    def extract_links(self, soup, selector='a'):
        links = soup.select(selector)
        return [link.get('href') for link in links if link.get('href')]
    
    def extract_text_list(self, soup, selector):
        elements = soup.select(selector)
        return [elem.text.strip() for elem in elements]

データ構造化クラス

class HTMLDataExtractor:
    def __init__(self, html):
        self.soup = BeautifulSoup(html, 'lxml')
    
    def to_dict(self, selectors):
        """セレクタ辞書からデータを抽出"""
        result = {}
        for key, selector in selectors.items():
            element = self.soup.select_one(selector)
            result[key] = element.text.strip() if element else None
        return result
    
    def extract_list(self, item_selector, data_selectors):
        """リスト形式のデータを抽出"""
        items = self.soup.select(item_selector)
        results = []
        
        for item in items:
            data = {}
            for key, selector in data_selectors.items():
                element = item.select_one(selector)
                data[key] = element.text.strip() if element else None
            results.append(data)
        
        return results

デバッグとトラブルシューティング

HTMLの構造確認

def debug_html_structure(soup, max_depth=3):
    def print_tree(element, depth=0):
        if depth > max_depth:
            return
        
        indent = "  " * depth
        if element.name:
            attrs = " ".join([f'{k}="{v}"' for k, v in element.attrs.items()])
            print(f"{indent}<{element.name} {attrs}>")
            
            for child in element.children:
                if child.name:
                    print_tree(child, depth + 1)
    
    print_tree(soup)

セレクタのテスト

def test_selectors(soup, selectors):
    """複数のセレクタをテスト"""
    for name, selector in selectors.items():
        elements = soup.select(selector)
        print(f"{name}: {len(elements)}個の要素が見つかりました")
        
        if elements:
            print(f"  最初の要素: {elements[0].name} - {elements[0].text[:50]}")

パフォーマンス測定

import time
from functools import wraps

def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__}: {end - start:.4f}秒")
        return result
    return wrapper

@measure_time
def scrape_page(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'lxml')
    return soup.find_all('p')

まとめ

BeautifulSoupは、PythonでHTMLを操作するための非常に強力で使いやすいライブラリです。基本的な要素検索から高度なデータ抽出まで、幅広い用途に対応できます。

主なポイント:

  • 柔軟な検索: find()、select()、正規表現を組み合わせた強力な検索
  • 直感的なAPI: Pythonらしい読みやすい記法
  • 豊富な機能: ナビゲーション、編集、作成機能
  • パフォーマンス: 適切なパーサー選択と最適化テクニック
  • エラー処理: 堅牢なスクレイピングのための例外処理

Webスクレイピングを行う際は、対象サイトの利用規約を確認し、適切なマナーを守って使用することが重要です。BeautifulSoupの強力な機能を活用して、効率的なデータ抽出を実現しましょう。

参考リンク

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座