はじめに:Webスクレイピングとは?

Webスクレイピング(Web Scraping)とは、Webサイトから必要なデータを自動的に抽出する技術です。Webクローリング(Web Crawling)とも呼ばれ、膨大な量のWebデータを効率的に収集できるため、データ分析、価格モニタリング、ニュース収集など、さまざまな分野で活用されています。

今回は、Pythonを使ったWebスクレイピングの基礎を学びます。requestsライブラリでWebページを取得し、BeautifulSoupでHTMLをパースして目的のデータを抽出する方法をステップバイステップで解説します。

1. 法的・倫理的考慮事項

Webスクレイピングを始める前に、必ず知っておくべき法的・倫理的考慮事項があります。

1.1 robots.txtの確認

robots.txtは、Webサイトのルートディレクトリに配置されたファイルで、Webクローラーがアクセスできるページとアクセスしてはいけないページを明示しています。

# robots.txtの例 (https://example.com/robots.txt)
User-agent: *
Disallow: /private/
Disallow: /admin/
Allow: /public/

Crawl-delay: 10
  • User-agent:ルールが適用されるクローラーを指定します。*はすべてのクローラーを意味します。
  • Disallow:クローリングが禁止されたパスを指定します。
  • Allow:クローリングが許可されたパスを指定します。
  • Crawl-delay:リクエスト間の待機時間(秒)を指定します。

1.2 Webスクレイピング倫理規範

  • robots.txtの尊重:Webサイトのrobots.txtルールを必ず確認し、遵守します。
  • サーバー負荷の最小化:リクエスト間に適切なディレイを設けて、サーバーに過負荷をかけないようにします。
  • 利用規約の確認:Webサイトの利用規約でスクレイピング関連の条項を確認します。
  • 個人情報保護:個人情報を無断で収集しません。
  • 著作権の尊重:収集したデータの著作権を尊重し、適切に使用します。

1.3 Pythonでrobots.txtを確認する

from urllib.robotparser import RobotFileParser

def check_robots_txt(url, user_agent='*'):
    """robots.txtを確認してクローリング可否を返します。"""
    # robots.txt URLを生成
    from urllib.parse import urlparse
    parsed = urlparse(url)
    robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"

    # RobotFileParserを設定
    rp = RobotFileParser()
    rp.set_url(robots_url)
    rp.read()

    # 該当URLのクローリング可否を確認
    can_fetch = rp.can_fetch(user_agent, url)
    crawl_delay = rp.crawl_delay(user_agent)

    return {
        'can_fetch': can_fetch,
        'crawl_delay': crawl_delay
    }

# 使用例
result = check_robots_txt('https://www.google.com/search')
print(f"クローリング可能:{result['can_fetch']}")
print(f"クローリングディレイ:{result['crawl_delay']}秒")

2. requestsライブラリ

requestsは、PythonでHTTPリクエストを送信するための最も人気のあるライブラリです。シンプルで直感的なAPIを提供し、Webページを簡単に取得できます。

2.1 インストール

# pipでインストール
pip install requests

2.2 HTTPメソッド

HTTPプロトコルで使用される主要なメソッドとrequestsでの使い方を学びます。

GETリクエスト

最も一般的なリクエスト方式で、サーバーからデータを取得する際に使用します。

import requests

# 基本的なGETリクエスト
response = requests.get('https://httpbin.org/get')
print(response.text)

# クエリパラメータ付きGETリクエスト
params = {
    'search': 'python',
    'page': 1,
    'limit': 10
}
response = requests.get('https://httpbin.org/get', params=params)
print(response.url)  # https://httpbin.org/get?search=python&page=1&limit=10

POSTリクエスト

サーバーにデータを送信する際に使用します。フォームデータやJSONデータを送ることができます。

import requests

# フォームデータ送信
form_data = {
    'username': 'user123',
    'password': 'pass456'
}
response = requests.post('https://httpbin.org/post', data=form_data)
print(response.json())

# JSONデータ送信
json_data = {
    'name': '田中太郎',
    'email': 'tanaka@example.com'
}
response = requests.post('https://httpbin.org/post', json=json_data)
print(response.json())

ヘッダー設定

User-AgentなどのHTTPヘッダーを設定してリクエストをカスタマイズできます。

import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
    'Referer': 'https://www.google.com/'
}

response = requests.get('https://httpbin.org/headers', headers=headers)
print(response.json())

2.3 レスポンス処理

requestsは、さまざまな形式でレスポンスを処理できる機能を提供しています。

import requests

response = requests.get('https://httpbin.org/get')

# ステータスコードを確認
print(f"ステータスコード:{response.status_code}")  # 200

# 成功かどうかを確認
if response.ok:  # status_codeが200-299の範囲の場合True
    print("リクエスト成功!")

# レスポンス本文(テキスト)
print(response.text)

# レスポンス本文(JSON)
data = response.json()
print(data)

# レスポンス本文(バイナリ)- 画像など
content = response.content

# レスポンスヘッダー
print(response.headers)
print(response.headers['Content-Type'])

# エンコーディング
print(response.encoding)  # UTF-8
response.encoding = 'utf-8'  # エンコーディングを指定

HTTPステータスコード

ステータスコード 意味 説明
200 OK リクエスト成功
301 Moved Permanently 永久的に移動
302 Found 一時的なリダイレクト
400 Bad Request 不正なリクエスト
403 Forbidden アクセス権限なし
404 Not Found ページが見つからない
500 Internal Server Error サーバー内部エラー

2.4 エラー処理とタイムアウト

import requests
from requests.exceptions import RequestException, Timeout, HTTPError

def safe_request(url, timeout=10):
    """安全なHTTPリクエストを実行します。"""
    try:
        response = requests.get(url, timeout=timeout)
        response.raise_for_status()  # 4xx、5xxエラー時に例外を発生
        return response
    except Timeout:
        print(f"タイムアウト発生:{url}")
    except HTTPError as e:
        print(f"HTTPエラー:{e.response.status_code}")
    except RequestException as e:
        print(f"リクエスト失敗:{e}")
    return None

# 使用例
response = safe_request('https://httpbin.org/get')
if response:
    print(response.text)

3. BeautifulSoupでHTMLをパース

BeautifulSoupは、HTMLとXMLドキュメントをパースするためのPythonライブラリです。複雑なHTML構造から目的のデータを簡単に抽出できます。

3.1 インストール

# BeautifulSoupとlxmlパーサーをインストール
pip install beautifulsoup4 lxml

3.2 基本的な使い方

from bs4 import BeautifulSoup
import requests

# Webページを取得
url = 'https://example.com'
response = requests.get(url)

# BeautifulSoupオブジェクトを作成
soup = BeautifulSoup(response.text, 'lxml')  # または 'html.parser'

# HTML構造を確認(きれいに出力)
print(soup.prettify())

3.3 タグで要素を検索

from bs4 import BeautifulSoup

html = """
<html>
<head><title>テストページ</title></head>
<body>
    <h1>メインタイトル</h1>
    <p>最初の段落</p>
    <p>2番目の段落</p>
    <a href="https://example.com">リンク1</a>
    <a href="https://google.com">リンク2</a>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 最初のタグを検索
title = soup.find('title')
print(title.text)  # テストページ

h1 = soup.find('h1')
print(h1.text)  # メインタイトル

# すべてのタグを検索
paragraphs = soup.find_all('p')
for p in paragraphs:
    print(p.text)

links = soup.find_all('a')
for link in links:
    print(link.text, link['href'])

3.4 クラスとIDで要素を検索

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <div id="header">ヘッダー領域</div>
    <div class="content">
        <p class="intro">紹介文</p>
        <p class="main-text">本文内容</p>
    </div>
    <div class="sidebar">サイドバー</div>
    <ul class="menu">
        <li class="menu-item active">ホーム</li>
        <li class="menu-item">紹介</li>
        <li class="menu-item">お問い合わせ</li>
    </ul>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# IDで検索
header = soup.find(id='header')
print(header.text)  # ヘッダー領域

# クラスで検索
content = soup.find(class_='content')
print(content.text)

# クラス名で複数の要素を検索
menu_items = soup.find_all(class_='menu-item')
for item in menu_items:
    print(item.text)

# 複合条件で検索
active_item = soup.find('li', class_='active')
print(active_item.text)  # ホーム

# CSSセレクターを使用(select)
main_text = soup.select_one('.content .main-text')
print(main_text.text)  # 本文内容

all_menu_items = soup.select('ul.menu li')
for item in all_menu_items:
    print(item.text)

3.5 属性アクセスとテキスト抽出

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <a href="https://example.com" title="サンプルリンク" data-id="123">
        <span>リンク</span>テキスト
    </a>
    <img src="image.jpg" alt="画像説明">
    <div class="article">
        <h2>タイトル</h2>
        <p>最初の段落</p>
        <p>2番目の段落</p>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 属性アクセス
link = soup.find('a')
print(link['href'])        # https://example.com
print(link['title'])       # サンプルリンク
print(link.get('data-id')) # 123
print(link.get('class'))   # None(なければNoneを返す)

# すべての属性を取得
print(link.attrs)  # {'href': 'https://example.com', 'title': 'サンプルリンク', 'data-id': '123'}

# 画像タグの属性
img = soup.find('img')
print(img['src'])  # image.jpg
print(img['alt'])  # 画像説明

# テキスト抽出
print(link.text)          # リンクテキスト(すべての子テキストを含む)
print(link.string)        # None(複数の子がある場合はNone)
print(link.get_text())    # リンクテキスト

# 空白を削除
print(link.get_text(strip=True))  # リンクテキスト

# 区切り文字で連結
article = soup.find(class_='article')
print(article.get_text(separator=' | ', strip=True))
# タイトル | 最初の段落 | 2番目の段落

3.6 CSSセレクターの高度な使い方

from bs4 import BeautifulSoup

html = """
<html>
<body>
    <table id="data-table">
        <tr><th>名前</th><th>年齢</th><th>職業</th></tr>
        <tr><td>田中太郎</td><td>30</td><td>開発者</td></tr>
        <tr><td>山田花子</td><td>25</td><td>デザイナー</td></tr>
        <tr><td>佐藤次郎</td><td>35</td><td>マネージャー</td></tr>
    </table>
    <div class="products">
        <div class="product" data-price="10000">
            <span class="name">商品 A</span>
        </div>
        <div class="product" data-price="20000">
            <span class="name">商品 B</span>
        </div>
    </div>
</body>
</html>
"""

soup = BeautifulSoup(html, 'lxml')

# 子孫セレクター(スペース)
cells = soup.select('#data-table td')
for cell in cells:
    print(cell.text)

# 直接の子セレクター(>)
rows = soup.select('#data-table > tr')

# 属性セレクター
products = soup.select('div[data-price]')
for product in products:
    name = product.select_one('.name').text
    price = product['data-price']
    print(f"{name}: {price}円")

# n番目の要素を選択
first_row = soup.select_one('#data-table tr:nth-child(2)')  # 最初のデータ行
print([td.text for td in first_row.find_all('td')])

# 複合セレクター
products_with_high_price = soup.select('.product[data-price="20000"]')
for product in products_with_high_price:
    print(product.select_one('.name').text)

4. 実践例:簡単なスクレイパーを作る

ここまで学んだ内容を総合して、実践的なスクレイパーを作ってみましょう。

4.1 ニュースヘッドライン収集器

import requests
from bs4 import BeautifulSoup
import time

class NewsHeadlineScraper:
    """ニュースのヘッドラインを収集するスクレイパー"""

    def __init__(self):
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
        self.session = requests.Session()
        self.session.headers.update(self.headers)

    def fetch_page(self, url):
        """Webページを取得します。"""
        try:
            response = self.session.get(url, timeout=10)
            response.raise_for_status()
            return response.text
        except requests.RequestException as e:
            print(f"ページリクエスト失敗:{e}")
            return None

    def parse_headlines(self, html, selector):
        """HTMLからヘッドラインを抽出します。"""
        soup = BeautifulSoup(html, 'lxml')
        headlines = []

        elements = soup.select(selector)
        for element in elements:
            title = element.get_text(strip=True)
            link = element.get('href', '')
            if title:
                headlines.append({
                    'title': title,
                    'link': link
                })

        return headlines

    def scrape(self, url, selector, delay=1):
        """指定されたURLからヘッドラインをスクレイピングします。"""
        print(f"スクレイピング開始:{url}")

        html = self.fetch_page(url)
        if not html:
            return []

        headlines = self.parse_headlines(html, selector)
        print(f"{len(headlines)}件のヘッドラインが見つかりました。")

        time.sleep(delay)  # サーバー負荷防止
        return headlines

# 使用例(実際の使用時は該当サイトの利用規約を確認してください)
if __name__ == '__main__':
    scraper = NewsHeadlineScraper()

    # 例(実際のURLとセレクターは対象サイトに合わせて修正)
    headlines = scraper.scrape(
        url='https://example.com/news',
        selector='a.headline'
    )

    for idx, headline in enumerate(headlines, 1):
        print(f"{idx}. {headline['title']}")
        print(f"   リンク:{headline['link']}")

4.2 テーブルデータ抽出器

import requests
from bs4 import BeautifulSoup
import csv

def extract_table_data(url, table_selector='table'):
    """Webページのテーブルデータを抽出します。"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }

    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, 'lxml')
    table = soup.select_one(table_selector)

    if not table:
        print("テーブルが見つかりません。")
        return []

    data = []
    rows = table.find_all('tr')

    for row in rows:
        # ヘッダーセルまたはデータセルを抽出
        cells = row.find_all(['th', 'td'])
        row_data = [cell.get_text(strip=True) for cell in cells]
        if row_data:
            data.append(row_data)

    return data

def save_to_csv(data, filename):
    """データをCSVファイルに保存します。"""
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerows(data)
    print(f"データが{filename}に保存されました。")

# 使用例
if __name__ == '__main__':
    # サンプルURL(実際の使用時は適切なURLに変更)
    data = extract_table_data(
        url='https://example.com/data-table',
        table_selector='#main-table'
    )

    if data:
        for row in data[:5]:  # 最初の5行のみ出力
            print(row)

        save_to_csv(data, 'extracted_data.csv')

4.3 画像ダウンローダー

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import os
import time

class ImageDownloader:
    """Webページから画像をダウンロードするクラス"""

    def __init__(self, download_dir='images'):
        self.download_dir = download_dir
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }

        # ダウンロードディレクトリを作成
        if not os.path.exists(download_dir):
            os.makedirs(download_dir)

    def get_image_urls(self, page_url, img_selector='img'):
        """ページから画像URLを抽出します。"""
        response = requests.get(page_url, headers=self.headers, timeout=10)
        response.raise_for_status()

        soup = BeautifulSoup(response.text, 'lxml')
        images = soup.select(img_selector)

        image_urls = []
        for img in images:
            src = img.get('src') or img.get('data-src')
            if src:
                # 相対URLを絶対URLに変換
                full_url = urljoin(page_url, src)
                image_urls.append(full_url)

        return image_urls

    def download_image(self, url, filename=None):
        """画像をダウンロードします。"""
        try:
            response = requests.get(url, headers=self.headers, timeout=30)
            response.raise_for_status()

            # ファイル名がなければURLから抽出
            if not filename:
                parsed = urlparse(url)
                filename = os.path.basename(parsed.path) or 'image.jpg'

            filepath = os.path.join(self.download_dir, filename)

            with open(filepath, 'wb') as f:
                f.write(response.content)

            print(f"ダウンロード完了:{filename}")
            return filepath

        except requests.RequestException as e:
            print(f"ダウンロード失敗({url}):{e}")
            return None

    def download_all(self, page_url, img_selector='img', delay=1):
        """ページのすべての画像をダウンロードします。"""
        image_urls = self.get_image_urls(page_url, img_selector)
        print(f"{len(image_urls)}件の画像が見つかりました。")

        downloaded = []
        for idx, url in enumerate(image_urls, 1):
            print(f"[{idx}/{len(image_urls)}] ダウンロード中...")
            filepath = self.download_image(url)
            if filepath:
                downloaded.append(filepath)
            time.sleep(delay)  # サーバー負荷防止

        return downloaded

# 使用例
if __name__ == '__main__':
    downloader = ImageDownloader(download_dir='downloaded_images')

    # サンプルURL(実際の使用時は適切なURLに変更)
    downloaded_files = downloader.download_all(
        page_url='https://example.com/gallery',
        img_selector='div.gallery img',
        delay=2
    )

    print(f"\n合計{len(downloaded_files)}件の画像をダウンロードしました。")

5. ヒントと注意事項

5.1 効率的なスクレイピングのためのヒント

  • Sessionの使用:同じサイトに複数回リクエストする場合は、requests.Session()を使用して接続を再利用します。
  • 適切なディレイ:リクエスト間にtime.sleep()を使用して、サーバーに過負荷をかけないようにします。
  • エラー処理:ネットワークエラー、タイムアウトなどに対する適切な例外処理を追加します。
  • キャッシュの活用:同じページを繰り返しリクエストしないように結果をキャッシュします。
  • ロギング:スクレイピング過程をロギングして、問題発生時のデバッグに活用します。

5.2 よくある問題と解決策

  • エンコーディング問題:response.encodingを明示的に設定するか、chardetライブラリを使用します。
  • 403 Forbidden:User-Agentヘッダーを設定するか、他のヘッダーを追加します。
  • 動的コンテンツ:JavaScriptでロードされるコンテンツはrequestsでは取得できません。次回のSeleniumを使用した解決方法を学びます。
  • IPブロック:リクエスト頻度を減らすか、プロキシを使用します。

まとめ

今回は、Python Webスクレイピングの基礎を学びました。requestsライブラリでWebページを取得し、BeautifulSoupでHTMLをパースして目的のデータを抽出する方法を習得しました。

次の第4編では、より複雑なWebスクレイピングシナリオを扱います。JavaScriptで動的に生成されるコンテンツを処理するためのSelenium、ログインが必要なサイトのスクレイピング、そして収集したデータを保存するさまざまな方法を学ぶ予定です。

シリーズのご案内:Python自動化マスターシリーズは続きます。次回は、より高度なWebスクレイピング技術を学びましょう!