Python Web クローリング実装:urllib から Scrapy までの進化

HTTP リクエストライブラリの比較と選定

Python において Web クローリングや HTTP 通信を行う場合、標準ライブラリからサードパーティ製の強力なライブラリまで、複数の選択肢が存在します。主に urlliburllib2requests、そしてフレームワークである Scrapy が挙げられます。それぞれの特性を理解し、目的に応じて使い分けることが重要です。

urllib と urllib2 の違い(Python 2 における歴史的背景)

Python 2 の時代には、URL 処理に関連する機能として urlliburllib2 という 2 つの異なるモジュールが存在しました。これらは互換性がなく、役割が分担されていました。

  • urllib2: Request オブジェクトを受け取り、HTTP ヘッダー(User-Agent など)のカスタマイズが可能でした。
  • urllib: URL 文字列のみを受け取り、主に URL エンコード(urlencode)などのユーティリティ機能を提供していました。

このため、Python 2 ではヘッダー設定のために urllib2 を、クエリパラメータの生成のために urllib を併用するのが一般的でした。なお、Python 3 에서는これらが統合され、urllib.request および urllib.parse として再編されています。

標準ライブラリによる HTTP 通信

標準ライブラリを使用すると、外部依存なしで通信が可能です。以下は、Python 3 の統合された urllib モジュールを使用した GET および POST リクエストの例です。

import urllib.request
import urllib.parse

def fetch_data(url, params=None):
    """URL からデータを取得する関数"""
    if params:
        # クエリパラメータのエンコード
        query_string = urllib.parse.urlencode(params)
        full_url = f"{url}?{query_string}"
        request = urllib.request.Request(full_url)
    else:
        request = urllib.request.Request(url)

    # User-Agent の設定などヘッダー操作も可能
    request.add_header('User-Agent', 'Mozilla/5.0')

    try:
        with urllib.request.urlopen(request) as response:
            return response.read().decode('utf-8')
    except urllib.error.HTTPError as e:
        return f"Error: {e.code}"

# 使用例
data = fetch_data('https://example.com/search', {'q': 'python', 'page': 1})
print(data)

requests ライブラリの活用

標準ライブラリは機能豊富ですが、記述が冗長になりがちです。requests ライブラリは、HTTP 通信を人間のために最適化された API でラップしており、非常に簡潔に記述できます。Cookie の管理やセッション維持も容易です。

import requests
import json

def send_post_request(endpoint, payload):
    """JSON データを POST 送信する"""
    headers = {'Content-Type': 'application/json'}
    response = requests.post(endpoint, data=json.dumps(payload), headers=headers)
    
    # 状態コードの確認
    if response.status_code == 200:
        return response.json()
    else:
        return None

# 使用例
payload = {'user_id': 101, 'action': 'login'}
result = send_post_request('https://httpbin.org/post', payload)
if result:
    print(f"Response: {result.get('json')}")

また、requests.Session オブジェクトを使用することで、複数のリクエスト間で Cookie や接続を維持することができ、ログイン状態を保持したままのクローリングなどに適しています。

Scrapy フレームワークの架构

大規模なクローリングやデータ抽出を行う場合、Scrapy フレームワークが有効です。Scrapy は非同期処理エンジンに基づいており、効率的なデータ収集が可能です。

主要コンポーネント

  • Engine: データフロー全体を制御する中枢。
  • Scheduler: リクエストをキューイングし、重複を除去して優先順位を管理。
  • Downloader: 実際の HTTP リクエストを実行し、レスポンスを取得。
  • Spiders: 具体的な抽出ロジックを記述するクラス。レスポンスを解析し、データまたは新しいリクエストを生成。
  • Item Pipeline: 抽出されたデータの検証、クリーニング、保存(DB やファイル)を担当。
  • Middlewares: リクエストやレスポンス、スパイダー処理の中間でフック処理を行う。

データフロー

  1. Engine が Spider から初期リクエストを取得し、Scheduler に渡す。
  2. Scheduler から次のリクエストを取り出し、Downloader へ送信。
  3. Downloader がレスポンスを返すと、Engine はそれを Spider に渡す。
  4. Spider はレスポンスを解析し、抽出データ(Item)または次のリクエストを Engine に返す。
  5. Item は Pipeline へ、リクエストは Scheduler へそれぞれ送られ、処理が継続する。

Scrapy による実装例

プロジェクトを作成するには、コマンドラインから scrapy startproject project_name を実行します。生成された構造に従って、Spider と Item を定義します。

Item の定義

抽出するデータの構造を items.py で定義します。

import scrapy

class ProductItem(scrapy.Item):
    product_name = scrapy.Field()
    price = scrapy.Field()
    product_url = scrapy.Field()
    category = scrapy.Field()

Spider の実装

特定のドメインに対してクローリングを行う Spider クラスを作成します。ここでは、ページ内のリンクをたどりながらデータを抽出するロジックを記述します。

import scrapy
from ..items import ProductItem

class GenericShopSpider(scrapy.Spider):
    name = "generic_shop"
    allowed_domains = ["example-shop.com"]
    start_urls = ["https://www.example-shop.com/products"]

    def parse(self, response):
        # 商品リストページの解析
        products = response.css('div.product-item')
        
        for product in products:
            item = ProductItem()
            item['product_name'] = product.css('h2.title::text').get()
            item['price'] = product.css('span.price::text').get()
            item['product_url'] = response.urljoin(product.css('a::attr(href)').get())
            item['category'] = response.css('div.category::text').get()
            
            yield item

        # 次のページへのリンクがあれば追随
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

Item Pipeline によるデータ保存

抽出されたデータは Pipeline で処理されます。ここでは、データを JSON ファイルに保存する例と、データベースへ挿入する準備を行う例を示します。

import json
import pymysql

class JsonExportPipeline:
    def __init__(self):
        self.file = open('products.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + "\n"
        self.file.write(line)
        return item

    def close_spider(self, spider):
        self.file.close()

class DatabasePipeline:
    def __init__(self, db_config):
        self.db_config = db_config
        self.conn = None
        self.cursor = None

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            db_config=crawler.settings.getdict('DB_SETTINGS')
        )

    def open_spider(self, spider):
        self.conn = pymysql.connect(**self.db_config)
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = "INSERT INTO products (name, price, url, category) VALUES (%s, %s, %s, %s)"
        params = (
            item.get('product_name'),
            item.get('price'),
            item.get('product_url'),
            item.get('category')
        )
        try:
            self.cursor.execute(sql, params)
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            print(f"DB Error: {e}")
        return item

    def close_spider(self, spider):
        if self.conn:
            self.conn.close()

設定ファイルの調整

settings.py でパイプラインの有効化や、ダウンロード遅延、同時請求数などを設定します。

ITEM_PIPELINES = {
    'project.pipelines.JsonExportPipeline': 300,
    'project.pipelines.DatabasePipeline': 400,
}

DOWNLOAD_DELAY = 1
CONCURRENT_REQUESTS = 16
DB_SETTINGS = {
    'host': 'localhost',
    'user': 'root',
    'password': 'password',
    'db': 'scrapy_db',
    'charset': 'utf8mb4'
}

タグ: Python web-scraping Scrapy requests urllib

7月11日 22:28 投稿