HTTP リクエストライブラリの比較と選定
Python において Web クローリングや HTTP 通信を行う場合、標準ライブラリからサードパーティ製の強力なライブラリまで、複数の選択肢が存在します。主に urllib、urllib2、requests、そしてフレームワークである Scrapy が挙げられます。それぞれの特性を理解し、目的に応じて使い分けることが重要です。
urllib と urllib2 の違い(Python 2 における歴史的背景)
Python 2 の時代には、URL 処理に関連する機能として urllib と urllib2 という 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: リクエストやレスポンス、スパイダー処理の中間でフック処理を行う。
データフロー
- Engine が Spider から初期リクエストを取得し、Scheduler に渡す。
- Scheduler から次のリクエストを取り出し、Downloader へ送信。
- Downloader がレスポンスを返すと、Engine はそれを Spider に渡す。
- Spider はレスポンスを解析し、抽出データ(Item)または次のリクエストを Engine に返す。
- 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'
}