APIの基本

3.エラー処理を行って実践的な価格取得プログラムを作成する

前の記事のおさらいと本記事でやること

前の記事では複数の仮想通貨の価格を取得するプログラムを作成しました。

このままでもプログラムとしては動作しますし、大きな問題はありませんが、本記事では、さらにエラー処理を加えることにより、より実践的なプログラムを作成します。

エラー処理をする意味

エラー処理はプログラミングを書くうえで避けては通れない部分です。非常にシンプルなコードの場合は書かなくても良いこともありますが、以下のような場合は必須となります。

  • 外部に公開するプログラム(Webアプリなど)の場合
  • エラーが発生しても処理を中断させたくない場合

今回は三つの仮想通貨の価格を取得するプログラムを書きましたが、例えばそのうちの一つの通貨の取得中にエラーが発生しても処理を中断させたくない・・・といった場合には、エラー処理を書くことになります。

また、エラー処理を書くことで、コードが読みやすくなったり、バグが発生したときに原因を特定しやすくなるといったメリットもあります。

ただし、まだほとんどコードを自分で書いたことのないようなプログラミング入門者の場合は、エラー処理は難しい部分でもありますので、最初から頑張って書こうとせずに、必要に迫られたときに勉強し徐々に書けるようになれば十分でしょう。

APIと通信するときのエラー処理

エラー処理を書くときは、どんなエラーが発生するのかを想定するのが重要です。

APIとの通信時には次の三つのエラーが主に考えられます。

  1. APIサーバーとの通信時
  2. 通信に成功後に発生する後のエラー(APIから結果のjsonは返ってくるがjsonの中身がエラー情報になっている場合)
  3. その他データ処理時のエラー

1と2のエラーについてはサーバーがダウン・混雑状態である可能性や、インターネットに接続されていない状態である可能性が排除できないため、APIとの通信のコードを書くときはエラー処理が必須といってもいいです。

3のエラーはプログラムによっては必要になることもありますが、本記事では省略して1と2のエラー処理についてのみ書いていきます。

APIサーバーとの通信時のエラー処理(requestsライブラリのエラー処理)

まずは、APIそのものにつながらず404エラーやタイムアウトなどのエラーが返ってくる場合のエラー処理です。

雑にエラー処理を書いてみる

まずは前の記事で作ったプログラムに雑にエラー処理を書き加えてみましょう。

import requests

urls = ["https://public.bitbank.cc/btc_jpy/ticker",
       "https://public.bitbank.cc/eth_btc/ticker",
       "https://public.bitbank.cc/mona_jpy/ticker"]
results = []
for url in urls:
    try:
        r = requests.get(url, timeout=5)
        r = r.json()
        last = r['data']['last']
        results.append(last)
    except:
        results.append(0)
...

9~12行目をtry節で囲むことにより、何らかのエラーが9~12行目で発生した場合に、14行目のexcept節に処理が移動することになります。

この場合は、エラーが起きたらリストに「0」を代入するというコードになっています。try~except文を全く書かないと、エラーが発生した瞬間に処理が止まってしまいますが、こうすることにより、途中でエラーが発生しても最後まで処理が止まらず、結果の変数resultsには何らかの値が代入されることになります。

しかしこの書き方は、11行目や12行目の変数を代入したりリストに要素を追加したりする段階のエラーでもexcept節に処理が移動してしまうため、バグの原因が逆に分かりづらくなるというデメリットが存在します。

何も考えずに全体をtryで囲うのは楽ではあるので、趣味で使うようなごく簡単なプログラムではこれでも十分な場合もありますが、より優れたプログラムを書くためにはこのような書き方はなるべくしないようにしましょう。

try節はエラーが起きそうな部分だけにする

今回はAPIとの通信時(=9行目)のエラー処理を書きたいので、そこの部分のみをtry節にするのがベストです。

for url in urls:
    try:
        r = requests.get(url, timeout=5)
    except:
        results.append(0)
        continue
    r = r.json()
    last = r['data']['last']
    results.append(last)

こうすることにより、13行目~15行目で想定しないエラーが発生すると、処理が止まりエラー内容が表示されるので、プログラム上のバグが見つかりやすくなります。

12行目のcontinueを書くことにより、13行目以降の処理がスキップされ、次のループに進ませることができます。

continue文の代わりにelse節で残りの部分を囲うことで残りの処理をスキップさせることも可能です。 今回はどちらでもいいですが、else節で囲うとインデントが深くなりコードが読みづらくなる場合もあるので、状況によって使い分けるのが良いでしょう。

ループや関数の途中にexcept節を挿入する場合は、except節の最後に「continue」やループを終了させたいときの「break」、関数の処理を終了させる「return」などを書くことが多いです。

エラーを特定して処理を書く

except文の横にエラー内容を書くとさらに良いでしょう。何も書かない場合は、プログラム上で発生するあらゆるエラーを捕捉してしまい、エラーの内容が特定しづらくなってしまうことがあります。

for url in urls:
    try:
        r = requests.get(url, timeout=5)
        r.raise_for_status()
    except requests.exceptions.RequestException as e:
        print("requests error:", e)
        results.append(0)
        continue
    r = r.json()
    last = r['data']['last']
    results.append(last)

11行目のexcept文のexceptions.RequestExceptionによって通信時に起きるrequestsライブラリ関連のすべてのエラー(例外)を捕捉することができます。

また、変数eにエラー内容を代入し12行目でprint文によりエラー内容を出力しています。

9行目のr.raise_for_status()を書かないと、404などのHTTPエラーがサーバー側から返されても捕捉することができずに、次の行へと処理が進んでしまいます。( HTTPエラーはプログラム上のエラーとして処理されないので、raise_for_status()によってわざとエラーを発生させています。)

このような書き方のほかにも、以下のようにHTTPのステータスコードを取得して、その場合分けによってエラー処理の代わりとすることがあります。

for url in urls:
    try:
        r = requests.get(url, timeout=5)
    except requests.exceptions.RequestException as e:
        print("requests error:", e)
        results.append(0)
        continue
    if r.status_code != 200:
        break
    r = r.json()
    last = r['data']['last']
    results.append(last)

status_codeによってステータスコードが取得できるので、そのステータスコードによって場合分けを行います。

except節はエラー内容によって細かく処理を分けることもできます。requestsライブラリでは、HTTPError、ConnectionError、Timeoutなどのエラーがあります。

for url in urls:
    try:
        r = requests.get(url, timeout=5)
        r.raise_for_status()
    except requests.exceptions.HTTPError as e:
        print("HTTPError:", e)
        results.append(0)
    except requests.exceptions.ConnectionError as e:
        print("ConnectionError:", e)
        results.append(0)
    except requests.exceptions.Timeout as e:
        print("TimeoutError:", e)
        results.append(0)
    except requests.exceptions.RequestException as e:
        print("RequestsError:", e)
        results.append(0)
    else:
        r = r.json()
        last = r['data']['last']
        results.append(last)

このような場合はcontinueをすべてのexcept節に書くのは面倒なので、else文を使うと良いでしょう。

なお、raise_for_status()で発生させたエラーはHTTPErrorとして処理されます。

上記の例では、どのexcept節でもすべて「0」をリストに追加する同じ文を書いていますが、実用的にはタイムアウトしたらリトライする処理を書くことなどが考えられます。

通信に成功後に発生する後のエラー

続いて通信時にはエラーが発生せずjsonを取得し辞書に変換することはできたが、そのデータがエラー情報だったという場合の処理です。

世の中のAPIの多くは、何らかのエラーが発生した場合、HTTPエラーを返した上でjson形式でエラー情報を返すようになっているので、ここの部分の処理は必ずしも必要でないことが多いかもしれません。

今回利用しているbitbank APIのエラーに関するドキュメントを確認してみましょう。

エラーが発生した場合はsuccessが0というjsonが返ってくるようです。

そこで、

    if r['success'] == 0:
        print("エラーコード:", r['data']['code'])
        results.append(0)
        break

というコードを挿入します。

通信に成功にしなかった場合(=successが0だった場合)、jsonとして返ってくるエラーコードをprintし、resultsに0を代入し、ループをbreakによって終了させています。

ここの条件分岐は、

    if r['success'] == 1:
        last = r['data']['last']
        results.append(last)
    else:
        print("エラーコード:", r['data']['code'])
        results.append(0)

と書くこともできます。

コードの読みやすさなども考慮してお好きな方法で書いてみてください。

bitbankのAPIではsuccessが0、つまり何らかのエラーが発生した場合は200以外のステータスコードが返されます。つまり、raise_for_status()によってexcept節が実行されて残りのコードがスキップされるので、上記のコードは実際には動作することはなく、書く必要もありません。

ただし、このようなときでもjson内のエラー情報を取得したい場面があるかと思います。その場合はraise_for_status()は書かずに、successの値によって場合分けするのが良いでしょう。

コードのまとめ

import requests

urls = ["https://public.bitbank.cc/btc_jpy/ticker",
       "https://public.bitbank.cc/eth_btc/ticker",
       "https://public.bitbank.cc/mona_jpy/ticker"]
results = []
for url in urls:
    try:
        r = requests.get(url, timeout=5)
        r.raise_for_status()
    except requests.exceptions.RequestException as e:
        print("RequestsError:", e)
        results.append(0)
        continue
        
    r = r.json()

    #bitbankの場合はすべてHTTPエラーとして処理されるようなので省略可
    if r['success'] == 0:
        print("エラーコード:", r['data']['code'])
        results.append(0)
        break

    last = r['data']['last']
    results.append(last)

results[1] = int(results[0]) * float(results[1])
print("BTC価格は{}円です。".format(results[0]))
print("ETH価格は{}円です。".format(results[1]))
print("MONA価格は{}円です。".format(results[2]))

これでエラー処理を追記したプログラムが完成しました。

エラーが発生した場合にはエラー内容が出力され、エラーが発生した仮想通貨ペアは「0円」として処理されることになります。

さらに実用的なプログラムへの応用

これでAPIによるデータ取得のプログラムが完成しましたが、実際には以下のような手順も組み合わせて使うことが多いと思われます。

定期取得

Pythonとは直接関係ありませんが、cronやWindowsタスクスケジューラなどで.pyファイルを定期実行することにより、定期的に価格を取得することが可能です。

データベースへの保存

今回はただ取得・表示するプログラムでしたが、このデータを毎回データベースやCSVなどに保存することにより、さらにデータを解析するプログラムに発展させることが可能です。

これらの応用プログラムをnoteで解説していますので、興味がある方はぜひご覧ください。

仮想通貨の価格を取得して分析するプログラミング解説~データベース構築&コード解説編~|みゃふのPythonプログラミング~応用編~|note
株や仮想通貨などの金額を定期的に自動で取得したり、取得した金額を分析したいと思ったことはありませんか? そんな方に、おススメの自動で価格を取得できるコードをご紹介します(^_-)-☆しかも同時に3つの仮想通貨を表示できちゃいます。 こんな方におススメ(*´▽`*) ・株や仮想通貨を分析したい ・決まった時間にデー...

タイトルとURLをコピーしました