Python
PDF
位置情報
pandas
LINEmessagingAPI

あなたを死なせないためのプログラミング

今年は、大規模な地震・台風と、災害が多い年になりました。でもそれらをニュースで見て、「今のうちに災害対策アプリを入れておこう」と行動に移した人は多くはないでしょう。

災害対策情報だけでなく、今やどんな情報も、その気になればすぐに手に入る世の中になったにもかかわらず、「その気になる」までの心理的ハードルは未だに高いように感じています。人間は、本気で危機感を覚えないと行動できない生き物なのでしょう。

確かに、必要に迫られていないのに新しいアプリを入れるのには抵抗がある人も多い。それなら、日本中で誰もが使っている「LINE」で避難場所を知ることができたら、みんなもう少し気軽に使えるんじゃないか?

ということで今回は、「近くの避難所 教えるくん」を作りました。

作ったもの

hinan.png
68747470733a2f2f7363646e2e6c696e652d617070732e636f6d2f6e2f6c696e655f6164645f667269656e64732f62746e2f6a612e706e67.png
268fgf5Vm_.png
①LINEで友達に追加し、②位置情報を送信してみてください。一番近くの避難場所を教えてくれます(送られた位置情報は一切保管しません)。こんな感じで表示されます。
a.jpg
これで避難場所を確認したことで、または災害への意識を持ったことで、災害時に誰かの生き残る確率を0.1%でも上げることができたなら嬉しいです。

以降は作り方をつらつらと書きます。

この投稿のポイント

  • PDFの表データを読み込む
  • 住所から位置情報(緯度と経度)を取得する
  • 位置情報(緯度と経度)から距離を取得する
  • LINEで位置情報メッセージをやりとりする

ちなみに全てPythonです。

全国の避難場所の取得(PDF読み込み)

まずは、全国各地の避難場所を探すところから。各都道府県のサイトにデータはあるようですが、フォーマットはバラバラでデータ内容もイマイチ。最初に辿り着いた、使えそうなデータはこれでした。

内閣官房 国民保護ポータルサイト 避難施設

綺麗ではあるんですが、PDFです。。。PDFの表をそう簡単にコピペできないのは、多少PCを扱う人ならご存知でしょう。最近では、 スクレイピング対策@Azunyan1111さん)としても挙がっていました。

hinan_tokyo_pdf.jpg

でも、せっかくデータが揃っているので、少し抗ってみることにしました。探してみると、Pythonのライブラリでtabula-pyというものが使えそう。こんな簡単なコードでPDFの表をpandasのDataframeにしてくれるとのこと。本当かね?

from tabula import read_pdf
df = read_pdf('hinan_tokyo.pdf',
         java_options="-Dfile.encoding=UTF8")

何も考えず動かしてみると、早速エラー。どうやらJavaプログラムをラップして作られているようなので、Javaをインストールして再実行すると、美しいDataframeに・・・?

hinan_tokyo_df1.jpg

って全然ダメじゃん。ヘッダ部分で既に改行がうまく読めていないし、複数の列が結合されちゃってる。でもしらべてみると表の罫線を認識してくれるlatticeオプションをつけれるらしい。一応試してみると。。。

import pandas as pd
from tabula import read_pdf
df = read_pdf('hinan_tokyo.pdf',
                   lattice=True,
                   java_options="-Dfile.encoding=UTF8")

hinan_tokyo_df2.jpg

おぉ、ちゃんと表を読み込んでDataframeにしてくれた!凄いなtabula-py!!全国のPDF嫌いの皆さん、たまには抗ってみるのもいいかもしれません。

※ちなみに数ページのPDFを読み込むと、途中で列を分割して読み込まれてしまう(PDFは全8列なのに、1列と7列のDataframeに分かれたりする)ことがありますが、うまく繋ぐことで綺麗な表になりました。

避難場所の位置情報(緯度、経度)を求める

次に、避難場所の位置情報を求めます。pygeocoderというライブラリが便利そうですが、無料枠内に留めるための設定がわかりにくいので、今回はYahoo!のジオコーダAPIを使うことに。クレカの登録も要らず、すごく簡単。コードはこんな感じ。

import requests
import json

appid = 'APIのアプリケーションID'

# 住所文字列から緯度と経度を返す
def get_coordinates(addr):
    url = f'https://map.yahooapis.jp/geocode/V1/geoCoder?appid={appid}&query={addr}&output=json'
    r = requests.get(url)
    data = json.loads(r.text)
    coordinates = data['Feature'][0]['Geometry']['Coordinates'].split(',')
    return coordinates[1], coordinates[0]

ido, kdo = get_coordinates('東京都墨田区押上1丁目1−2')
print(ido) # → 35.71026838
print(kdo) # → 139.81215754

これを、先程のDataframeの各住所に対して実行し、新しい列として緯度と経度を追加すれば、避難場所情報の取得完了。

# 都市名と住所から緯度と経度を求めて、Dataframeのido列とkdo列に追加
for index, row in df.iterrows():
    ido, kdo = get_coordinates(row['city'] + row['addr'])
    df.at[index, 'ido'] = ido
    df.at[index, 'kdo'] = kdo

これで避難場所のデータは揃いました。と思いきや、ここで予期せぬ事態が。「住所から位置情報が特定できないデータ」が、東京だけで100件以上ありました。PDF資料上の住所では、位置を求めるには情報が不足しているみたい。あぁ〜これインプットにしちゃダメなやつだったわ。。。

ということで、結局これまでのコードは思い出にしまっておくことに。

全国の避難場所の取得(仕切り直し)

ここで一回立ち戻って考えてみます。そもそも、避難場所ほど重要な場所の位置情報が、整備されていないはずがないのでは?と思い、改めて調べてみると、ありました。位置情報付きの全国の避難場所情報が。ですよねぇ。。。

ということで、これを利用します。JSONフォーマットはこんな感じ。

{
    "type": "FeatureCollection",
    "features": [{
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": [126.7615284001, 26.3501419141]
        },
        "properties": {
            "施設・場所名": "大田タンク付近",
            "住所": "沖縄県久米島町大田地区",
            "洪水": "",
            "がけ崩れ、土石流及び地滑り": "",
            "高潮": "",
            "地震": "",
            "津波": "◎",
            "大規模な火事": "",
            "内水氾濫": "",
            "火山現象": "",
            "指定避難所との重複": "",
            "備考": ""
        }
    }]
}

わかりやすいし、位置情報もしっかり入ってる。他の避難所関連のアプリも、内容的にこの情報を利用しているように見えました。これをDataframeにしやすいように事前にcsvにしておきます。

import json
import pandas as pd

# jsonファイル読み込み
f = open('all.geojson', 'r', encoding='utf-8')
data = json.load(f)

# Dataframe初期化
df = pd.DataFrame(columns=['name', 'addr', 'flood', 'collapse',
                           'hightide', 'equake', 'tsunami', 'fire', 'inflood', 'volcanic'])

# featuresごとに1行のDataframe作成
for feature in data['features']:
    ido = feature['geometry']['coordinates'][1]
    kdo = feature['geometry']['coordinates'][0]

    name = feature['properties']['施設・場所名']
    addr = feature['properties']['住所']
    flood = feature['properties']['洪水']
    collapse = feature['properties']['がけ崩れ、土石流及び地滑り']
    hightide = feature['properties']['高潮']
    equake = feature['properties']['地震']
    tsunami = feature['properties']['津波']
    fire = feature['properties']['大規模な火事']
    inflood = feature['properties']['内水氾濫']
    volcanic = feature['properties']['火山現象']

    se = pd.Series([name, addr, ido, kdo, flood, collapse, hightide, equake, tsunami, fire, inflood, volcanic],
                   ['name', 'addr', 'ido', 'kdo', 'flood', 'collapse', 'hightide', 'equake', 'tsunami', 'fire', 'inflood', 'volcanic'])
    df = df.append(se, ignore_index=True)

# CSV化
df.to_csv('all.csv', index=False)

CSVにするだけならDataframe使う必要ないかもですが、コピペで作ったので気にしないでください。これで全国の避難場所の位置情報が揃いました。

位置情報(緯度と経度)から距離を取得する

避難場所から現在位置までの距離を求めるため、pyprojライブラリで2点間の直線距離を求めます。

import pyproj

ido1, kdo1 = 34.705185, 135.498468  # 梅田駅
ido2, kdo2 = 35.170897, 136.881537  # 名古屋駅

g = pyproj.Geod(ellps='WGS84')
result = g.inv(kdo1, ido1, kdo2, ido2)
distance = result[2]

print(distance) # → 136506.23110113875(メートル)

これで2点間の距離を求めることができるようになりました。あとは、LINEからの位置情報受信と、最も近い避難場所の特定、そしてその応答ができれば完成です。

LINEで位置情報メッセージをやりとりする

LINEとのやりとりは、LINE Messaging APIを使います。また、アプリは常に稼働させたいけどお金はかけたくないので、HEROKUの無料枠を使うことにします。この辺りの記事が参考になりました。

位置情報の取得と送信はこんな感じ。

# 位置情報のメッセージ取得
@handler.add(MessageEvent, message=LocationMessage)
def handle_location(event):
    ido = event.message.latitude   #緯度
    kdo = event.message.longitude  #経度
    ・・・
# 位置情報のメッセージ送信
    ・・・
    line_bot_api.reply_message(
        event.reply_token,
        [
            LocationSendMessage(
                title='表示タイトル',
                address='表示住所',
                latitude=34.705185,
                longitude=135.498468
            )
        ]
    )

ちなみに位置情報を送信するとLINE上で地図が表示されますが、titleやaddressは表示用の文字列でしかなく、表示位置はlatitudeとlongitudeの値から決まるようです。

最も近い避難場所の特定

では最後に、先程の避難場所全体の中から、現在位置に最も近い避難場所を求めます。LINEで位置情報を受信したときに動く処理ですね。長いので抜粋です。

# 避難場所CSVから全情報を取得
df = pd.read_csv('all.csv').fillna('')

# 列名と表示文字列のdict
disaster = {'equake': '地震', 'tsunami': '津波', 'flood': '洪水', 'collapse': '土砂災害',
          'inflood': '内水氾濫', 'hightide': '高潮', 'fire': '火災', 'volcanic': '火山噴火'}

# 2点間の距離計算
def calc_distance(ido1, kdo1, ido2, kdo2):
    g = pyproj.Geod(ellps='WGS84')
    result = g.inv(kdo1, ido1, kdo2, ido2)
    distance = result[2]
    return round(distance)

# 避難場所の災害種別情報
def format_disaster_info(row):
    message = ''
    for key in disaster:
        message += f'\n{disaster[key]}:{row[key]}'
    return message

# メッセージ(位置情報)受信時
@handler.add(MessageEvent, message=LocationMessage)
def handle_location(event):

    # 現在位置付近の避難場所を特定対象にする
    ido_from = event.message.latitude - 0.05
    ido_to = event.message.latitude + 0.05
    kdo_from = event.message.longitude - 0.05
    kdo_to = event.message.longitude + 0.05
    df_near = df.query(
        f'ido > {ido_from} and ido < {ido_to} and kdo > {kdo_from} and kdo < {kdo_to}')

    # 現在位置から一番近い避難場所を特定する
    min_distance = None
    min_row = None
    for index, row in df_near.iterrows():
        hinan_ido = row['ido']
        hinan_kdo = row['kdo']
        distance = calc_distance(
            event.message.latitude, event.message.longitude, hinan_ido, hinan_kdo)
        if min_distance is None or distance < min_distance:
            min_distance = distance
            min_row = row

    if min_distance == None:
        # 避難場所が見つからない場合は固定メッセージ送信
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='近くに避難場所が見つからないよ!')
        )
    else:
        # 避難場所が見つかった場合は詳細と位置情報送信
        message = f"1番近い避難場所はココ!\n<{min_row['name']}>"
        message += format_disaster_info(min_row)
        line_bot_api.reply_message(
            event.reply_token,
            [
                TextSendMessage(text=message),
                LocationSendMessage(
                    title=f"{min_row['name']}(距離:{min_distance}M)",
                    address=min_row['addr'],
                    latitude=min_row['ido'],
                    longitude=min_row['kdo']
                )
            ]
        )


全量は後でGitに上げます。

あとがき

先日、一般に売られているビールを、「注ぎ方」を変えることで味を変え、人気になった酒屋さんをテレビで見ました。

プログラミングも同じだと思います。つまり、どこにでもある、誰にでも書けるプログラムでも、「見せ方」や「提供方法」を変えることで、ユーザにとって使いやすくなったり、面白いものになることがあるでしょう。

例えば、以前にAIでLINEから自動文字起こししてくれる「文字おこし君」を作りました。@kkdmgs110さん)が話題になったのも、それが如実に現れた結果だと思います。

今回作ったものは話題になるほどのレベルではないかもしれませんが、どう作るかだけではなく、それがどう使われるか。世の中に何が求められるか。そういう視点を大切にしていきたいですね。


※避難場所情報については、国土地理院ウェブサイトの指定緊急避難場所データを元に加工し表示しています。
※本アプリは、通知なく稼働を停止する場合があります。また、避難場所情報の更新予定はありません。安定性や情報量的にも、細かい避難所情報を探す際は「Yahoo!防災速報」などの大手公式アプリを使うことをオススメします。

何かあればTwitterまで。