読者です 読者をやめる 読者になる 読者になる

じゅむてっくブログ

"人力ななぴ"管理人jumのtechblog

railsふかぼり01:railsでWebAPIを叩く

jumtechです。

今回から始まった、ふかぼり企画。

railsとか、一行書いただけで裏側でものすごい処理をしていたりするので、 特に私のような初心者はちゃんと何が起こっているのか理解した上で使わないと、
ついついわかった気になってしまいがちです。

その裏側に光を当てて、 理解を深める企画です。

今回は、 railsでWeb APIを叩く機会があったので、 そのコードを深掘りしてみました。

用語解説

WebAPI

Webの技術を使って、外部サイトの機能や情報を取得するための手段。
GoogleMapをWebサイト上に掲載するときとかに使う。
特定のURLにパラメタ付きのリクエストを送ると、欲しいレスポンスが返ってくる。
Qiita:WebAPIについての説明

JSON

データの形式。
hashと同じく、keyとvalueが並んでいる。 XMLCSVなどと同じレイヤーの言葉。
Wikipedia:JSON

参考にした記事

Qiita:RubyでWebAPIを叩く
この記事を元に、railsで郵便番号から住所を表示するプログラムを作ります。

使用WebAPI

郵便番号検索API
郵便番号をリクエストパラメタに指定すると、
住所を返してくれるAPI
試しにブラウザに
http://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060
と入れると、
生のJSON形式で結果が表示されます。

{
    "message": null,
    "results": [
        {
            "address1": "高知県",
            "address2": "南国市",
            "address3": "蛍が丘",
            "kana1": "コウチケン",
            "kana2": "ナンコクシ",
            "kana3": "ホタルガオカ",
            "prefcode": "39",
            "zipcode": "7830060"
        }
    ],
    "status": 200
}

このJSON形式の文字列を、プログラム内でいい感じに処理します。

下準備

  • rails new apitest
    テスト用のrailsプロジェクト作成
  • cd apitest
    ディレクトリ移動
  • rails generate controller address show
    addressコントローラーとshowアクションを生成
  • apitest/views/show.html.erbに結果を表示させるhtmlを書く
<p>
郵便番号: <%= @zipcode %>
</p>
<p>
都道府県: <%= @address1 %>
</p>
<p>
市区町村: <%= @address2 %>
</p>
<p>
町名: <%= @address3 %>
</p>
<p>
<!-- リクエストパラメタを表示-->
リクエストパラメタ: <%= @query%>
</p>
<!-- 生のjsonを表示-->
result: <%= @result%>
</p>
<p>
  <!-- エラーメッセージを表示-->
message: <%= @message%>
</p>

参考元記事のコードを移植

Qiita:RubyでWebAPIを叩く
- apitest/controllers/address_controller.rbに、APIを叩いてインスタンス変数に入れる処理を記述 - ログは取らずに、@messageに格納

# 以下のrequireは、railsの自動require機能により不要になる(!)
=begin
require 'net/http'
require 'uri'
require 'json'
=end
class AddressController < ApplicationController
  def show
    # hash形式でパラメタ文字列を指定し、URL形式にエンコード
    params = URI.encode_www_form({zipcode: '7830060'})
    # URIを解析し、hostやportをバラバラに取得できるようにする
    uri = URI.parse("http://zipcloud.ibsnet.co.jp/api/search?#{params}")
    # リクエストパラメタを、インスタンス変数に格納
    @query = uri.query

    # 新しくHTTPセッションを開始し、結果をresponseへ格納
    response = Net::HTTP.start(uri.host, uri.port) do |http|
      # 接続時に待つ最大秒数を設定
      http.open_timeout = 5
      # 読み込み一回でブロックして良い最大秒数を設定
      http.read_timeout = 10
      # ここでWebAPIを叩いている
      # Net::HTTPResponseのインスタンスが返ってくる
      http.get(uri.request_uri)
    end
    # 例外処理の開始
    begin
      # responseの値に応じて処理を分ける
      case response
      # 成功した場合
      when Net::HTTPSuccess
        # responseのbody要素をJSON形式で解釈し、hashに変換
        @result = JSON.parse(response.body)
        # 表示用の変数に結果を格納
        @zipcode = @result["results"][0]["zipcode"]
        @address1 = @result["results"][0]["address1"]
        @address2 = @result["results"][0]["address2"]
        @address3 = @result["results"][0]["address3"]
      # 別のURLに飛ばされた場合
      when Net::HTTPRedirection
        @message = "Redirection: code=#{response.code} message=#{response.message}"
      # その他エラー
      else
        @message = "HTTP ERROR: code=#{response.code} message=#{response.message}"
      end
    # エラー時処理
    rescue IOError => e
      @message = "e.message"
    rescue TimeoutError => e
      @message = "e.message"
    rescue JSON::ParserError => e
      @message = "e.message"
    rescue => e
      @message = "e.message"
    end
  end
end

実行

  • rails server -b 0.0.0.0
  • http://[host名]:3000/address/showにブラウザからアクセス
郵便番号: 7830060

都道府県: 高知県

市区町村: 南国市

町名: 蛍が丘

リクエストパラメタ: zipcode=7830060

result: {"message"=>nil, "results"=>[{"address1"=>"高知県", "address2"=>"南国市", "address3"=>"蛍が丘", "kana1"=>"コウチケン", "kana2"=>"ナンコクシ", "kana3"=>"ホタルガオカ", "prefcode"=>"39", "zipcode"=>"7830060"}], "status"=>200}
message:
  • apitest/controllers/address_controller.rb内のパラメタを"7830060"以外にして、結果が変わることを確認。
郵便番号:

都道府県:

市区町村:

町名:

リクエストパラメタ: zipcode=9999999

result: {"message"=>nil, "results"=>nil, "status"=>200}
message: undefined method `[]' for nil:NilClass

この例では、
存在しない郵便番号を設定。
HTTPのstatusは200(正常)だが、
存在しないのでresultsがnilで返ってくる。
(その後、nilに対してで値の取得をしようとしてundefined method `' for nil:NilClassのエラーが起きている。)

解説

(参考サイトのコード内コメントがすごく詳しいので、もはやあまり言うことないです)

ライブラリ読み込み

ruby用のコードでは明示的にrequireでライブラリを読み込んでいますが、
railsではなくても動きます。
railsでは未知のクラスが登場したら自動的にライブラリを読み込む仕組みがあるからです。
例えば

params = URI.encode_www_form({zipcode: '7830060'})

の部分でURIクラスが登場します。
このとき、自動でuri.rbの検索が走ります。
まずrubyの$LOAD_PATHを検索し、
なければrails側で設定されているautoload-paths(デフォルトでは、RAILS_ROOT/app/の中のディレクトリ)を検索するようです。
$LOAD_PATHを確認してみると、私の環境では - ruby -e 'puts $LOAD_PATH'

/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/did_you_mean-1.0.0/lib
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/site_ruby/2.3.0
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/site_ruby/2.3.0/i686-linux
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/site_ruby
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/vendor_ruby/2.3.0
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/vendor_ruby/2.3.0/i686-linux
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/vendor_ruby
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/2.3.0
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/2.3.0/i686-linux

となります。
この中の、
/home/vagrant/.rbenv/versions/2.3.1/lib/ruby/2.3.0
を見ると、uri.rb、json.rb、net/http.rbが全て見つかります。
Qiita:Railsの自動読み込みについて
RailsGuide:Autoloading and Reloading Constants

URIを構築

rubyの標準ライブラリのuri.rbを使います。
まず、

params = URI.encode_www_form({zipcode: '7830060'})  

で、郵便番号検索APIのリクエストパラメタであるzipcodeをhash形式で指定し、
"zipcode=7830060"という文字列に変換しています。
今回はパラメタが一つで半角英数なのでURI.encode_www_formを使う恩恵はあまりないですが、
特にパラメタの値に日本語を含んだりすると、
ハードコーディングするのは大変なので必須になります。

その後、

uri = URI.parse("http://zipcloud.ibsnet.co.jp/api/search?#{params}")  

で、郵便番号検索API用のURI文字列をURI::HTTPクラスに変換し、
uriに格納します。

URIモジュール等の詳しい仕様は、以下。
Rubyリファレンスマニュアル:URIモジュール Rubyリファレンスマニュアル:URI::HTTPクラス

WebAPIを叩く

rubyの標準ライブラリのnet/http.rbを使います。

response = Net::HTTP.start(uri.host, uri.port) do |http|

以下で、先ほど構築したURIに対してHTTPリクエストを投げます。
doでブロックを与えている理由は、
Net::HTTPの仕様で、ブロックが終わった時に接続を自動で閉じてくれるからっぽいです。

ブロックを与えた場合には生成したオブジェクトをそのブロックに 渡し、ブロックが終わったときに接続を閉じます。このときは ブロックの値を返り値とします。

「ブロックの値を返り値とします」って言っている割に、returnとかしなくていいのかよと思いますが、
ブロックの仕様で、最後に評価された値(ここでは、"http.get(url.request_url)")が自動で返されるみたいです。
rubyすごい。
というわけで、
無事にresponseに郵便番号検索APIから戻って来たレスポンスが格納されます。
Net::HTTPの詳しい仕様は、以下。
Rubyリファレンスマニュアル:Net::HTTP

取得したJSON形式の文字列を加工する

JSON自体はWebAPIとは直接関係ないですが、
WebAPIのレスポンスはJSON形式であることが多いようです。
rubyの標準ライブラリのjson.rbを使います。

@result = JSON.parse(response.body)

まず、
Net::HTTPResponseのインスタンスであるresponseから、
実データであるbody要素を取り出しています。

{
    "message": null,
    "results": [
        {
            "address1": "高知県",
            "address2": "南国市",
            "address3": "蛍が丘",
            "kana1": "コウチケン",
            "kana2": "ナンコクシ",
            "kana3": "ホタルガオカ",
            "prefcode": "39",
            "zipcode": "7830060"
        }
    ],
    "status": 200
}

JSON.parse()は引数にJSON形式の文字列を取り、
Rubyのhashを返します。

{"message"=>nil, "results"=>[{"address1"=>"高知県", "address2"=>"南国市", "address3"=>"蛍が丘", "kana1"=>"コウチケン", "kana2"=>"ナンコクシ", "kana3"=>"ホタルガオカ", "prefcode"=>"39", "zipcode"=>"7830060"}], "status"=>200}

これで、@resultに郵便番号検索APIの結果がhash化されて格納されます。
あとは、煮るなり焼くなり好きにします。

例えば、

@zipcode = @result["results"][0]["zipcode"]

では、@result内の"key=result"の値(配列[{...}])の、0番目の要素(hash{"address"〜200})の、"key=zipcode"の値を取得しています。
ややこしいですね。。。
ネストの深いhashから値を取得する方法については、
いろいろあるようです。
Qiita:RubyでネストしたHashやArrayから値を取り出す方法いろいろ
JSONモジュールの詳しい仕様は、以下を参照。
Rubyリファレンスマニュアル:JSON

エラー処理

なくても動きますが、
実務ではHTTPリスエストはエラー要因が多いので、
頑張った方がいいのだろう、きっと。
begin-rescueというrubyのエラー処理構文(javaでいうtry-catch)で、
Errorの内容ごとに処理を書いています。
また、HTTPリクエストは返ってきたけど、状態がNet::HTTPSuccessではないケースについては、

case response

で振り分けを行っています。

まとめ

requireしなくてもいい理由をめっちゃ調べたら時間かかった。
WebAPIを組み合わせたら凄いアプリが簡単にできるから頑張ろう(小並感)。