4日目



# Pythonによるウェブスクレイピング実装で学んだこと - 再帰制限との戦いと構造化の重要性

## はじめに

今回は、Pythonでウェブスクレイピングを実装した際の経験と学びを共有したいと思います。
特に、再帰処理での躓きと、それを乗り越えた過程で得られた知見をまとめてみました。

## 実装の背景

DataikuのドキュメントサイトからURLを収集する必要があり、Pythonでスクレイピングを実装することになりました。最初は単純そうに見えた作業でしたが、思わぬ壁にぶつかることに...

## 実装内容

### 1. 基本的な構成

```python
import requests
from bs4 import BeautifulSoup
import os
import logging
import json
from datetime import datetime
from urllib.parse import urljoin
import sys

# ベースURL設定
base_url = "https://doc.dataiku.com/dss/latest/"
start_url = "https://doc.dataiku.com/dss/latest/concepts/index.html"

# データ構造の準備
visited_urls = set()
collected_urls = []
collected_pages = []
```

### 2. メイン処理の実装

```python
def scrape_page(url, parent_url=None):
    """ページをスクレイピングする"""
    url = normalize_url(url)
    if url in visited_urls:
        logging.info(f"Skipping already visited URL: {url}")
        return
    
    visited_urls.add(url)
    collected_urls.append(url)
    
    try:
        response = requests.get(url)
        response.raise_for_status()
        soup = BeautifulSoup(response.content, 'html.parser')
        
        # ページ情報を収集
        title = soup.find('title')
        title_text = title.text.strip() if title else "No title"
        
        page_info = {
            'url': url,
            'title': title_text,
            'parent_url': parent_url,
            'timestamp': datetime.now().isoformat()
        }
        collected_pages.append(page_info)
        
        # メニュー内のリンクを取得
        menu = soup.find('div', class_='wy-menu-vertical')
        if menu:
            for link in menu.find_all('a', class_='reference internal', href=True):
                href = link['href']
                if href.startswith('#'):
                    continue
                
                full_url = urljoin(url, href)
                if full_url.startswith(base_url):
                    scrape_page(full_url, url)
                    
    except requests.RequestException as e:
        logging.error(f"Failed to retrieve {url}: {e}")
```

## 直面した問題と解決策

### 1. セレクタの問題
最初は`ul class="current"`を探していましたが、実際のメニューは`div class="wy-menu-vertical"`にありました。
HTML構造をしっかり確認することで解決！

### 2. 再帰制限エラー
```
ERROR - 予期せぬエラーが発生しました: maximum recursion depth exceeded while calling a Python object
```

このエラーに遭遇し、以下の対策で解決しました：

```python
import sys
sys.setrecursionlimit(10000)  # 再帰制限を増やす
```

### 3. データの構造化
収集したデータを整理して保存する仕組みを実装：

```python
def save_results(output_dir):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    # URLリストをテキストファイルとして保存
    url_file = os.path.join(output_dir, f"url_list_{timestamp}.txt")
    with open(url_file, "w", encoding='utf-8') as f:
        for url in sorted(collected_urls):
            f.write(f"{url}\n")
            
    # 詳細情報をJSONファイルとして保存
    json_file = os.path.join(output_dir, f"pages_info_{timestamp}.json")
    with open(json_file, "w", encoding="utf-8") as f:
        json_data = {
            'timestamp': datetime.now().isoformat(),
            'total_pages': len(collected_pages),
            'pages': collected_pages
        }
        json.dump(json_data, f, indent=2, ensure_ascii=False)
```

## 得られた学び

1. **事前調査の重要性**
   - HTML構造は実際に確認するまで分からない
   - `curl`などでの事前確認が重要

2. **エラーハンドリングの大切さ**
   - ネットワークエラー
   - 再帰制限
   - 予期せぬHTML構造の変化
   
3. **データ管理の工夫**
   - 重複排除（`set`の活用）
   - 構造化データの保存（JSON）
   - タイムスタンプ付きの命名

4. **スクレイピングの作法**
   - アクセス間隔の考慮
   - エラー時のログ記録
   - クリーンな終了処理

## 最終的な成果

- 収集したURL数：1077件
- 出力ファイル：
  - URLリスト（テキスト形式）
  - ページ情報（JSON形式）

## まとめ

ウェブスクレイピングは単純そうに見えて、実際にはさまざまな考慮が必要な作業でした。特に、再帰処理やエラーハンドリング、データの構造化など、プログラミングの基本的な要素が詰まっていて、とても勉強になりました！

次回は、今回の経験を活かして、より効率的なスクレイピング手法に挑戦してみたいと思います。

## 参考リンク
- [Beautiful Soup Documentation](https://www.crummy.com/software/BeautifulSoup/bs4/doc/)
- [Python Requests Documentation](https://docs.python-requests.org/en/latest/)
- [Python sys.setrecursionlimit](https://docs.python.org/3/library/sys.html#sys.setrecursionlimit)

---