0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonでパブリックIPを取得する — スクリプト、Django、FastAPI対応ガイド

0
Posted at

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検出に別のアプローチを使っていますか?コメントで共有してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?