PythonでパブリックIPを取得する — スクリプト、Django、FastAPI対応ガイド
スクリプトを書いている場合でも、DjangoのWebアプリを構築している場合でも、FastAPIのマイクロサービスを設計している場合でも、サーバーやクライアントのパブリックIPアドレスを知る必要が繰り返し発生します。この記事では、IPPubblico.org を使って3つのシナリオすべてを実用的なコードで説明します。APIキー不要、HTTPS対応、CORS有効の無料APIです。
なぜIPPubblico?
コードに入る前に、使う価値がある理由を簡単にまとめます:
- APIキー不要 — 即座に動作、ゼロセットアップ
- HTTPSのみ — デフォルトで安全
- プレーンテキストエンドポイント — シンプルなケースではパース不要
- 完全なJSONエンドポイント — 必要に応じて都市、国、ISP、ASN、タイムゾーン取得可能
- ハードなレート制限なし — 乱用防止のソフトリミットあり
- 無料枠での商用利用可能
ユースケース1 — requestsを使ったシンプルなスクリプト
最も一般的なシナリオ:自分のパブリックIPを知る必要があるスタンドアロンスクリプト。
import requests
def get_public_ip():
try:
response = requests.get(
'https://ipv4.ippubblico.org/',
timeout=5
)
response.raise_for_status()
return response.text.strip()
except requests.RequestException as e:
print(f"IPの取得に失敗: {e}")
return None
if __name__ == '__main__':
ip = get_public_ip()
print(f"パブリックIP: {ip}")
# パブリックIP: 203.0.113.42
ユースケース2 — requestsを使った完全なジオロケーション
IPに加えて国、都市、ISP、タイムゾーンが必要な場合:
import requests
from dataclasses import dataclass
from typing import Optional
@dataclass
class IPInfo:
ip: str
country: Optional[str]
country_code: Optional[str]
city: Optional[str]
region: Optional[str]
isp: Optional[str]
timezone: Optional[str]
lat: Optional[float]
lon: Optional[float]
def get_ip_info(ip: str = None) -> Optional[IPInfo]:
"""
IPアドレスのジオロケーション情報を取得する。
ipがNoneの場合、現在のパブリックIPの情報を返す。
"""
url = 'https://ippubblico.org/?api=1'
if ip:
url += f'&ip={ip}'
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
if data.get('status') != 'ok':
return None
geo = data.get('geo', {})
return IPInfo(
ip=data.get('ip', ''),
country=geo.get('country'),
country_code=geo.get('country_code'),
city=geo.get('city'),
region=geo.get('region'),
isp=data.get('isp'),
timezone=data.get('timezone'),
lat=geo.get('lat'),
lon=geo.get('lon'),
)
except requests.RequestException as e:
print(f"リクエスト失敗: {e}")
return None
if __name__ == '__main__':
info = get_ip_info()
if info:
print(f"IP: {info.ip}")
print(f"国: {info.country} ({info.country_code})")
print(f"都市: {info.city}, {info.region}")
print(f"ISP: {info.isp}")
print(f"タイムゾーン: {info.timezone}")
ユースケース3 — httpxを使った非同期スクリプト
httpxを使った非同期Pythonスクリプトの場合:
import asyncio
import httpx
async def get_public_ip() -> str | None:
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ipv4.ippubblico.org/')
response.raise_for_status()
return response.text.strip()
except httpx.HTTPError as e:
print(f"IP取得失敗: {e}")
return None
async def get_ip_info() -> dict | None:
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
response.raise_for_status()
data = response.json()
return data if data.get('status') == 'ok' else None
except httpx.HTTPError as e:
print(f"IP情報取得失敗: {e}")
return None
async def main():
ip = await get_public_ip()
print(f"パブリックIP: {ip}")
info = await get_ip_info()
if info:
print(f"国: {info['geo']['country']}")
print(f"都市: {info['geo']['city']}")
asyncio.run(main())
ユースケース4 — 国検出のためのDjangoミドルウェア
すべてのリクエストでユーザーの国を検出し、request.country_codeに追加するDjangoミドルウェア。地域ベースのコンテンツ、通貨選択、ロギングに便利です。
# myapp/middleware.py
import requests
from django.core.cache import cache
CACHE_TIMEOUT = 3600 # IPごとに1時間
class CountryDetectionMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
client_ip = self._get_client_ip(request)
request.client_ip = client_ip
request.country_code = self._get_country(client_ip)
response = self.get_response(request)
return response
def _get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
def _get_country(self, ip: str) -> str | None:
if not ip:
return None
cache_key = f'country_{ip}'
cached = cache.get(cache_key)
if cached:
return cached
try:
response = requests.get(
'https://ippubblico.org/?api=1',
timeout=3
)
data = response.json()
country_code = data.get('geo', {}).get('country_code')
if country_code:
cache.set(cache_key, country_code, CACHE_TIMEOUT)
return country_code
except Exception:
return None
settings.pyに登録:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'myapp.middleware.CountryDetectionMiddleware', # ここに追加
# ... 残りのミドルウェア
]
任意のビューで使用:
# views.py
from django.http import JsonResponse
def my_view(request):
return JsonResponse({
'ip': request.client_ip,
'country': request.country_code,
})
ユースケース5 — リクエストごとのジオロケーションを持つDjangoビュー
グローバルではなく特定のビューで完全なジオロケーションデータが必要な場合:
# views.py
import requests
from django.http import JsonResponse
from django.views import View
class UserLocationView(View):
def get(self, request):
ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() \
or request.META.get('REMOTE_ADDR')
try:
response = requests.get(
'https://ippubblico.org/?api=1',
timeout=5
)
data = response.json()
geo = data.get('geo', {})
return JsonResponse({
'ip': data.get('ip'),
'country': geo.get('country'),
'country_code': geo.get('country_code'),
'city': geo.get('city'),
'timezone': data.get('timezone'),
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
ユースケース6 — FastAPI依存性注入
最もクリーンなFastAPIパターン:任意のルートにIP情報を注入する再利用可能な依存関係。
# dependencies.py
import httpx
from fastapi import Request
from typing import Optional
from pydantic import BaseModel
class GeoInfo(BaseModel):
city: Optional[str] = None
region: Optional[str] = None
country: Optional[str] = None
country_code: Optional[str] = None
lat: Optional[float] = None
lon: Optional[float] = None
class IPInfo(BaseModel):
ip: str
isp: Optional[str] = None
timezone: Optional[str] = None
geo: GeoInfo = GeoInfo()
def get_client_ip(request: Request) -> str:
forwarded = request.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0].strip()
return request.client.host
async def get_ip_info(request: Request) -> Optional[IPInfo]:
ip = get_client_ip(request)
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
data = response.json()
if data.get('status') == 'ok':
return IPInfo(**{
'ip': data.get('ip', ip),
'isp': data.get('isp'),
'timezone': data.get('timezone'),
'geo': GeoInfo(**data.get('geo', {}))
})
except Exception:
pass
return None
任意のルートで使用:
# main.py
from fastapi import FastAPI, Depends
from dependencies import IPInfo, get_ip_info
app = FastAPI()
@app.get('/location')
async def location(ip_info: IPInfo = Depends(get_ip_info)):
if not ip_info:
return {'error': '位置情報を検出できませんでした'}
return {
'ip': ip_info.ip,
'country': ip_info.geo.country,
'city': ip_info.geo.city,
'timezone': ip_info.timezone,
}
ユースケース7 — Redisキャッシュ付きFastAPI
高トラフィックAPIの場合、IPごとにジオロケーション結果をキャッシュして外部APIへの重複呼び出しを避ける:
# dependencies_cached.py
import httpx
import json
from fastapi import Request
from typing import Optional
import redis.asyncio as redis
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
CACHE_TTL = 3600 # 1時間
async def get_ip_info_cached(request: Request) -> Optional[dict]:
forwarded = request.headers.get('X-Forwarded-For')
ip = forwarded.split(',')[0].strip() if forwarded else request.client.host
cache_key = f'ipinfo:{ip}'
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get('https://ippubblico.org/?api=1')
data = response.json()
if data.get('status') == 'ok':
await redis_client.setex(cache_key, CACHE_TTL, json.dumps(data))
return data
except Exception:
pass
return None
レート制限の処理
同じIPから短時間に多くのリクエストを行うと、429 Too Many Requestsレスポンスが返ってきます。APIは何秒待つべきかを示すRetry-Afterヘッダーを返します:
import time
import requests
def get_ip_with_retry(max_retries: int = 3) -> str | None:
for attempt in range(max_retries):
response = requests.get(
'https://ipv4.ippubblico.org/',
timeout=5
)
if response.status_code == 200:
return response.text.strip()
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 20))
print(f"レート制限中。{retry_after}秒待機後にリトライ {attempt + 1}/{max_retries}")
time.sleep(retry_after)
continue
response.raise_for_status()
return None
実際には、アプリケーションレベルで結果をキャッシュすることで、ほとんどのレート制限の問題を解消できます。
クイックリファレンス
| 用途 | エンドポイント | レスポンス |
|---|---|---|
| IPv4のみ | https://ipv4.ippubblico.org/ |
203.0.113.42 |
| IPv6のみ | https://ipv6.ippubblico.org/ |
2001:db8::1 または NONE
|
| 両プロトコル | https://ippubblico.org/?text=1 |
IPv4: x\nIPv6: x |
| 完全なジオロケーション | https://ippubblico.org/?api=1 |
国、都市、ISP付きJSON |
完全なAPIドキュメント(43言語対応):ippubblico.org/docs.html
まとめ
IPPubblicはPythonのあらゆるユースケースをカバーします — シェルスクリプトの1行から、Redisキャッシュ付きの適切に型付けされたFastAPI依存関係まで。プレーンテキストとJSONエンドポイントにわたる一貫したAPIにより、シンプルに始めて必要な時だけ複雑さを追加できます。
ここで示したDjangoミドルウェアとFastAPI依存関係パターンは、本番環境対応の出発点です:プロキシ背後でのクライアントIP抽出、冗長なAPI呼び出しを避けるための結果キャッシュ、外部サービスが利用できない場合のグレースフルな失敗処理を行います。
PythonでのIP検出に別のアプローチを使っていますか?コメントで共有してください。