Python
api
ポエム
ぐるナビ

ぐるなびAPIのサンプルコードでポエムを書いた(Python版)

More than 1 year has passed since last update.


この記事を読む前に

ぐるナビAPIのサンプルコードでポエム書いた - Qiita


この記事に悪意はありません.単に「公式のサンプルコードとしてこれは如何なものか…」と感じたので,初心者が困惑しないように適当に手直ししてみたまでです.

もちろん自分も汚いコードを存分に書いてきましたし,そもそもコードの美しさは個人の主観に左右されるところはあります.…が,それでもちょっとこれは…,という感じだったので,お許し下さい


本記事は上記事の便乗記事です。PythonのサンプルコードもPHPに負けず汚いコードたったので、手直しでコードを書いてみました。


動作環境はPython2.7.11、標準ライブラリに入っているものだけで実装しています。


コード比較


ぐるなびサンプルコード

ぐるなび Web Service - レストラン検索API


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#*****************************************************************************************
# ぐるなびWebサービスのレストラン検索APIで緯度経度検索を実行しパースするプログラム
# 注意:ここでは緯度と経度の値は固定でいれています。
#    APIアクセスキーの値にはユーザ登録で取得したものを入れてください。
#*****************************************************************************************
import sys
import urllib
import json

####
# 変数の型が文字列かどうかチェック
####
def is_str( data = None ) :
if isinstance( data, str ) or isinstance( data, unicode ) :
return True
else :
return False

####
# 初期値設定
####
# APIアクセスキー
keyid = "input your accesskey"
# エンドポイントURL
url = "http://api.gnavi.co.jp/RestSearchAPI/20150630/"
# 緯度・経度、範囲を変数に入れる
# 緯度経度は日本測地系で日比谷シャンテのもの。範囲はrange=1で300m以内を指定している。
# 緯度
latitude = "35.670083"
# 経度
longitude = "139.763267"
# 範囲
range = "1"

####
# APIアクセス
####
# URLに続けて入れるパラメータを組立
query = [
( "format", "json" ),
( "keyid", keyid ),
( "latitude", latitude ),
( "longitude", longitude ),
( "range", range )
]
# URL生成
url += "?{0}".format( urllib.urlencode( query ) )
# API実行
try :
result = urllib.urlopen( url ).read()
except ValueError :
print u"APIアクセスに失敗しました。"
sys.exit()

####
# 取得した結果を解析
####
data = json.loads( result )

# エラーの場合
if "error" in data :
if "message" in data :
print u"{0}".format( data["message"] )
else :
print u"データ取得に失敗しました。"
sys.exit()

# ヒット件数取得
total_hit_count = None
if "total_hit_count" in data :
total_hit_count = data["total_hit_count"]

# ヒット件数が0以下、または、ヒット件数がなかったら終了
if total_hit_count is None or total_hit_count <= 0 :
print u"指定した内容ではヒットしませんでした。"
sys.exit()

# レストランデータがなかったら終了
if not "rest" in data :
print u"レストランデータが見つからなかったため終了します。"
sys.exit()

# ヒット件数表示
print "{0}件ヒットしました。".format( total_hit_count )
print "----"

# 出力件数
disp_count = 0

# レストランデータ取得
for rest in data["rest"] :
line = []
id = ""
name = ""
access_line = ""
access_station = ""
access_walk = ""
code_category_name_s = []
# 店舗番号
if "id" in rest and is_str( rest["id"] ) :
id = rest["id"]
line.append( id )
# 店舗名
if "name" in rest and is_str( rest["name"] ) :
name = u"{0}".format( rest["name"] )
line.append( name )
if "access" in rest :
access = rest["access"]
# 最寄の路線
if "line" in access and is_str( access["line"] ) :
access_line = u"{0}".format( access["line"] )
# 最寄の駅
if "station" in access and is_str( access["station"] ) :
access_station = u"{0}".format( access["station"] )
# 最寄駅から店までの時間
if "walk" in access and is_str( access["walk"] ) :
access_walk = u"{0}分".format( access["walk"] )
line.extend( [ access_line, access_station, access_walk ] )
# 店舗の小業態
if "code" in rest and "category_name_s" in rest["code"] :
for category_name_s in rest["code"]["category_name_s"] :
if is_str( category_name_s ) :
code_category_name_s.append( u"{0}".format( category_name_s ) )
line.extend( code_category_name_s )
# タブ区切りで出力
print "\t".join( line )
disp_count += 1

# 出力件数を表示して終了
print "----"
print u"{0}件出力しました。".format( disp_count )
sys.exit()


僕が書いたコード

#!/usr/bin/env python

# -*- coding: utf-8 -*-
import sys
import urllib
import json

def gnavi_api():
latitude = '35.670083'
longitude = '139.763267'
key = 'input your key'
url = "http://api.gnavi.co.jp/RestSearchAPI/20150630/"
search_range = '1'
params = urllib.urlencode({
'format': 'json',
'keyid': key,
'latitude': latitude,
'longitude': longitude,
'range': search_range
})
try:
responce = urllib.urlopen(url + '?' + params)
return responce.read()
except:
raise Exception(u'APIアクセスに失敗しました')

def do_json(data):
parsed_data = json.loads(data)
if 'error' in parsed_data:
if 'message' in parsed_data:
raise Exception(u'{0}'.format(parsed_data['message']))
else:
raise Exception(u'データ取得に失敗しました')
total_hit_count = parsed_data.get('total_hit_count', 0)

if total_hit_count < 1:
raise Exception(u'指定した内容ではヒットしませんでした\nレストランデータが存在しなかったため終了します')
print('{0}件ヒットしました。'.format(total_hit_count))
print('---')

for (count, rest) in enumerate(parsed_data.get('rest')):
access = rest.get('access', {})
id_ = rest.get('id', '')
name = u'{0}'.format(rest.get('name', ''))
access_line = u'{0}'.format(access.get('line', ''))
access_station = u'{0}'.format(access.get('station', ''))
access_walk = u'{0}分'.format(access.get('walk', ''))
categories= rest.get('code', {}).get('category_name_s', [])
category_names = filter(lambda n: isinstance(n, (str,unicode)), categories)
result_list = [id_, name, access_line, access_station, access_walk] + category_names
result = '\t'.join(result_list)
print(result)
print('---')
print(u'{0}件出力しました'.format(count+1))

if __name__ == '__main__':
try:
my = gnavi_api()
do_json(my)
except Exception as e:
print(e.message.encode('utf-8'))

半分くらいになりました。わーい!


ポエム


ビルドイン関数の上書き

range     = "1"

Pythonのビルドイン関数のrangeを上書きするのはやめましょう。 サンプルコードとはいえこれより下でrange使えなくなります。


:poop: sys.exit() :poop:

# エラーの場合

if "error" in data :
if "message" in data :
print u"{0}".format( data["message"] )
else :
print u"データ取得に失敗しました。"
sys.exit()

グローバルにtry-except節も書かずに処理をベタ書きしてる弊害なのですが、エラーが起きた時に終了する方法が:poop:sys.exit():poop:呼ぶしかなくなっています。例外機構をしっかり使いましょう。また、このモジュールimportしたと思ったらAPI取得に失敗してそのままプログラムが落ちたりするといったミスに繋がります。モジュールのトップレベルで:poop:sys.exit():poop:するのはやめましょう。


修正案.py

def do_json(data):

parsed_data = json.loads(data)
if 'error' in parsed_data:
if 'message' in parsed_data:
raise Exception(u'{0}'.format(parsed_data['message']))
else:
raise Exception(u'データ取得に失敗しました')
#省略...

if __name__ == '__main__':
try:
#...省略
do_json(data)
except Exception as e:
print(e.message)


このように呼び出し元で出てきた例外をまとめて受け取って処理することにより、呼び出し先でいちいち:poop:sys.exit():poop:しなくてよくなります。


辞書型で存在するかわからないインデックスを参照したいときはgetメソッドを使おう

if "total_hit_count" in data :

total_hit_count = data["total_hit_count"]

# ヒット件数が0以下、または、ヒット件数がなかったら終了
if total_hit_count is None or total_hit_count <= 0 :
print u"指定した内容ではヒットしませんでした。"
sys.exit()

Pythonでは存在してないインデックスに添字アクセスするとエラーを吐いて落ちてしまいます。それを避けるために存在しているかどうかを先にif文を使って判定して代入しています。

しかし、Pythonの辞書型にはgetというとても便利なメソッドが生えています。:thumbsup::thumbsup::thumbsup:


5. 組み込み型 — Python 2.7.x ドキュメント


get(key[, default])(原文)

もし key が辞書にあれば、 key に対する値を返します。そうでなければ、 default を返します。 default が与えられなかった場合、デフォルトでは None となります。そのため、このメソッドは KeyError を送出することはありません。


このメソッドを使うことにより上記のコードは以下のように書くことが出来ます。

total_hit_count = parsed_data.get('total_hit_count', 0)

if total_hit_count < 1:
raise Exception(u'指定した内容ではヒットしませんでした\nレストランデータが存在しなかったため終了します')

要素が存在しない可能性に怯える夜も、total_hit_countNoneが入ってる可能性に怯える朝にもサヨナラです!。


ループを数え上げたいときはenumerateを使おう

# 出力件数

disp_count = 0

# レストランデータ取得
for rest in data["rest"] :
line = []
id = ""
name = ""
access_line = ""
access_station = ""
access_walk = ""
code_category_name_s = []
# 店舗番号
if "id" in rest and is_str( rest["id"] ) :
id = rest["id"]
line.append( id )
# 店舗名
if "name" in rest and is_str( rest["name"] ) :
name = u"{0}".format( rest["name"] )
line.append( name )
if "access" in rest :
access = rest["access"]
# 最寄の路線
if "line" in access and is_str( access["line"] ) :
access_line = u"{0}".format( access["line"] )
# 最寄の駅
if "station" in access and is_str( access["station"] ) :
access_station = u"{0}".format( access["station"] )
# 最寄駅から店までの時間
if "walk" in access and is_str( access["walk"] ) :
access_walk = u"{0}分".format( access["walk"] )
line.extend( [ access_line, access_station, access_walk ] )
# 店舗の小業態
if "code" in rest and "category_name_s" in rest["code"] :
for category_name_s in rest["code"]["category_name_s"] :
if is_str( category_name_s ) :
code_category_name_s.append( u"{0}".format( category_name_s ) )
line.extend( code_category_name_s )
# タブ区切りで出力
print "\t".join( line )
disp_count += 1

# 出力件数を表示して終了
print "----"
print u"{0}件出力しました。".format( disp_count )

上記のコードは、数え上げるために一つ変数を用意してあげてループ枚に変数をインクリメントしています。


しかしPythonにはインデックス付きでループを回したい場合に使える超便利なビルドイン関数があります。

2. 組み込み関数 — Python 2.7.x ドキュメント


enumerate(sequence, start=0)(原文)

列挙オブジェクトを返します。 sequence はシーケンス型、イテレータ型、反復をサポートする他のオブジェクト型のいずれかでなければなりま せん。 enumerate() が返すイテレータの next() メソッドは、 (ゼロから始まる) カウント値と、値だけ sequence を反復操作して得ら れる、対応するオブジェクトを含むタプルを返します。 enumerate() はインデクス付けされた値の列: (0, seq[0]), (1, seq[1]), (2, seq[2]), ... を得るのに便利です。


説明文見てもよくわからないですね。コードで例を書いてみます

for (count, i) in enumerate(['spam', 'ham', 'eggs']):

print(u'{0}: {1}'.format(count, i))

# 実行結果
# 0: spam
# 1: ham
# 2: eggs

このようにループ変数を自前でインクリメントしながらループを回さなくてもインデックスを取得することが出来ます。便利!!


ひとつ前の見出しで話したテクニックとenumerateを用いることにより先程ののfor文は以下のように書くことが出来ます。

for (count, rest) in enumerate(parsed_data.get('rest')):

access = rest.get('access', {})
id_ = rest.get('id', '')
name = u'{0}'.format(rest.get('name', ''))
access_line = u'{0}'.format(access.get('line', ''))
access_station = u'{0}'.format(access.get('station', ''))
access_walk = u'{0}分'.format(access.get('walk', ''))
categories= rest.get('code', {}).get('category_name_s', [])
category_names = filter(lambda n: isinstance(n, (str,unicode)), categories)
result_list = [id_, name, access_line, access_station, access_walk] + category_names
result = '\t'.join(result_list)
print(result)
print('---')
print(u'{0}件出力しました'.format(count+1))


変更点



  • enumerateのループにした


  • idがビルドイン関数と名前がかぶっていたためid_に変更


  • dict.getでデータを取得するようにしたので最初に空文字列などで代入したり、if文で要素が存在するかチェックしなくて良くなった。

  • 文字列かどうか判別するために わざわざis_str関数を作っていたが、isinstanceofにtupleでクラスを返すビルドイン関数を入れて、どちらかのインスタンスだった場合 Trueを返すのでそちらを採用し、is_str関数を消した。


最後に

突っ込みどころ多い気がしたけど意外に少なかった気がします。

返って来たデータが文字列かどうかをとても比較してる気がしますが、これって文字列以外が返って来る場合があるんですかね?ドキュメント見る限り、力強く文字列で返す感じでドキュメント書かれていますが、本当にその比較は必要なんでしょうか?

Python3.xを使えばunicodeの呪縛から解き放たれるのでPython3.xを使いましょう