前回に引き続き、これまでにチュートリアルで学んできたことを使ってPythonで実用的なツールを作成する方法を紹介していく。今回は、IPアドレスから、IPアドレスを使っているサーバの位置情報を取得する方法を取り上げる。

実用的なコマンド - IPアドレスから位置情報を取得する

IPアドレスから、IPアドレスを使っているサーバの位置情報を取得する方法はいろいろとあるのだが、ここではcurlコマンドを利用する方法をPythonで実装するやり方を紹介しよう。

■curlコマンドでIPアドレスから位置情報を得る

% curl https://ipvigilante.com/93.184.216.34
{"status":"success","data":{"ipv4":"93.184.216.34","continent_name":"North America","country_name":"United States","subdivision_1_name":"Massachusetts","subdivision_2_name":null,"city_name":"Norwell","latitude":"42.15080","longitude":"-70.82280"}}
%

URLにIPアドレスを含めてipvigilante.comにHTTP GETリクエストを送信すると、該当するIPアドレスの位置情報がJSON形式で返ってくるわけだ。Pythonで同じようにデータを取得し、人が見やすい形式に変換して出力してみよう。

urlopen()でデータを取得

URLを指定してコンテンツを取得する方法として、以前、Pythonのurlopen()を使う方法を取り上げた。前述したcurlコマンドの実行は、Pythonで実装するなら次のようになる。

■Pythonで実装したcurlコマンドの実行(get_geoipinfo.py)

#!/usr/bin/env python3

from urllib.request import urlopen

ipaddr='93.184.216.34'
geoipurl='https://ipvigilante.com/'

geodata=''
with urlopen(geoipurl + ipaddr) as response:
    for line in response:
        geodata += line.decode('utf-8')
    print(geodata)

このスクリプトを実行すると次のようになる。

% ./get_geoipinfo.py
{"status":"success","data":{"ipv4":"93.184.216.34","continent_name":"North America","country_name":"United States","subdivision_1_name":"Massachusetts","subdivision_2_name":null,"city_name":"Norwell","latitude":"42.15080","longitude":"-70.82280"}}
%

curlコマンドで実行したように、指定したURLからコンテンツが取得できたことがわかる。

JSONデータを扱う

PythonはjsonモジュールでJSONを扱うための基本的な方法を提供している。jsonモジュールを使うと、JSON形式の文字列をPythonのオブジェクトに変換することや、その逆も可能だ。まず、このモジュールの使い方を探っていこう。

次のサンプルは、JSONデータを文字列として用意してから、json.loads(文字列)を使ってPythonのオブジェクトへ変換するというものだ。Pythonのオブジェクトに変換した後は、json.dumps()で文字列に変換し直すことができる。いったんPythonオブジェクトに変換してしまえばPythonで扱うことができるので、json.loads()とjson.dumps()の扱いがわかればいいというわけだ。以下に示す「gen_json_1.py」をご覧いただきたい。

#!/usr/bin/env python3

import json

# JSONに相当するデータを文字列で用意
jsonstr = '{"status":"success","data":{"ipv4":"93.184.216.34","continent_name":"North America","country_name":"United States","subdivision_1_name":"Massachusetts","subdivision_2_name":null,"city_name":"Norwell","latitude":"42.15080","longitude":"-70.82280"}}'

# 文字列からJSONに相当するディクショナリデータを生成
jsondic = json.loads(jsonstr)

# ディクショナリデータを形式指定なしでJSON文字列として出力
print('----')
print(json.dumps(jsondic))

# ディクショナリデータをインデント指定してJSON文字列として出力
print('----')
print(json.dumps(jsondic, indent=4))

上記のスクリプトを実行すると次のようになる。

% ./gen_json_1.py
----
{"status": "success", "data": {"ipv4": "93.184.216.34", "continent_name": "North America", "country_name": "United States", "subdivision_1_name": "Massachusetts", "subdivision_2_name": null, "city_name": "Norwell", "latitude": "42.15080", "longitude": "-70.82280"}}
----
{
    "status": "success",
    "data": {
        "ipv4": "93.184.216.34",
        "continent_name": "North America",
        "country_name": "United States",
        "subdivision_1_name": "Massachusetts",
        "subdivision_2_name": null,
        "city_name": "Norwell",
        "latitude": "42.15080",
        "longitude": "-70.82280"
    }
}
%

なお、json.loads()とjson.dumps()の「s」はstring(文字列)の「s」を意味している。

では、JSON文字列ではなく、最初からPythonオブジェクトとしてデータを用意しておけばjson.loads()で読み込む必要はないんじゃないか、と思われるかもしれない。その通りだ。次に示す「gen_json_2.py」のように、データを直接Pythonオブジェクトとして用意してあれば、json.loads()することなくjson.dumps()に指定して利用することができる。

#!/usr/bin/env python3

import json

# JSONに相当するデータをディクショナリで用意
jsondic = {"status":"success","data":{"ipv4":"93.184.216.34","continent_name":"North America","country_name":"United States","subdivision_1_name":"Massachusetts","subdivision_2_name":None,"city_name":"Norwell","latitude":"42.15080","longitude":"-70.82280"}}

# ディクショナリデータを形式指定なしでJSON文字列として出力
print('----')
print(json.dumps(jsondic))

# ディクショナリデータをインデント指定してJSON文字列として出力
print('----')
print(json.dumps(jsondic, indent=4))

gen_json_2.pyを実行すると次のようになる。

% ./gen_json_2.py
----
{"status": "success", "data": {"ipv4": "93.184.216.34", "continent_name": "North America", "country_name": "United States", "subdivision_1_name": "Massachusetts", "subdivision_2_name": null, "city_name": "Norwell", "latitude": "42.15080", "longitude": "-70.82280"}}
----
{
    "status": "success",
    "data": {
        "ipv4": "93.184.216.34",
        "continent_name": "North America",
        "country_name": "United States",
        "subdivision_1_name": "Massachusetts",
        "subdivision_2_name": null,
        "city_name": "Norwell",
        "latitude": "42.15080",
        "longitude": "-70.82280"
    }
}
%

Pythonから直接JSONデータを生成する場合には、上記のようにPythonオブジェクトから直接JSONデータに変換することになる。なお、JSONでは「null」という表記だが、Pythonではこれが「None」になる点に留意しておこう。

次に、JSON文字列をPythonオブジェクトに変換して、そこから特定のフォーマットに整形した文字列を出力する方法を考える。得られるデータはディクショナリになっているので、Pythonの文字列フォーマット機能を使えばフォーマットされた文字列を簡単に用意することができる。スクリプトは次の通りだ(gen_json_3.py)。

#!/usr/bin/env python3

import json

# JSONに相当するデータを文字列で用意
jsonstr = '{"status":"success","data":{"ipv4":"93.184.216.34","continent_name":"North America","country_name":"United States","subdivision_1_name":"Massachusetts","subdivision_2_name":null,"city_name":"Norwell","latitude":"42.15080","longitude":"-70.82280"}}'

# 文字列からJSONに相当するディクショナリデータを生成
jsondic = json.loads(jsonstr)

# 出力する文字列を作成
outputstr = """\
IPv4    :   {ip}
国 :   {cn}
市 :   {ci}
経度  :   {la}
緯度  :   {lo}\
""".format( ip=jsondic["data"]["ipv4"],
            co=jsondic["data"]["continent_name"],
            cn=jsondic["data"]["country_name"],
            s1=jsondic["data"]["subdivision_1_name"],
            s2=jsondic["data"]["subdivision_2_name"],
            ci=jsondic["data"]["city_name"],
            la=jsondic["data"]["latitude"],
            lo=jsondic["data"]["longitude"])

# 加工済み文字列を出力
print(outputstr)

gen_json_3.pyを実行すると、次のようになる。

% ./gen_json_3.py
IPv4    :   93.184.216.34
国 :   United States
市 :   Norwell
経度  :   42.15080
緯度  :   -70.82280
%

こんな感じで、Pythonでは比較的簡単にJSONデータを扱うことができる。JSONとPythonは、そもそも用意されているデータ型的に相性が良いのである。

ip2geoinfoコマンド

上記サンプルを組み合わせて、「ip2geoinfo.py」というコマンドを作ってみよう。これは、引数にIPアドレスを指定すると、指定されたIPアドレスの位置情報を出力するコマンドだ。引数はsysモジュールの「argv」で利用できる。まとめたスクリプト「ip2geoinfo.py」は次のようになる。

#!/usr/bin/env python3

from sys import argv
from urllib.request import urlopen
import json

# コマンドの使い方
usage="""\
usage:
    ip2geoinfo.py IPアドレス"""

# 引数が1つ以外だった場合はコマンドの使い方を出力して終了
if (2 != len(argv)):
    print(usage)
    exit()

# 指定されたIPアドレス
ipaddr=argv[1]
geoipurl='https://ipvigilante.com/'

# IPアドレスの医師情報をJSON文字列として取得
jsonstr=''
with urlopen(geoipurl + ipaddr) as response:
    for line in response:
        jsonstr += line.decode('utf-8')

# 文字列からJSONに相当するディクショナリデータを生成
jsondic = json.loads(jsonstr)

# 出力する文字列を作成
outputstr = """\
IPv4    :   {ip}
国 :   {cn}
市 :   {ci}
経度  :   {la}
緯度  :   {lo}\
""".format( ip=jsondic["data"]["ipv4"],
            co=jsondic["data"]["continent_name"],
            cn=jsondic["data"]["country_name"],
            s1=jsondic["data"]["subdivision_1_name"],
            s2=jsondic["data"]["subdivision_2_name"],
            ci=jsondic["data"]["city_name"],
            la=jsondic["data"]["latitude"],
            lo=jsondic["data"]["longitude"])

# 加工済み文字列を出力
print(outputstr)

ちなみに、コマンドらしくするために、引数の指定が間違っていた場合には「使い方」を出力するようにしておいた。引数の指定を間違えると、次のように引数部分に「IPアドレス」を入力することを示す。

% ./ip2geoinfo.py
usage:
    ip2geoinfo.py IPアドレス
%

ip2geoinfo.pyにIPアドレスを指定して実行すると、次のような出力を得ることができる。

% ./ip2geoinfo.py 93.184.216.34
IPv4    :   93.184.216.34
国 :   United States
市 :   Norwell
経度  :   42.15080
緯度  :   -70.82280
% ./ip2geoinfo.py 172.217.25.100
IPv4    :   172.217.25.100
国 :   United States
市 :   Mountain View
経度  :   37.41920
緯度  :   -122.05740
% ./ip2geoinfo.py 99.84.143.16
IPv4    :   99.84.143.16
国 :   United States
市 :   None
経度  :   37.75100
緯度  :   -97.82200
% ./ip2geoinfo.py 183.79.135.206
IPv4    :   183.79.135.206
国 :   Japan
市 :   Akasaka
経度  :   34.85000
緯度  :   137.30000
%

* * *

意外と簡単にツールを作れることがおわかりいただけたのではないだろうか。上記のサンプルは、JSONを返すタイプのWebサービス全般で応用が効く。シンプルな実装でここまでできることで、Pythonの便利さや強力さが少なからず、わかっていただけただろう。

参考資料