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 + Flask】緊急避難場所検索APIを作ってみた - 【CSV運用】初心者向け完全解説

Last updated at Posted at 2025-09-02

はじめに

南海トラフ自身や富士山噴火といった、自然災害への注意喚起をニュースでよく耳にするようになりました。日本の優秀な研究者もそういった、何かしらの特徴変化の波を捉えているからなのでしょうか??
私自身も「近くの緊急避難場所も知らないなぁ~!?」と思い、おそらく自身大国ゆえに慣れっこになったほとんどの日本国民が、同じ状況だろうと想像に至ったので、全国の人が使えるようなアプリの開発を行ってみた次第です。最初は地元の足立区版を作ったのですが、参照元の.csvデータを差し替えるだけなので:stuck_out_tongue_winking_eye:全国版に拡張することにしました。

※下調べの過程でいくつかの「対災害対策アプリ」の存在は存じ上げておりますが、手っ取り早く最低限の操作で、___ ただ避難場所にたどり着く! ___ ことを目的にした 緊急避難場所検索API となってます。

この記事では、Python と Flask を使って、緊急避難場所検索API を開発する方法を初心者向けに詳しく解説します。

開発環境

  • PC: Windows
  • DB: 国土交通省(国土地理院)からcsvデータをDL
    (運用面と管理のしやすさから、csvでそのままマッチングさせてます)

注意:

現在地情報を取得して、最寄りの避難場所を素早く見つけることを目的にしたので、位置情報の許可は必須です。

image.png

緊急避難場所アプリ(Python版) 環境により5~6秒かかります。
現在地情報から最も近い緊急避難場所(5件リスト)を表示します。何れかをクリックするとGoogleMapへ画面遷移し、現在地からクリックした避難場所への経路が表示されます。


この記事で学べること

  • Flask を使った Web API の基本的な作り方
  • CSV データの読み込みと処理方法
  • 距離計算(Haversine公式)の実装
  • Google Maps API との連携方法
  • フロントエンドとバックエンドの連携
  • CORS 設定とセキュリティの基礎

完成イメージ

  • 現在地から最寄りの避難場所を検索
  • 郵便番号から避難場所を検索
  • 地図上での結果表示
  • 各避難場所へのルート案内

プロジェクト構成

emgcy_API_py/
├── app.py              # メインアプリケーション
├── self_test.py        # 自動テストスクリプト
├── requirements.txt    # 依存パッケージ
├── README.md          # プロジェクト説明
├── static/
│   └── app.js         # フロントエンドJavaScript
└── templates/
    └── index.html     # HTMLテンプレート

セットアップ手順

1. 仮想環境の作成

# 仮想環境を作成
python -m venv .venv

# 仮想環境を有効化(Windows PowerShell)
.venv\Scripts\Activate.ps1

# 仮想環境を有効化(Windows Command Prompt)
.venv\Scripts\activate.bat

# 仮想環境を有効化(macOS/Linux)
source .venv/bin/activate

2. 依存パッケージのインストール

pip install -r requirements.txt

requirements.txt の内容:

Flask==3.0.3
requests==2.32.3

3. Google Maps API キーの取得方法

郵便番号検索機能を使用する場合は、Google Maps API キーが必要です。以下の手順で取得してください:

3-1. Google Cloud Platform(GCP)でのプロジェクト作成

  1. Google Cloud Console にアクセス

  2. 新しいプロジェクトを作成

    1. 画面上部の「プロジェクトを選択」をクリック
    2. 「新しいプロジェクト」をクリック  
    3. プロジェクト名を入力(例:「shelter-search-app」)
    4. 「作成」をクリック
    
  3. 作成したプロジェクトを選択

    • プロジェクト選択画面で、作成したプロジェクトをクリック

3-2. Google Maps API の有効化

  1. APIライブラリに移動

    1. 左側のナビゲーションメニューから「APIとサービス」→「ライブラリ」をクリック
    2. 検索ボックスに「Maps JavaScript API」と入力
    3. 「Maps JavaScript API」をクリック
    4. 「有効にする」をクリック
    
  2. Geocoding API も有効化

    1. 再度「ライブラリ」に戻る
    2. 検索ボックスに「Geocoding API」と入力  
    3. 「Geocoding API」をクリック
    4. 「有効にする」をクリック
    

3-3. API キーの作成

  1. 認証情報の作成

    1. 左側のナビゲーションから「APIとサービス」→「認証情報」をクリック
    2. 「認証情報を作成」→「APIキー」をクリック
    3. APIキーが生成されます(例:AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)
    
  2. APIキーの制限設定(セキュリティ強化)

    1. 作成されたAPIキーの「編集」ボタンをクリック
    2. 「アプリケーションの制限」で以下を選択:
       - 開発環境:「なし」
       - 本番環境:「HTTPリファラー(ウェブサイト)」
    3. 「APIの制限」で「キーを制限」を選択
    4. 以下のAPIにチェック:
       - Maps JavaScript API
       - Geocoding API
    5. 「保存」をクリック
    

3-4. 課金の設定(重要)

Google Maps API は従量課金制です:

  1. 請求先アカウントの設定

    1. 左側のナビゲーションから「お支払い」をクリック
    2. 請求先アカウントを作成(クレジットカード情報が必要)
    
  2. 無料利用枠について

    - Maps JavaScript API: 月間 28,000 回まで無料
    - Geocoding API: 月間 40,000 回まで無料
    - 個人開発・テスト用途なら十分な範囲
    
  3. 予算アラートの設定(推奨)

    1. 「お支払い」→「予算とアラート」
    2. 「予算を作成」をクリック
    3. 月額上限(例:1,000円)を設定
    4. アラート通知を有効化
    

3-5. セキュリティのベストプラクティス

  1. APIキーの適切な管理

    # 環境変数として保存(GitHubなどにコミットしない)
    # .env ファイルに記載
    GOOGLE_MAPS_API_KEY=AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    
    # .gitignore に .env を追加
    echo ".env" >> .gitignore
    
  2. 本番環境でのリファラー制限

    APIキー設定でHTTPリファラーを制限:
    - https://yourdomain.com/*
    - https://www.yourdomain.com/*
    
  3. 使用量の監視

    Google Cloud Console の「APIとサービス」→「割り当て」で
    定期的に使用量をチェック
    

4. Google Maps API キーの設定

取得したAPIキーを環境変数として設定します:

# Windows PowerShell
$env:GOOGLE_MAPS_API_KEY="あなたのAPIキー"

# Windows Command Prompt
set GOOGLE_MAPS_API_KEY=あなたのAPIキー

# macOS/Linux
export GOOGLE_MAPS_API_KEY="あなたのAPIキー"

4. アプリケーションの起動

python app.py

ブラウザで http://127.0.0.1:8000/ にアクセスしてください。

コードの詳細解説

1. データモデル(app.py)

まず、避難場所の情報を格納するデータクラスを定義します:

from dataclasses import dataclass

@dataclass
class Shelter:
    """避難場所データクラス"""
    id: str
    name: str
    address: str
    latitude: float
    longitude: float
    notes: str = ""

    def to_dict(self) -> dict:
        """API レスポンス用の辞書形式に変換"""
        return {
            "id": self.id,
            "name": self.name,
            "address": self.address,
            "lat": self.latitude,
            "lon": self.longitude,
            "notes": self.notes,
        }

ポイント:

  • @dataclass デコレータで簡潔にクラス定義
  • to_dict() メソッドで JSON API 用の形式に変換

2. 距離計算(Haversine公式)

2点間の緯度経度から正確な距離を計算する関数:

import math

def haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """
    Haversine公式による2点間の距離計算(km単位)
    
    地球を半径6371kmの球体として近似し、
    2点間の大圏距離(最短距離)を計算します。
    """
    radius_km = 6371.0  # 地球の半径(km)
    
    # 度からラジアンに変換
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    
    # Haversine公式の計算
    a = (
        math.sin(dlat / 2) ** 2
        + math.cos(math.radians(lat1))
        * math.cos(math.radians(lat2))
        * math.sin(dlon / 2) ** 2
    )
    c = 2 * math.asin(min(1.0, math.sqrt(a)))
    return radius_km * c

数学の解説:

  • Haversine公式は球面上の2点間の最短距離を求める公式
  • GPS座標から実際の移動距離を高精度で計算可能
  • 数十km程度の距離では非常に正確

3. CSVデータの読み込み

複数の CSV フォーマットに対応した柔軟なデータ読み込み:

import csv
from functools import lru_cache

@lru_cache(maxsize=1)
def load_shelters() -> List[Shelter]:
    """
    CSVファイルから避難場所データを読み込み
    
    LRUキャッシュにより、初回読み込み後は
    メモリ上のデータを再利用して高速化
    """
    csv_path = _detect_csv_path()  # CSVファイルを自動検出
    
    with open(csv_path, "r", encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        parser = _choose_parser(reader.fieldnames or [])  # 形式を自動判定
        shelters = []
        
        for row in reader:
            shelter = parser(row)
            if shelter is not None:  # 有効なデータのみ追加
                shelters.append(shelter)
                
    return shelters

ポイント:

  • @lru_cache でデータを一度だけ読み込み、メモリにキャッシュ
  • encoding="utf-8-sig" で BOM 付き UTF-8 に対応
  • 複数のCSVフォーマットを自動判定して適切にパース

4. Flask API エンドポイント

ヘルスチェック

@app.get("/health")
def health():
    """アプリケーションの動作確認用"""
    return jsonify({"status": "ok"})

避難場所一覧取得(フィルタ・ページネーション対応)

@app.get("/shelters")
def list_shelters():
    """
    避難場所の一覧を取得
    
    Query Parameters:
    - q: 施設名・住所での部分一致検索
    - bbox: 地理的範囲フィルタ "minLon,minLat,maxLon,maxLat"
    - limit: 取得件数上限 (1-500, デフォルト: 100)
    - offset: 取得開始位置 (デフォルト: 0)
    """
    all_items = load_shelters()
    
    # テキスト検索フィルタ
    q = (request.args.get("q") or "").strip()
    if q:
        q_lower = q.lower()
        all_items = [
            s for s in all_items
            if (s.name and q_lower in s.name.lower())
            or (s.address and q_lower in s.address.lower())
        ]
    
    # 地理的範囲フィルタ
    bbox = (request.args.get("bbox") or "").strip()
    if bbox:
        parts = bbox.split(",")
        if len(parts) == 4:
            min_lon, min_lat, max_lon, max_lat = map(float, parts)
            all_items = [
                s for s in all_items
                if (min_lat <= s.latitude <= max_lat) 
                and (min_lon <= s.longitude <= max_lon)
            ]
    
    # ページネーション
    limit = max(1, min(500, int(request.args.get("limit", "100"))))
    offset = max(0, int(request.args.get("offset", "0")))
    
    page_items = all_items[offset: offset + limit]
    
    return jsonify({
        "total": len(all_items),
        "count": len(page_items),
        "offset": offset,
        "limit": limit,
        "items": [s.to_dict() for s in page_items],
    })

最寄り避難場所検索

@app.get("/nearest")
def nearest():
    """
    緯度経度指定での最寄り避難場所検索
    
    Query Parameters:
    - lat: 基準点の緯度
    - lon: 基準点の経度  
    - limit: 取得件数上限 (デフォルト: 5)
    """
    try:
        lat = float(request.args.get("lat", ""))
        lon = float(request.args.get("lon", ""))
    except ValueError:
        return jsonify({"error": "lat and lon must be numbers"}), 400

    # 緯度経度の範囲チェック
    if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0):
        return jsonify({"error": "lat must be in [-90,90], lon in [-180,180]"}), 400

    limit = max(1, min(50, int(request.args.get("limit", "5"))))
    
    # 距離計算・ソート
    sorted_pairs = sort_by_distance(lat, lon, load_shelters())
    items = [
        {**shelter.to_dict(), "distance_km": round(distance_km, 3)}
        for shelter, distance_km in sorted_pairs[:limit]
    ]
    
    return jsonify({
        "origin": {"lat": lat, "lon": lon},
        "limit": limit,
        "items": items,
    })

郵便番号検索

@app.get("/nearest/by-zip")
def nearest_by_zip():
    """
    郵便番号指定での最寄り避難場所検索
    Google Geocoding API を使用
    """
    zip_code = (request.args.get("zip") or "").strip()
    if not zip_code or len(zip_code) != 7 or not zip_code.isdigit():
        return jsonify({"error": "zip must be 7 digits (no hyphen)"}), 400

    api_key = os.environ.get("GOOGLE_MAPS_API_KEY")
    if not api_key:
        return jsonify({"error": "Server missing GOOGLE_MAPS_API_KEY"}), 500

    # Google Geocoding API 呼び出し
    import requests
    resp = requests.get(
        "https://maps.googleapis.com/maps/api/geocode/json",
        params={"address": zip_code, "key": api_key},
        timeout=10,
    )
    
    data = resp.json()
    if data.get("status") != "OK":
        return jsonify({"error": "Could not geocode zip"}), 404

    # 緯度経度を取得して最寄り検索実行
    location = data["results"][0]["geometry"]["location"]
    lat, lon = float(location["lat"]), float(location["lng"])
    
    sorted_pairs = sort_by_distance(lat, lon, load_shelters())
    items = [
        {**shelter.to_dict(), "distance_km": round(distance_km, 3)}
        for shelter, distance_km in sorted_pairs[:5]
    ]
    
    return jsonify({
        "origin": {"lat": lat, "lon": lon, "zip": zip_code},
        "items": items
    })

5. フロントエンド(JavaScript)

Google Maps を使った地図表示とAPI連携:

// Google Maps 初期化
async function initMap() {
    if (!window.google?.maps) {
        console.warn("Google Maps API key is not set");
        return;
    }

    const { Map } = await google.maps.importLibrary("maps");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");

    // 地図を初期化(西新井大師さま ※震災などの厄除けと言ったら都内ではココ!!なので。)
    map = new Map(document.getElementById("map"), {
        center: { lat: 35.7814, lng: 139.7700 },
        zoom: 13,
        mapId: "DEMO_MAP_ID",
    });

    setupEventListeners();
}

// 現在地検索
document.getElementById("current-location-btn").addEventListener("click", () => {
    navigator.geolocation.getCurrentPosition(
        (pos) => {
            const loc = { lat: pos.coords.latitude, lon: pos.coords.longitude };
            findAndDisplayShelters(loc);
        },
        () => alert("現在地を取得できませんでした")
    );
});

// API呼び出し・結果表示
async function findAndDisplayShelters(userLoc) {
    const url = `${window.API_BASE}/nearest?lat=${userLoc.lat}&lon=${userLoc.lon}&n=5`;
    const res = await fetch(url);
    const data = await res.json();
    
    // 地図とリストに結果を表示
    updateUIFromNearest(data);
}

テスト方法

自動テストスクリプトが用意されています:

python self_test.py

テスト内容:

  1. ヘルスチェック (/health)
  2. 避難場所一覧取得 (/shelters)
  3. 最寄り検索 (/nearest)
  4. テキスト検索フィルタ (/shelters?q=足立)
  5. 地理的範囲フィルタ (/shelters?bbox=...)

API仕様

エンドポイント一覧

エンドポイント メソッド 説明
/health GET ヘルスチェック
/shelters GET 避難場所一覧(フィルタ・ページネーション対応)
/nearest GET 最寄り避難場所検索(緯度経度指定)
/nearest/by-zip GET 最寄り避難場所検索(郵便番号指定)

レスポンス例

最寄り検索 (/nearest?lat=35.77&lon=139.80&limit=3)

{
  "origin": {
    "lat": 35.77,
    "lon": 139.80
  },
  "limit": 3,
  "items": [
    {
      "id": "13121_001",
      "name": "足立区立○○小学校",
      "address": "東京都足立区○○1-2-3",
      "lat": 35.7745,
      "lon": 139.8034,
      "notes": "体育館利用可",
      "distance_km": 0.125
    }
  ]
}

セキュリティとベストプラクティス

1. CORS設定

@app.after_request
def add_cors_headers(response):
    """外部サイトからのAPI利用を許可"""
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return response

2. 入力値検証

# 緯度経度の範囲チェック
if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0):
    return jsonify({"error": "Invalid coordinates"}), 400

# 郵便番号の形式チェック
if not zip_code.isdigit() or len(zip_code) != 7:
    return jsonify({"error": "Invalid zip code format"}), 400

3. レート制限

# 取得件数の上限設定
limit = max(1, min(500, int(limit_param)))

本番環境への対応

1. WSGI サーバー(Gunicorn)での運用

pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app

2. 環境変数設定

export FLASK_ENV=production
export GOOGLE_MAPS_API_KEY="your_api_key_here"

3. リバースプロキシ(Nginx)設定例

location /emgcy_API_py/ {
    proxy_pass http://127.0.0.1:8000/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

📈 拡張アイデア

1. データベース対応

現在はCSVファイルを使用していますが、PostgreSQL + PostGIS での地理空間データベース対応:

# 例:PostGISでの距離検索クエリ
SELECT *, 
       ST_Distance(ST_Point(longitude, latitude)::geography, 
                   ST_Point(%s, %s)::geography) as distance
FROM shelters 
ORDER BY distance 
LIMIT %s;

2. キャッシュ機能

Redis を使った検索結果のキャッシュ:

import redis
r = redis.Redis()

def cached_nearest_search(lat, lon, limit):
    cache_key = f"nearest:{lat}:{lon}:{limit}"
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    
    result = sort_by_distance(lat, lon, load_shelters())[:limit]
    r.setex(cache_key, 300, json.dumps(result))  # 5分間キャッシュ
    return result

3. 認証・認可

JWT トークンベースの認証:

from flask_jwt_extended import JWTManager, jwt_required

@app.route('/api/shelters')
@jwt_required()
def protected_shelters():
    return jsonify(get_shelters())

まとめ

この記事では、Python + Flask を使って緊急避難場所検索APIを作成する方法を詳しく解説しました。

学んだ技術

  • Flask での REST API 開発
  • CSV データの効率的な処理
  • Haversine公式による距離計算
  • Google Maps API との連携
  • CORS 設定とセキュリティ
  • フロントエンド・バックエンド連携

実用的なポイント

  • 複数CSVフォーマットへの対応
  • エラーハンドリングの充実
  • パフォーマンス最適化(LRUキャッシュ)
  • 本番環境対応(WSGI、リバースプロキシ)

災害時の避難場所検索は実際に役立つアプリケーションです。このコードを参考に、あなたの地域の避難場所データで実装してみてください!

関連リンク

最後までお読みいただき、ありがとうございました!

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?