はじめに
南海トラフ自身や富士山噴火といった、自然災害への注意喚起をニュースでよく耳にするようになりました。日本の優秀な研究者もそういった、何かしらの特徴変化の波を捉えているからなのでしょうか??
私自身も「近くの緊急避難場所も知らないなぁ~!?」と思い、おそらく自身大国ゆえに慣れっこになったほとんどの日本国民が、同じ状況だろうと想像に至ったので、全国の人が使えるようなアプリの開発を行ってみた次第です。最初は地元の足立区版を作ったのですが、参照元の.csvデータを差し替えるだけなので
全国版に拡張することにしました。
※下調べの過程でいくつかの「対災害対策アプリ」の存在は存じ上げておりますが、手っ取り早く最低限の操作で、___ ただ避難場所にたどり着く! ___ ことを目的にした 緊急避難場所検索API となってます。
この記事では、Python と Flask を使って、緊急避難場所検索API を開発する方法を初心者向けに詳しく解説します。
開発環境
- PC: Windows
- DB: 国土交通省(国土地理院)からcsvデータをDL
(運用面と管理のしやすさから、csvでそのままマッチングさせてます)
注意:
現在地情報を取得して、最寄りの避難場所を素早く見つけることを目的にしたので、位置情報の許可は必須です。
緊急避難場所アプリ(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)でのプロジェクト作成
-
Google Cloud Console にアクセス
- Google Cloud Console にアクセス
- Googleアカウントでログイン
-
新しいプロジェクトを作成
1. 画面上部の「プロジェクトを選択」をクリック 2. 「新しいプロジェクト」をクリック 3. プロジェクト名を入力(例:「shelter-search-app」) 4. 「作成」をクリック -
作成したプロジェクトを選択
- プロジェクト選択画面で、作成したプロジェクトをクリック
3-2. Google Maps API の有効化
-
APIライブラリに移動
1. 左側のナビゲーションメニューから「APIとサービス」→「ライブラリ」をクリック 2. 検索ボックスに「Maps JavaScript API」と入力 3. 「Maps JavaScript API」をクリック 4. 「有効にする」をクリック -
Geocoding API も有効化
1. 再度「ライブラリ」に戻る 2. 検索ボックスに「Geocoding API」と入力 3. 「Geocoding API」をクリック 4. 「有効にする」をクリック
3-3. API キーの作成
-
認証情報の作成
1. 左側のナビゲーションから「APIとサービス」→「認証情報」をクリック 2. 「認証情報を作成」→「APIキー」をクリック 3. APIキーが生成されます(例:AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) -
APIキーの制限設定(セキュリティ強化)
1. 作成されたAPIキーの「編集」ボタンをクリック 2. 「アプリケーションの制限」で以下を選択: - 開発環境:「なし」 - 本番環境:「HTTPリファラー(ウェブサイト)」 3. 「APIの制限」で「キーを制限」を選択 4. 以下のAPIにチェック: - Maps JavaScript API - Geocoding API 5. 「保存」をクリック
3-4. 課金の設定(重要)
Google Maps API は従量課金制です:
-
請求先アカウントの設定
1. 左側のナビゲーションから「お支払い」をクリック 2. 請求先アカウントを作成(クレジットカード情報が必要) -
無料利用枠について
- Maps JavaScript API: 月間 28,000 回まで無料 - Geocoding API: 月間 40,000 回まで無料 - 個人開発・テスト用途なら十分な範囲 -
予算アラートの設定(推奨)
1. 「お支払い」→「予算とアラート」 2. 「予算を作成」をクリック 3. 月額上限(例:1,000円)を設定 4. アラート通知を有効化
3-5. セキュリティのベストプラクティス
-
APIキーの適切な管理
# 環境変数として保存(GitHubなどにコミットしない) # .env ファイルに記載 GOOGLE_MAPS_API_KEY=AIzaSyBxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # .gitignore に .env を追加 echo ".env" >> .gitignore -
本番環境でのリファラー制限
APIキー設定でHTTPリファラーを制限: - https://yourdomain.com/* - https://www.yourdomain.com/* -
使用量の監視
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
テスト内容:
- ヘルスチェック (
/health) - 避難場所一覧取得 (
/shelters) - 最寄り検索 (
/nearest) - テキスト検索フィルタ (
/shelters?q=足立) - 地理的範囲フィルタ (
/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、リバースプロキシ)
災害時の避難場所検索は実際に役立つアプリケーションです。このコードを参考に、あなたの地域の避難場所データで実装してみてください!
関連リンク
最後までお読みいただき、ありがとうございました!
