Python自動化マスター 第3編:Webスクレイピング基礎
Python Automation Master Part 3: Web Scraping Basics
はじめに: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スクレイピング技術を学びましょう!