🌐

【Rails】GeoLite2を使ってIPアドレスから都道府県を特定する

2024/04/22に公開

前提知識

  • Railsで簡単なCRUDアプリを作れる
  • GeoIP2のことを理解している

今回触れないこと

  • gemのインストール方法
  • active_hashの説明
  • CRUDの実装
  • GeoLiteの会員登録方法

コード実装

controllers/concerns/geoip_module.rbを実装する

  • geoip_module.rbを以下のように実装する
    • 環境変数は適宜変更してください
app/controllers/concerns/geoip_module.rb
  module GeoipModule
 
    def get_to_geoip(ip)
      uri = URI.parse("https://geolite.info/geoip/v2.1/city/#{ip}")
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true
      req = Net::HTTP::Get.new(uri)
      # 環境変数には会員登録時に発行されるIDとパスワードを設定する
      req.basic_auth(ENV['ID'], ENV['PASSWORD'])
      prefecture = http.request(req)
      JSON.parse(prefecture.body.encode('SJIS', 'UTF-8', invalid: :replace, undef: :replace, replace: '')&.encode('UTF-8'), symbolize_names: true)
    end
  end

models/prefecture.rbを実装する

  • prefecture.rbを以下のように実装する
app/models/prefecture.rb
 class Prefecture < ActiveHash::Base
  #-----------------------
  #  Active_hash 
  #-----------------------
   self.data = [
    {id: 1, name: '北海道'}, {id: 2, name: '青森県'}, {id: 3, name: '岩手県'},
    {id: 4, name: '宮城県'}, {id: 5, name: '秋田県'}, {id: 6, name: '山形県'},
    {id: 7, name: '福島県'}, {id: 8, name: '茨城県'}, {id: 9, name: '栃木県'},
    {id: 10, name: '群馬県'}, {id: 11, name: '埼玉県'}, {id: 12, name: '千葉県'},
    {id: 13, name: '東京都'}, {id: 14, name: '神奈川県'}, {id: 15, name: '新潟県'},
    {id: 16, name: '富山県'}, {id: 17, name: '石川県'}, {id: 18, name: '福井県'},
    {id: 19, name: '山梨県'}, {id: 20, name: '長野県'}, {id: 21, name: '岐阜県'},
    {id: 22, name: '静岡県'}, {id: 23, name: '愛知県'}, {id: 24, name: '三重県'},
    {id: 25, name: '滋賀県'}, {id: 26, name: '京都府'}, {id: 27, name: '大阪府'},
    {id: 28, name: '兵庫県'}, {id: 29, name: '奈良県'}, {id: 30, name: '和歌山県'},
    {id: 31, name: '鳥取県'}, {id: 32, name: '島根県'}, {id: 33, name: '岡山県'},
    {id: 34, name: '広島県'}, {id: 35, name: '山口県'}, {id: 36, name: '徳島県'},
    {id: 37, name: '香川県'}, {id: 38, name: '愛媛県'}, {id: 39, name: '高知県'},
    {id: 40, name: '福岡県'}, {id: 41, name: '佐賀県'}, {id: 42, name: '長崎県'},
    {id: 43, name: '熊本県'}, {id: 44, name: '大分県'}, {id: 45, name: '宮崎県'},
    {id: 46, name: '鹿児島県'}, {id: 47, name: '沖縄県'}
   ]
   #-----------------------
   #  Scope
   #-----------------------
   scope :get_prefecture_name, ->(prefecture_name) { find_by(name: prefecture_name) }
end

controllers/article_controller.rbを実装する

  • article_controller.rbを以下のように実装する
app/controllers/article_controller.rb
class ArticleController < ApplicationController
  include GeoipModule

  def create
      @articles = Article.new(article_params)
      begin
        remote_ip = request.env["HTTP_X_FORWARDED_FOR"]&.to_s&.split(', ')&.first || request.remote_ip
        subdivisions = get_to_geoip(remote_ip)&.dig(:subdivisions)
        prefecture_name = subdivisions.present? ? subdivisions[0].dig(:names, :ja) : nil
        @articles.get_prefecture_id = Prefecture.get_prefecture_name(prefecture_name)&.id
      rescue => e
        Rails.logger.error(e)
        Rails.logger.error(e.backtrace.join("\n"))
      end
      
      if @articles.save
        redirect_to home_index_path
      else
        render "new"
      end
   end
end

controllers/article_controller.rbの解説

※今回remote_ipの中には東京都のIPアドレスが入っていることを想定しています。
まずget_to_geoip(remote_ip)でGeoIPにリクエストを投げるとレスポンスとして以下のような値が返ってきます。

{:city=>{:geoname_id=>1850147, :names=>{:"zh-CN"=>"京", :de=>"Tokio", :en=>"Tokyo", :es=>"Tokio", :fr=>"Tokyo", :ja=>"東京", :"pt-BR"=>"Tquio", :ru=>"Токио"}}, 
:continent=>{:code=>"AS", :geoname_id=>6255147, :names=>{:"zh-CN"=>"洲", :de=>"Asien", :en=>"Asia", :es=>"Asia", :fr=>"Asie", :ja=>"アジア", :"pt-BR"=>"sia", :ru=>"Азия"}}, 
:country=>{:iso_code=>"JP", :geoname_id=>1861060, :names=>{:fr=>"Japon", :ja=>"日本", :"pt-BR"=>"Japo", :ru=>"Япония", :"zh-CN"=>"日本", :de=>"Japan", :en=>"Japan", :es=>"Japn"}}, 
:location=>{:accuracy_radius=>50, :latitude=>35.6893, :longitude=>139.6899, :time_zone=>"Asia/Tokyo"}, :postal=>{:code=>"102-0082"},
:registered_country=>{:iso_code=>"JP", :geoname_id=>1861060, :names=>{:en=>"Japan", :es=>"Japn", :fr=>"Japon", :ja=>"日本", :"pt-BR"=>"Japo", :ru=>"Япония", :"zh-CN"=>"日本", :de=>"Japan"}},
:subdivisions=>[{:iso_code=>"13", :geoname_id=>1850144, :names=>{:ja=>"東京都", :en=>"Tokyo", :fr=>"Prfecture de Tokyo"}}]}

今回は、都市名"東京都"を取得したいのでget_to_geoip(remote_ip)&.dig(:subdivisions)で取得した値をsubdivisions変数の中に格納しています。
変数の中身は以下の通りになっているかと思います。

subdivisions = [{:iso_code=>"13", :geoname_id=>1850144, :names=>{:en=>"Tokyo", :fr=>"Prfecture de Tokyo", :ja=>"東京都"}}]

次にprefecture_name変数にsubdivisions.present?がtrueの場合は都市名を
falseの場合にはnil返す処理をしています。

 # trueの場合
prefecture_name = subdivisions.present? ? subdivisions[0].dig(:names, :ja) : nil
→ "東京都"

 # falseの場合
 prefecture_name = subdivisions.present? ? subdivisions[0].dig(:names, :ja) : nil
→ nil

都市名を取得できたらprefecture.rbで定義したスコープget_prefecture_nameの引数にprefecture_nameを渡せば...

 # 今回はidが欲しいのでidを取得
Prefecture.get_prefecture_by_name(prefecture_name)&.id
→ 13

あとは@articles.saveで無事保存されていることが確認できたら完成です!

request.env["HTTP_X_FORWARDED_FOR"]について

  • Ruby on Railsではwebサーバーの種類によりますが以下のコードIPの取得ができます
request.env["HTTP_X_FORWARDED_FOR"] || request.remote_ip
  • しかしrequest.env["HTTP_X_FORWARDED_FOR"]ではカンマ区切りで複数のIPが返ってきてしまうので以下の形に書き換えることを推奨します
- request.env["HTTP_X_FORWARDED_FOR"] || request.remote_ip
+ request.env["HTTP_X_FORWARDED_FOR"]&.to_s&.split(', ')&.first || request.remote_ip

また今回のコードではnilが返ってくる可能性が高いのでぼっち演算子を使用しています。
メソッド名が存在しない場合はNoMethodErrorが出力されてしまうので注意が必要です。

最後に

不明な点などありましたらお気軽にコメントよろしくお願いします🤲
Twitterのフォローもよろしくお願いします! → https://twitter.com/te_obata

Discussion