📋 このステップで学ぶこと
- REST APIの基本概念と呼び出し方法
- requestsライブラリの使い方
- JSONレスポンスの解析とPandasへの変換
- ページネーションの実装パターン
- 認証方式(APIキー、Bearer Token、Basic認証)
- Beautiful Soupを使ったHTML解析
- 倫理的なWebスクレイピングのルール
🎯 1. REST APIの基本
1-1. REST APIとは?
REST API(RESTful API)は、Webサービスがデータを外部に公開するための仕組みです。
STEP 2で学んだ「レストランの注文」の例えを覚えていますか?APIはまさにその仕組みを使っています。
🍽️ 復習:レストランの注文フロー
あなた(クライアント)→ メニュー(API仕様書)を見て注文 →
ウェイター(API)が注文を受け取り →
キッチン(サーバー)で調理 →
料理(データ)が届く
1-2. HTTPリクエストの種類
REST APIでは、目的に応じて異なるHTTPメソッドを使います。
| メソッド |
目的 |
例 |
ETLでの使用頻度 |
| GET |
データを取得する |
ユーザー一覧を取得 |
★★★★★(非常に多い) |
| POST |
データを作成する |
新しいユーザーを登録 |
★★☆☆☆(時々) |
| PUT |
データを更新する |
ユーザー情報を更新 |
★☆☆☆☆(まれ) |
| DELETE |
データを削除する |
ユーザーを削除 |
★☆☆☆☆(まれ) |
📝 ETLではGETがメイン
ETLの「E(Extract:抽出)」では、GETメソッドを使ってデータを取得することがほとんどです。
POST、PUT、DELETEは、ETLの「L(Load:ロード)」で使うことがあります。
1-3. requestsライブラリのインストール
Pythonで REST API を呼び出すには、requestsライブラリを使います。
非常にシンプルで使いやすいライブラリです。
# pipでインストール
pip install requests
# Google Colabの場合(先頭に!をつける)
!pip install requests
1-4. 基本的なGETリクエスト
まずは、最もシンプルなAPIコールから始めましょう。
# ===== 基本的なGETリクエスト =====
import requests
# GETリクエストを送信(GitHubの公開API)
response = requests.get(‘https://api.github.com/users/octocat’)
# ステータスコードを確認
print(f”ステータスコード: {response.status_code}”)
# JSONデータを取得
data = response.json()
# データを表示
print(f”ユーザー名: {data[‘login’]}”)
print(f”名前: {data[‘name’]}”)
print(f”フォロワー数: {data[‘followers’]}”)
【実行結果】
ステータスコード: 200
ユーザー名: octocat
名前: The Octocat
フォロワー数: 9999
📝 コードの説明
requests.get(URL):指定したURLにGETリクエストを送信
response.status_code:HTTPステータスコード(200=成功)
response.json():レスポンスのJSONをPython辞書に変換
data['login']:辞書からデータを取り出す
1-5. HTTPステータスコード
APIからのレスポンスには、処理結果を示すステータスコードが含まれます。
| コード |
意味 |
説明・対処法 |
| 200 |
OK(成功) |
正常にデータが取得できた |
| 201 |
Created |
データの作成に成功した(POSTの成功) |
| 400 |
Bad Request |
リクエストの形式が間違っている → パラメータを確認 |
| 401 |
Unauthorized |
認証エラー → APIキーやトークンを確認 |
| 403 |
Forbidden |
アクセス権限がない → 権限を確認 |
| 404 |
Not Found |
リソースが見つからない → URLを確認 |
| 429 |
Too Many Requests |
レート制限に達した → 時間をおいて再試行 |
| 500 |
Internal Server Error |
サーバー側のエラー → 時間をおいて再試行 |
1-6. URLパラメータを指定する
多くのAPIは、パラメータを指定して検索条件や取得件数を制御できます。
# ===== パラメータを指定してリクエスト =====
import requests
# パラメータを辞書で定義
params = {
‘q’: ‘python’, # 検索キーワード
‘sort’: ‘stars’, # ソート順(スター数)
‘order’: ‘desc’, # 降順
‘per_page’: 5 # 1ページあたり5件
}
# パラメータ付きでリクエスト
response = requests.get(
‘https://api.github.com/search/repositories’,
params=params
)
data = response.json()
# 検索結果を表示
print(f”総件数: {data[‘total_count’]}件”)
print()
for repo in data[‘items’]:
print(f”リポジトリ名: {repo[‘name’]}”)
print(f”説明: {repo[‘description’][:50]}…”)
print(f”スター数: {repo[‘stargazers_count’]:,}”)
print(“—“)
📝 paramsの使い方
paramsに辞書を渡すと、自動的にURLの末尾に?q=python&sort=stars...のように追加されます。
URLに直接書くより、辞書で渡す方が読みやすく、エスケープも自動で行われます。
1-7. エラーハンドリング
本番環境では、ネットワークエラーやサーバーエラーに備えて、エラーハンドリングが必須です。
# ===== 安全なAPIコール =====
import requests
def fetch_api_data(url, params=None, timeout=10):
“””
安全にAPIからデータを取得する関数
Parameters:
url: APIのURL
params: URLパラメータ(辞書)
timeout: タイムアウト秒数
Returns:
成功時: JSONデータ(辞書)
失敗時: None
“””
try:
# リクエストを送信(タイムアウト設定は必須!)
response = requests.get(url, params=params, timeout=timeout)
# ステータスコードをチェック(200番台以外は例外を発生)
response.raise_for_status()
# JSONデータを返す
return response.json()
except requests.exceptions.Timeout:
print(f”エラー: タイムアウト({timeout}秒)”)
return None
except requests.exceptions.HTTPError as e:
print(f”HTTPエラー: {e}”)
return None
except requests.exceptions.ConnectionError:
print(“エラー: ネットワーク接続に失敗”)
return None
except requests.exceptions.RequestException as e:
print(f”リクエストエラー: {e}”)
return None
except ValueError:
print(“エラー: JSONのパースに失敗”)
return None
# 使用例
data = fetch_api_data(‘https://api.github.com/users/octocat’)
if data:
print(f”ユーザー名: {data[‘login’]}”)
else:
print(“データの取得に失敗しました”)
⚠️ timeout設定は必須!
timeoutを設定しないと、サーバーが応答しない場合に永遠に待ち続けます。
必ずtimeout=10のように秒数を指定しましょう(通常は5〜30秒)。
📄 2. JSONレスポンスの処理
2-1. JSONとは?
JSON(JavaScript Object Notation)は、Web APIで最も一般的なデータ形式です。
人間にも読みやすく、プログラムでも処理しやすい形式です。
【JSONの構造】
{
“user_id”: 1, ← 数値
“name”: “山田太郎”, ← 文字列
“email”: “yamada@example.com”, ← 文字列
“is_active”: true, ← 真偽値
“tags”: [“VIP”, “Premium”], ← 配列(リスト)
“orders”: [ ← 配列の中にオブジェクト
{
“order_id”: 101,
“product”: “ノートPC”,
“price”: 89800
},
{
“order_id”: 102,
“product”: “マウス”,
“price”: 1980
}
]
}
📝 JSONの基本ルール
- オブジェクト:
{ }で囲む(Pythonの辞書に対応)
- 配列:
[ ]で囲む(Pythonのリストに対応)
- キーは必ず
"ダブルクォート"で囲む
- 文字列も
"ダブルクォート"で囲む
- 数値、true/false、nullはそのまま
2-2. JSONからデータを取り出す
response.json()で取得したデータは、Pythonの辞書として扱えます。
# ===== JSONデータの解析 =====
import requests
response = requests.get(‘https://api.example.com/user/1’)
data = response.json()
# 単純な値を取得(辞書のキーでアクセス)
user_id = data[‘user_id’] # 1
name = data[‘name’] # “山田太郎”
email = data[‘email’] # “yamada@example.com”
is_active = data[‘is_active’] # True
# 配列(リスト)を取得
tags = data[‘tags’] # [“VIP”, “Premium”]
print(f”タグ: {tags[0]}”) # “VIP”
# ネストしたデータ(配列の中のオブジェクト)を取得
orders = data[‘orders’]
for order in orders:
order_id = order[‘order_id’]
product = order[‘product’]
price = order[‘price’]
print(f”注文ID: {order_id}, 商品: {product}, 価格: {price}円”)
2-3. 安全にデータを取り出す(.get()メソッド)
存在しないキーにアクセスするとエラーになります。
.get()メソッドを使うと、キーがない場合にデフォルト値を返せます。
# ===== 安全なデータ取得 =====
data = {‘name’: ‘山田太郎’, ‘email’: ‘yamada@example.com’}
# 存在しないキーにアクセス → エラー!
# phone = data[‘phone’] # KeyError: ‘phone’
# .get()を使えば安全
phone = data.get(‘phone’) # None(キーがない場合)
phone = data.get(‘phone’, ‘未設定’) # ‘未設定’(デフォルト値を指定)
print(f”名前: {data.get(‘name’, ‘不明’)}”)
print(f”電話: {data.get(‘phone’, ‘未設定’)}”)
✅ .get()を使うべき場面
- APIのレスポンスにオプショナルなフィールドがある場合
- データの形式が一定でない可能性がある場合
- エラーで処理を止めたくない場合
2-4. PandasのDataFrameに変換
APIから取得したJSONデータを、Pandas DataFrameに変換して分析できます。
# ===== JSONをDataFrameに変換 =====
import requests
import pandas as pd
# APIからデータ取得
response = requests.get(‘https://api.github.com/users/octocat/repos’)
data = response.json()
# JSONリストをDataFrameに変換
df = pd.DataFrame(data)
# 必要なカラムだけを選択
df_selected = df[[‘name’, ‘description’, ‘stargazers_count’, ‘language’]]
# カラム名を日本語に変更
df_selected.columns = [‘リポジトリ名’, ‘説明’, ‘スター数’, ‘言語’]
print(df_selected.head())
2-5. ネストしたJSONを平坦化する
複雑な階層構造のJSONを、平らな表形式に変換するにはjson_normalizeを使います。
# ===== ネストしたJSONの平坦化 =====
import pandas as pd
from pandas import json_normalize
# 複雑な構造のJSONデータ
data = {
‘user_id’: 1,
‘name’: ‘山田太郎’,
‘address’: {
‘city’: ‘東京’,
‘zip’: ‘100-0001’
},
‘orders’: [
{‘order_id’: 101, ‘product’: ‘ノートPC’, ‘price’: 89800},
{‘order_id’: 102, ‘product’: ‘マウス’, ‘price’: 1980}
]
}
# 方法1:基本的な平坦化
df1 = json_normalize(data)
print(“=== 基本的な平坦化 ===”)
print(df1.columns.tolist())
# [‘user_id’, ‘name’, ‘address.city’, ‘address.zip’, ‘orders’]
# 方法2:ネストしたリストを展開
df2 = json_normalize(
data,
record_path=[‘orders’], # 展開するリスト
meta=[‘user_id’, ‘name’] # 親の情報を含める
)
print(“\n=== ordersを展開 ===”)
print(df2)
【実行結果】
=== ordersを展開 ===
order_id product price user_id name
0 101 ノートPC 89800 1 山田太郎
1 102 マウス 1980 1 山田太郎
📝 json_normalizeのポイント
record_path:展開するネストしたリストのパス
meta:親レベルから含めるフィールド
- ネストした辞書は
address.cityのように「.」区切りで展開される
📑 3. ページネーション
3-1. ページネーションとは?
ページネーション(Pagination)とは、大量のデータを複数のページに分けて返す仕組みです。
1回のリクエストで全データを返すと、サーバーに負荷がかかるため、この仕組みが使われます。
📚 例え話:図書館の本棚
図書館に1万冊の本があるとします。
ページネーションなし:「全部の本を見せて!」と言われて、1万冊を一度に運ぶ → 大変!
ページネーションあり:「1ページ目(1〜100冊)を見せて」「次のページを見せて」と少しずつ → 効率的!
3-2. ページネーションの3つの方式
| 方式 |
仕組み |
使用例 |
| オフセット方式 |
offset(開始位置)とlimit(件数)を指定 |
多くのREST API |
| ページ番号方式 |
page(ページ番号)とper_page(1ページの件数)を指定 |
GitHub API、多くのWeb API |
| カーソル方式 |
次のデータを指すカーソル(トークン)を使用 |
Twitter API、Facebook API |
3-3. オフセット方式の実装
# ===== オフセット方式のページネーション =====
import requests
import time
def fetch_all_data_offset(base_url, page_size=100):
“””
オフセット方式で全データを取得する
Parameters:
base_url: APIのベースURL
page_size: 1ページあたりの件数
Returns:
全データのリスト
“””
all_data = []
offset = 0
while True:
# パラメータを指定
params = {
‘limit’: page_size, # 1ページあたりの件数
‘offset’: offset # 開始位置
}
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# データがなければ終了
if not data.get(‘items’, []):
break
# データを追加
all_data.extend(data[‘items’])
# 次のページへ
offset += page_size
print(f”取得済み: {len(all_data)}件”)
# レート制限対策(1秒待機)
time.sleep(1)
return all_data
# 使用例
# all_users = fetch_all_data_offset(‘https://api.example.com/users’)
# print(f”合計: {len(all_users)}件”)
3-4. ページ番号方式の実装
# ===== ページ番号方式のページネーション =====
import requests
import time
def fetch_all_data_page(base_url, per_page=100):
“””
ページ番号方式で全データを取得する
Parameters:
base_url: APIのベースURL
per_page: 1ページあたりの件数
Returns:
全データのリスト
“””
all_data = []
page = 1
while True:
# パラメータを指定
params = {
‘page’: page, # ページ番号
‘per_page’: per_page # 1ページあたりの件数
}
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# データがなければ終了
if not data:
break
# データを追加
all_data.extend(data)
print(f”ページ {page} 取得完了(累計: {len(all_data)}件)”)
# 次のページへ
page += 1
# レート制限対策
time.sleep(1)
return all_data
# 使用例(GitHub APIの実例)
# repos = fetch_all_data_page(‘https://api.github.com/users/octocat/repos’, per_page=30)
3-5. カーソル方式の実装
# ===== カーソル方式のページネーション =====
import requests
import time
def fetch_all_data_cursor(base_url, per_page=100):
“””
カーソル方式で全データを取得する
Parameters:
base_url: APIのベースURL
per_page: 1ページあたりの件数
Returns:
全データのリスト
“””
all_data = []
next_cursor = None
while True:
# パラメータを指定
params = {‘count’: per_page}
# カーソルがあれば追加
if next_cursor:
params[‘cursor’] = next_cursor
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# データを追加
items = data.get(‘items’, [])
all_data.extend(items)
print(f”取得済み: {len(all_data)}件”)
# 次のカーソルを取得
next_cursor = data.get(‘next_cursor’)
# 次のカーソルがなければ終了
if not next_cursor:
break
# レート制限対策
time.sleep(1)
return all_data
⚠️ レート制限に注意!
多くのAPIにはレート制限(Rate Limit)があります。
例:「1時間に100リクエストまで」「1分間に60リクエストまで」
対策:
time.sleep(1)でリクエスト間に待機時間を入れる
- レスポンスヘッダーの
X-RateLimit-Remainingを確認
- 429エラー(Too Many Requests)が出たら待機して再試行
🔐 4. 認証(APIキー、Bearer Token)
4-1. なぜ認証が必要なのか?
多くのAPIは、誰がアクセスしているかを確認するため、認証を要求します。
- 不正利用の防止:誰でもアクセスできると悪用される
- 利用量の制限:ユーザーごとにリクエスト数を制限
- 課金:有料プランの利用者を識別
4-2. APIキー認証
APIキーは、サービスから発行される文字列です。
ヘッダーまたはURLパラメータに含めて送信します。
方法1:ヘッダーに含める(推奨)
# ===== APIキーをヘッダーに含める =====
import requests
# APIキー(実際は環境変数から取得)
api_key = ‘YOUR_API_KEY_HERE’
# ヘッダーにAPIキーを含める
headers = {
‘X-API-Key’: api_key
}
response = requests.get(
‘https://api.example.com/data’,
headers=headers,
timeout=10
)
data = response.json()
print(data)
方法2:URLパラメータに含める
# ===== APIキーをURLパラメータに含める =====
import requests
api_key = ‘YOUR_API_KEY_HERE’
# パラメータにAPIキーを含める
params = {
‘api_key’: api_key,
‘query’: ‘python’
}
response = requests.get(
‘https://api.example.com/search’,
params=params,
timeout=10
)
data = response.json()
print(data)
📝 ヘッダー vs パラメータ
- ヘッダーが推奨される理由:URLに機密情報が残らない、ログに記録されにくい
- どちらを使うかはAPIの仕様書を確認してください
4-3. Bearer Token認証
Bearer Tokenは、OAuth2.0などで使用される認証方式です。
AuthorizationヘッダーにBearer {トークン}の形式で含めます。
# ===== Bearer Token認証 =====
import requests
# アクセストークン(実際は環境変数から取得)
access_token = ‘YOUR_ACCESS_TOKEN_HERE’
# Authorizationヘッダーに含める
headers = {
‘Authorization’: f’Bearer {access_token}’
}
response = requests.get(
‘https://api.example.com/me’,
headers=headers,
timeout=10
)
data = response.json()
print(data)
4-4. Basic認証
Basic認証は、ユーザー名とパスワードを使った認証方式です。
# ===== Basic認証 =====
import requests
from requests.auth import HTTPBasicAuth
# 方法1:HTTPBasicAuthを使う
response = requests.get(
‘https://api.example.com/data’,
auth=HTTPBasicAuth(‘username’, ‘password’),
timeout=10
)
# 方法2:タプルで指定(簡潔)
response = requests.get(
‘https://api.example.com/data’,
auth=(‘username’, ‘password’),
timeout=10
)
data = response.json()
print(data)
4-5. APIキーの安全な管理
APIキーやトークンは秘密情報です。
コードに直接書かず、環境変数や設定ファイルで管理しましょう。
# ===== 環境変数から取得(推奨)=====
import os
# 環境変数から取得
api_key = os.environ.get(‘MY_API_KEY’)
if not api_key:
raise ValueError(“環境変数 MY_API_KEY が設定されていません”)
# 使用
headers = {‘X-API-Key’: api_key}
# ===== .envファイルを使う(python-dotenv)=====
# まず .env ファイルを作成
# MY_API_KEY=your_actual_api_key_here
# MY_SECRET=another_secret_value
from dotenv import load_dotenv
import os
# .envファイルを読み込む
load_dotenv()
# 環境変数として取得
api_key = os.getenv(‘MY_API_KEY’)
secret = os.getenv(‘MY_SECRET’)
⚠️ APIキー管理の重要なルール
- コードに直接書かない
- .envファイルは
.gitignoreに追加してGitにコミットしない
- 漏洩した場合はすぐに無効化して再発行
- チームで共有する場合はパスワードマネージャーを使う
🕷️ 5. Beautiful Soupの基礎
5-1. Webスクレイピングとは?
Webスクレイピングとは、Webページから必要な情報を自動で抽出する技術です。
HTMLを解析して、テキストやリンク、画像URLなどを取り出します。
✅ APIを使う場合
- サービスが公式に提供
- データ形式が明確(JSON)
- 安定している
- 合法的で推奨
⚠️ スクレイピングを使う場合
- APIがない場合の代替手段
- HTMLの構造が変わると動かない
- 利用規約の確認が必要
- サーバーへの負荷に注意
💡 基本方針
APIがあればAPIを使うのが基本です。
APIがない場合にのみ、スクレイピングを検討しましょう。
5-2. Beautiful Soupのインストール
# Beautiful Soupのインストール
pip install beautifulsoup4
# HTMLパーサー(lxml推奨)
pip install lxml
5-3. 基本的な使い方
# ===== Beautiful Soupの基本 =====
import requests
from bs4 import BeautifulSoup
# WebページのHTMLを取得
url = ‘https://example.com’
response = requests.get(url, timeout=10)
# BeautifulSoupでHTMLを解析
soup = BeautifulSoup(response.content, ‘lxml’)
# タイトルを取得
title = soup.title.string
print(f”タイトル: {title}”)
# すべてのリンクを取得
links = soup.find_all(‘a’)
for link in links:
href = link.get(‘href’) # href属性を取得
text = link.string # リンクテキストを取得
print(f”{text}: {href}”)
📝 コードの説明
BeautifulSoup(HTML, 'lxml'):HTMLを解析してオブジェクトを作成
soup.title:<title>タグを取得
.string:タグ内のテキストを取得
soup.find_all('a'):すべての<a>タグをリストで取得
link.get('href'):href属性の値を取得
5-4. タグを検索する方法
# ===== タグの検索方法 =====
from bs4 import BeautifulSoup
html = ”’
<html>
<body>
<div class=”product” id=”item1″>
<h2>ノートPC</h2>
<p class=”price”>89,800円</p>
<a href=”/detail/1″>詳細を見る</a>
</div>
<div class=”product” id=”item2″>
<h2>マウス</h2>
<p class=”price”>1,980円</p>
<a href=”/detail/2″>詳細を見る</a>
</div>
</body>
</html>
”’
soup = BeautifulSoup(html, ‘lxml’)
# 1. find() – 最初の1つだけ取得
first_product = soup.find(‘div’, class_=’product’)
print(f”最初の商品: {first_product.h2.string}”) # ノートPC
# 2. find_all() – すべて取得
all_products = soup.find_all(‘div’, class_=’product’)
for product in all_products:
name = product.h2.string
price = product.find(‘p’, class_=’price’).string
print(f”{name}: {price}”)
# 3. IDで検索
item2 = soup.find(‘div’, id=’item2′)
print(f”ID=item2: {item2.h2.string}”) # マウス
# 4. select() – CSSセレクタで検索
prices = soup.select(‘.product .price’)
for price in prices:
print(price.string)
5-5. CSSセレクタの使い方
| セレクタ |
意味 |
例 |
.class |
クラス名で検索 |
.product → class=”product” |
#id |
IDで検索 |
#header → id=”header” |
tag |
タグ名で検索 |
h2 → <h2>タグ |
親 子 |
子孫要素を検索 |
.product .price |
親 > 子 |
直下の子要素のみ |
.product > h2 |
[属性] |
属性で検索 |
a[href^="https"] → httpsで始まるリンク |
5-6. テキストの取得と整形
# ===== テキストの取得方法 =====
from bs4 import BeautifulSoup
html = ‘<div> 価格: <span>1,980円</span> </div>’
soup = BeautifulSoup(html, ‘lxml’)
div = soup.find(‘div’)
# .string – 直接の子テキストのみ(子要素があるとNone)
print(f”string: {div.string}”) # None
# .get_text() – すべてのテキストを取得
print(f”get_text: ‘{div.get_text()}'”) # ” 価格: 1,980円 ”
# .get_text(strip=True) – 前後の空白を削除
print(f”strip: ‘{div.get_text(strip=True)}'”) # “価格: 1,980円”
# 数値を抽出する例
price_text = soup.find(‘span’).get_text(strip=True) # “1,980円”
price = int(price_text.replace(‘円’, ”).replace(‘,’, ”)) # 1980
print(f”数値: {price}”)
5-7. 実践例:商品リストのスクレイピング
# ===== 実践的なスクレイピング =====
import requests
from bs4 import BeautifulSoup
import pandas as pd
import time
def scrape_products(url):
“””
商品リストページから情報を抽出する
Parameters:
url: スクレイピング対象のURL
Returns:
商品情報のDataFrame
“””
# ヘッダーを設定(User-Agent)
headers = {
‘User-Agent’: ‘MyBot/1.0 (your-email@example.com)’
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, ‘lxml’)
# 商品データを格納するリスト
products = []
# 各商品を取得
for item in soup.select(‘.product-item’):
# 商品名
name_elem = item.select_one(‘.product-name’)
name = name_elem.get_text(strip=True) if name_elem else ‘不明’
# 価格
price_elem = item.select_one(‘.price’)
if price_elem:
price_text = price_elem.get_text(strip=True)
price = int(price_text.replace(‘円’, ”).replace(‘,’, ”))
else:
price = 0
# リンク
link_elem = item.select_one(‘a’)
link = link_elem.get(‘href’) if link_elem else None
products.append({
‘name’: name,
‘price’: price,
‘link’: link
})
# DataFrameに変換
return pd.DataFrame(products)
# 使用例
# df = scrape_products(‘https://example-shop.com/products’)
# print(df)
⚖️ 6. 倫理的なスクレイピング
6-1. スクレイピングのルール
Webスクレイピングは便利な技術ですが、ルールを守って使う必要があります。
⚠️ スクレイピングの注意点
- robots.txtを確認する
- サイトの利用規約を確認する
- サーバーに負荷をかけない(短時間に大量リクエスト禁止)
- 個人情報を取得しない
- 取得したデータを無断で公開・販売しない
- 著作権に注意する
6-2. robots.txtの確認
robots.txtは、サイトがクローラー(ボット)に対して「ここはアクセスしないで」と指示するファイルです。
# robots.txtの確認方法
# ブラウザで以下のURLを開く
# 例:
User-agent: * # すべてのボットに対して
Disallow: /admin/ # /admin/以下はアクセス禁止
Disallow: /api/ # /api/以下はアクセス禁止
Allow: / # その他は許可
Crawl-delay: 10 # 10秒間隔を空けてアクセス
# Pythonでrobots.txtを確認
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url(‘https://example.com/robots.txt’)
rp.read()
# 特定のURLにアクセスしてよいか確認
url = ‘https://example.com/products’
can_fetch = rp.can_fetch(‘*’, url)
print(f”{url}: {‘許可’ if can_fetch else ‘禁止’}”)
6-3. サーバーに優しいスクレイピング
# ===== サーバーに優しいスクレイピング =====
import requests
from bs4 import BeautifulSoup
import time
def polite_scrape(urls, delay=2):
“””
礼儀正しいスクレイピング
Parameters:
urls: スクレイピングするURLのリスト
delay: リクエスト間の待機秒数
“””
# 自分を識別できるUser-Agentを設定
headers = {
‘User-Agent’: ‘MyResearchBot/1.0 (contact: your-email@example.com)’
}
results = []
for i, url in enumerate(urls):
try:
print(f”処理中 ({i+1}/{len(urls)}): {url}”)
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, ‘lxml’)
# データ処理…
title = soup.title.string if soup.title else ‘No Title’
results.append({‘url’: url, ‘title’: title})
except requests.exceptions.RequestException as e:
print(f”エラー: {e}”)
results.append({‘url’: url, ‘title’: ‘Error’})
# 待機(サーバーへの負荷を軽減)
if i < len(urls) - 1: # 最後のURLの後は待たない
time.sleep(delay)
return results
# 使用例
# urls = ['https://example.com/page1', 'https://example.com/page2']
# results = polite_scrape(urls, delay=2)
✅ ベストプラクティス
- APIがあればAPIを使う(公式の方法)
- スクレイピング前にrobots.txtと利用規約を確認
- 2秒以上の待機時間を設ける
- User-Agentに連絡先を含める
- エラーが出たらすぐに停止する
- 取得したデータは私的利用のみ
📝 STEP 5 のまとめ
✅ このステップで学んだこと
- requests:PythonでREST APIを呼び出すライブラリ
- HTTPステータスコード:200=成功、4xx=クライアントエラー、5xx=サーバーエラー
- JSON:
response.json()でPython辞書に変換
- ページネーション:オフセット、ページ番号、カーソルの3方式
- 認証:APIキー、Bearer Token、Basic認証
- Beautiful Soup:HTMLを解析してデータ抽出
- CSSセレクタ:
.class、#id、tagで検索
- 倫理的スクレイピング:robots.txt確認、待機時間、User-Agent設定
💡 実務でのポイント
1. APIがあればAPIを使う
スクレイピングは最後の手段。公式APIがあれば必ずそちらを使いましょう。
2. エラーハンドリングとtimeoutは必須
本番環境では、ネットワークエラーやサーバーエラーに備えましょう。
3. APIキーは環境変数で管理
コードに直接書かず、.envファイルや環境変数を使いましょう。
🎯 次のステップの予告
次のSTEP 6では、「大容量ファイル処理」を学びます。
- チャンク処理(chunksize)
- dtype指定によるメモリ削減
- 並列読み込み
- ストリーミング処理
メモリに収まらない巨大なファイルを効率的に処理する方法を習得しましょう!
📝 練習問題
問題 1
基礎
requestsを使って、GitHub APIからユーザー情報を取得し、名前とフォロワー数を表示してください。
URL: https://api.github.com/users/torvalds
【解答例】
import requests
response = requests.get(‘https://api.github.com/users/torvalds’, timeout=10)
data = response.json()
print(f”ユーザー名: {data[‘login’]}”)
print(f”名前: {data[‘name’]}”)
print(f”フォロワー数: {data[‘followers’]:,}”)
問題 2
基礎
JSONデータから安全にデータを取り出す方法を書いてください。キーが存在しない場合は「不明」と表示すること。
【解答例】
data = {‘name’: ‘山田太郎’, ‘email’: ‘yamada@example.com’}
# .get()を使って安全に取り出す
name = data.get(‘name’, ‘不明’)
phone = data.get(‘phone’, ‘不明’)
print(f”名前: {name}”)
print(f”電話: {phone}”)
問題 3
基礎
Beautiful Soupを使って、HTMLからすべてのh2タグのテキストを取得してください。
【解答例】
from bs4 import BeautifulSoup
html = ”’
<html>
<body>
<h2>見出し1</h2>
<p>本文1</p>
<h2>見出し2</h2>
<p>本文2</p>
</body>
</html>
”’
soup = BeautifulSoup(html, ‘lxml’)
h2_tags = soup.find_all(‘h2’)
for h2 in h2_tags:
print(h2.get_text(strip=True))
問題 4
基礎
HTTPステータスコード200、401、404、429はそれぞれ何を意味しますか?
【解答】
- 200:OK(成功)
- 401:Unauthorized(認証エラー)
- 404:Not Found(リソースが見つからない)
- 429:Too Many Requests(レート制限に達した)
問題 5
応用
エラーハンドリング付きのAPI呼び出し関数を作成してください。タイムアウトとHTTPエラーに対応すること。
【解答例】
import requests
def safe_api_call(url, timeout=10):
“””安全にAPIを呼び出す関数”””
try:
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print(“タイムアウトしました”)
return None
except requests.exceptions.HTTPError as e:
print(f”HTTPエラー: {e}”)
return None
except requests.exceptions.RequestException as e:
print(f”リクエストエラー: {e}”)
return None
# 使用例
data = safe_api_call(‘https://api.github.com/users/octocat’)
if data:
print(data[‘login’])
問題 6
応用
ページ番号方式のページネーションを実装して、全ページのデータを取得する関数を作成してください。
【解答例】
import requests
import time
def fetch_all_pages(base_url, per_page=30):
“””全ページのデータを取得する”””
all_data = []
page = 1
while True:
params = {‘page’: page, ‘per_page’: per_page}
response = requests.get(base_url, params=params, timeout=10)
data = response.json()
if not data:
break
all_data.extend(data)
print(f”ページ {page} 取得完了”)
page += 1
time.sleep(1) # レート制限対策
return all_data
# 使用例
# repos = fetch_all_pages(‘https://api.github.com/users/octocat/repos’)
問題 7
応用
Bearer Token認証を使ってAPIにアクセスするコードを書いてください。トークンは環境変数から取得すること。
【解答例】
import requests
import os
# 環境変数からトークンを取得
access_token = os.environ.get(‘API_TOKEN’)
if not access_token:
raise ValueError(“環境変数 API_TOKEN が設定されていません”)
# Bearer Token認証
headers = {
‘Authorization’: f’Bearer {access_token}’
}
response = requests.get(
‘https://api.example.com/me’,
headers=headers,
timeout=10
)
data = response.json()
print(data)
問題 8
応用
Beautiful Soupを使って、商品名と価格を抽出するコードを書いてください。価格は数値に変換すること。
【解答例】
from bs4 import BeautifulSoup
html = ”’
<div class=”product”>
<h3 class=”name”>ノートPC</h3>
<span class=”price”>89,800円</span>
</div>
<div class=”product”>
<h3 class=”name”>マウス</h3>
<span class=”price”>1,980円</span>
</div>
”’
soup = BeautifulSoup(html, ‘lxml’)
products = []
for item in soup.select(‘.product’):
name = item.select_one(‘.name’).get_text(strip=True)
price_text = item.select_one(‘.price’).get_text(strip=True)
price = int(price_text.replace(‘円’, ”).replace(‘,’, ”))
products.append({‘name’: name, ‘price’: price})
for p in products:
print(f”{p[‘name’]}: {p[‘price’]}円”)
問題 9
発展
APIから取得したネストしたJSONを、pandas.json_normalizeを使って平坦化してください。
【解答例】
import pandas as pd
from pandas import json_normalize
data = {
‘user_id’: 1,
‘name’: ‘山田太郎’,
‘orders’: [
{‘order_id’: 101, ‘product’: ‘ノートPC’, ‘price’: 89800},
{‘order_id’: 102, ‘product’: ‘マウス’, ‘price’: 1980}
]
}
# ordersを展開し、親の情報も含める
df = json_normalize(
data,
record_path=[‘orders’],
meta=[‘user_id’, ‘name’]
)
print(df)
問題 10
発展
礼儀正しいスクレイピングを行う関数を作成してください。User-Agent設定、待機時間、エラーハンドリングを含めること。
【解答例】
import requests
from bs4 import BeautifulSoup
import time
def polite_scrape(urls, delay=2):
“””礼儀正しいスクレイピング”””
headers = {
‘User-Agent’: ‘MyBot/1.0 (contact@example.com)’
}
results = []
for i, url in enumerate(urls):
try:
print(f”処理中 ({i+1}/{len(urls)}): {url}”)
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, ‘lxml’)
title = soup.title.string if soup.title else ‘No Title’
results.append({‘url’: url, ‘title’: title, ‘status’: ‘OK’})
except Exception as e:
print(f”エラー: {e}”)
results.append({‘url’: url, ‘title’: None, ‘status’: str(e)})
if i < len(urls) - 1:
time.sleep(delay)
return results
# 使用例
# results = polite_scrape(['https://example.com', 'https://example.org'])