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

【ごみ収集曜日APIシリーズ】Gemに渡す町コードを一括保存するバッチを作成した

先日、以下の記事で紹介した、ごみ収集曜日を取得する処理をGemへ渡すためのコードを取得するバッチを作成しました。

takapi86.hatenablog.com

なぜ作成したか。

先日書いた記事でも紹介しましたが、このGemでごみ収集曜日を取得するためには以下のパラメータとして以下のコードを渡す必要があります。

しかし、上記コードを見つけるためには、実際に横浜市のHPにアクセスし、URLを確認してパラメータを設定しなければなりません。 それだと、そもそも横浜市のHPにアクセスしているので「普通にで確認すれば良いじゃん」になるので、一括で上記コードを取得し、DBに保存するバッチを作成しました。

ページの構造

ごみ収集曜日のページ(横浜市資源循環局)は以下のような階層構造になっています。

収集曜日ページのトップです。区を選択することができます。 区の名前をクリックすると、その区の町名の頭文字を選択する画面に遷移します。

  • 頭文字の選択ページ

例:港北区(区コード:16) http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16

町(収集場所)の頭文字を選択する画面です。以下のような構成で各行ごとにリンクが貼られています。

[1] あ~お
[2] か~こ
[3] さ~そ
[4] た~と
[5] な~の
[6] は~ほ
[7] ま~も
[8] やゆよ
[9] ら~ろ、わ
  • 町名の選択ページ

例:港北区(区コード:16) さ行(頭文字コード:3) http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3

選択した区の選択した行(あかさたな)に該当する町名一覧が表示されます。

港北区のさ行では、以下の一覧が表示されます。

町名にリンクが貼られており、クリックすると、該当の町の収集曜日が表示されます。

篠原北1丁目
篠原北2丁目
篠原台町
篠原町
篠原西町
篠原東町1丁目
篠原東町2・3丁目
下田町1・2・3丁目
下田町4・5・6丁目
新横浜1・2・3丁目
新吉田町
新吉田東1~6丁目
新吉田東7・8丁目
  • ごみ・資源物の収集日ページ

ここで、ごみ・資源物の収集日を確認することができます。

例:港北区(区コード:16) さ行(頭文字コード:3) 町コード 1073 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3/1073

Gemは、このURLを見てページからごみ収集曜日を取得します。

バッチの取得対象

先述した通り、

がわかれば、ごみ収集曜日を取得することができるので、 区から町名を選択する画面まで、リンクに含まれる各コードと名称を取得しながら再帰的にデータを取得していきます。

収集場所までバッチで持ってくれば良いのでは?とお思いの方もいらっしゃると思うのですが、 すべてのページのデータを持ってくるためには、 18区×あかたたな(9行)×町名分必要であり、横浜市のサーバへかなりのリクエストが発生してしまうため含めておりません。

あとは、もうGemつくちゃったしな・・・と、なんかすいません。といった感じです。

また、負荷については怒られないように、配慮していてバッチ内では5秒一回しかリクエストを飛ばさないようにしています。

作ってみた所感

今回は、基本となる機能は雑に一気に仕上げ、それから例外処理の追加やリファクタリングをしていくという流れで実装していきました。 ほぼ勢いで実装しましたが、丁寧に積み上げていく方式よりも自分の感覚的に合っているなと感じました。

というのは、処理を書く上で色々考えなければならないことがシンプルになるということ * Rubyっぽく書くには * クラスをどう分ける? * わかりやすい変数名は?

と、普段は実装する上で同時に考えることが多いので、まず一気に仕上げてから細かい部分を調整していくやり方にしてみて、こっちの方が性にあっているのでは?という気持ちになっていますす。(精神的にも疲れにくく、集中して取り組めたのではないかと感じました。)

すべてのケースには当てはまらないと思いますが、実装の仕方はこれからも試行錯誤しながらやっていきたいと思います。

【ごみ収集曜日APIシリーズ】ごみ収集曜日を取得する処理をGem化した

ごみ収集曜日を取得する処理だけを切り出し、Gem化したので、使い方をメモしておきます。

機能は随時更新していく予定です。

どのようなGemか?

横浜市資源循環局のページから、ごみ収集曜日の情報を取得しHashの配列で返します。

データ量の少ないモバイル版から取得しています。 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/

使い方

例えば、港北区篠原町の収集曜日は以下のURLです。 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3/1073

区、頭文字、町名にそれぞれコードが振られており、上記URLの場合は、

  • 区: 16
  • 頭文字: 3
  • 町名: 1073

になっています。

情報を取得するには、 Yokohama::Gomi::Schedule::App#fetch_schedule メソッドを叩く必要があり、 引数にそれぞれ区、頭文字、町名、 ward_id:,initial_code,town_id を指定します。

戻りは、取得した曜日(漢字)に対して、曜日に応じたごみの情報が入ったHashの配列(月〜土)で返ってきます。

作ってみて

Gemを作成するのは初めてでしたが、 bundle gem コマンドを使うことで簡単に作成することができました。 現在は区、頭文字、町名のコードをがわかっていないと使えないので、今後は文字列から各コードを参照するような機能などを追加していきたいと思います。

【ごみ収集曜日APIシリーズ】DatabaseRewinderを追加した

【ごみシリーズ】FactoryGirlを追加した - takapi86のブログ

の続きです。 前回は、テストを2回目実行しようとすると、ユニーク制約に引っかかりエラーになってしまう問題が残ってしまいました。 今回はその対応として、DatabaseRewinderというGemを使い、Rspecのテスト後に、 Createで作成したTestデータをデータベースから削除するようにしていきます。

github.com

上記のREADME.mdを参考にし導入しました。

導入

Gemfiledatabase_rewinder を追加して、bundle installします。

テスト環境のみで使用するので、 grouptest だけでよいでしょう。

group :test do
  gem 'database_rewinder'
end

rails_helper に以下の記述を追加します。

複数DBを扱うわけではないので、基本的な設定でいきます。

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseRewinder.clean_all
  end

  config.after(:each) do
    DatabaseRewinder.clean
  end
end

試してみる

データがない状態で確認をしたいので、 以下のコマンドで、DBをリセットしておきます。

RAILS_ENV=test bundle exec rake db:migrate:reset

実行してみます。

bundle exec rspec spec/requests/towns_spec.rb
..

Finished in 2.18 seconds (files took 3.28 seconds to load)
2 examples, 0 failures

bundle exec rspec spec/requests/towns_spec.rb
..

Finished in 1.43 seconds (files took 3.2 seconds to load)
2 examples, 0 failures

データを見てみます。

[1] pry(main)> Town.all
  Town Load (3.0ms)  SELECT "towns".* FROM "towns"
=> []
[2] pry(main)>

2回以上実行しても、正常に動作し、テスト後のデータもすべてクリアされていました。

バッと書いてしまいましたが、今回はこんなところで。

【ごみ収集曜日APIシリーズ】FactoryGirlを追加した

いつもの、ごみシリーズです。

今週土日は、自分の所属しているバドミントンクラブのHPの作成で終わってしまいました。 デザイン難しい。

さて、今回は前回 rspecでHTTPのリクエスト・レスポンスのテストを行った。 - takapi86のブログ で追加したテストに、テストデータを追加していきます。

こちらを参考に導入を行いました。

File: GETTING_STARTED — Documentation for factory_girl (4.8.0)

導入

  • Gemfilefactory_girl_railsを追加して、bundle installします。
group :test do
  gem 'factory_girl_rails'
end
  • factory_girlのテストデータを作成するコードを保管する場所として、spec/factories/ を作成しておきます。

ファイルを作成する

区モデル(ward)、町モデル(town)のFactoryGirlファイルを作成します。 ※ward、townは1:nの関係です。

詳しくは、

  • spec/factories/wards.rbを作成
FactoryGirl.define do
  factory :ward do # モデル名を指定
    sequence(:id)
    ward_code 16
    name '港北区'
  end
end
  • spec/factories/towns.rbを作成
FactoryGirl.define do
  factory :town do # モデル名を指定
    sequence(:id)
    ward_code 16
    initial_code 3
    town_code 1073
    name '篠原町'
  end
end

試してみる

  • test環境にコンソールで入ります。
RAILS_ENV=test bundle exec rails c

wardのデータを作ってみる

[1] pry(main)> ward = FactoryGirl.create(:ward)
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "wards" ("id", "ward_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["id", 1], ["ward_code", 16], ["name", "港北区"], ["created_at", 2017-03-26 14:57:30 UTC], ["updated_at", 2017-03-26 14:57:30 UTC]]
   (145.8ms)  commit transaction
=> #<Ward:0x00559a99c3ccb8
 id: 1,
 ward_code: 16,
 name: "港北区",
 created_at: Sun, 26 Mar 2017 14:57:30 UTC +00:00,
 updated_at: Sun, 26 Mar 2017 14:57:30 UTC +00:00>

townのデータを作ってみる

[2] pry(main)> FactoryGirl.create(:town, ward: ward)
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "towns" ("id", "ward_code", "initial_code", "town_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["id", 1], ["ward_code", 2], ["initial_code", 3], ["town_code", 1073], ["name", "篠原町"], ["created_at", 2017-03-26 14:59:00 UTC], ["updated_at", 2017-03-26 14:59:00 UTC]]
   (60.9ms)  commit transaction
=> #<Town:0x00559a9daa47f8
 id: 1,
 ward_code: 2,
 initial_code: 3,
 town_code: 1073,
 name: "篠原町",
 created_at: Sun, 26 Mar 2017 14:59:00 UTC +00:00,
 updated_at: Sun, 26 Mar 2017 14:59:00 UTC +00:00>

うまくいったっぽいです。 念の為、testのDBはクリアしておきます。

RAILS_ENV=test bundle exec rake db:migrate:reset

前回のテストに追加

require 'rails_helper'

RSpec.describe TownsController, type: :request, json: true do
  let(:town) { FactoryGirl.create(:town, ward: create(:ward)) }

  describe 'GET /towns/:name.json' do
    before do
      get URI.escape("/towns/#{town.name}.json")
    end

    it '200 OK を返す' do
      expect(response.status).to eq(200)
    end
  end
end

こちらもうまくいったようです。

ちなみに、ここではFactoryGirl.createと記載していますが、クラス名は、 spec_helper.rbの Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } のコメントを外し、以下のようにファイルを追加するか、そのままconfig.include FactoryGirl::Syntax::Methods と追記することで、省略できます。

# spec/support/factory_girl.rb
RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end

ただ、このままでは2回目実行すると、以下のようなエラーが発生してしまうようです。

ActiveRecord::RecordNotUnique:
       SQLite3::ConstraintException: UNIQUE constraint failed: wards.id: INSERT INTO "wards" ("id", "ward_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)
     # ./spec/requests/towns_spec.rb:2:in `block (2 levels) in <top (required)>'
     # ./spec/requests/towns_spec.rb:7:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # SQLite3::ConstraintException:
     #   UNIQUE constraint failed: wards.id
     #   ./spec/requests/towns_spec.rb:2:in `block (2 levels) in <top (required)>'

どうやら、続けてテストする場合は、テスト後にテストしたデータを削除する処理を入れる必要があるようです。 こちらは、次回対応していきたいと思います。

【ごみ収集曜日APIシリーズ】rspecでHTTPのリクエスト・レスポンスのテストを行った。

rspecでHTTPのリクエスト・レスポンスのテストを行った。

今回も、ごみシリーズです。

ごみ収集日の検索をLineからできるようにした。 - takapi86のブログで、APIを作っていく中で、rspecでのテストでHTTPのリクエスト・レスポンスのテストができるということを知ったので、メモして起きます。

事前準備

  • 以下のgemはインストール済みである前提で行います。

対象

  • ごみ収集日の検索をLineからできるようにした。にも書いてありますが、ゴミ収集APIには以下のエンドポイントがあります。
    • 収集場所コードから収集場所のスケジュールを返すAPI
    • 収集場所名を前方一致で検索し、一覧で返すAPI
    • 上記2つのAPIを使い、Lineでメッセージを受け取り、スケジュールや収集場所一覧を返すAPI

今回は上記のうち、収集場所名を前方一致で検索し、一覧で返すAPIのテストを書きたいと思います。

導入

rspecのテストファイルは以下の場所 * spec/requests/towns_spec.rb

  • 今回はTownsControllerのテストで、リクエストを投げて確認するため、以下のように記述します。
describe TownsController, type: :request do
end
  • エンドポイントはGETメソッドで、/towns/:nameなので以下のように記述します。
describe TownsController, type: :request do
  describe 'GET /towns/:name' do
  end
end
  • ステータスコードは200で返ってくるのを期待するテストを追加します。
  • ここでは省略していますが、事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。
describe TownsController, type: :request do
  describe 'GET /towns/:name' do
    it '200 OK を返す' do
      get URI.escape("/towns/#{"日吉"}")
      expect(response.status).to eq(200)
    end
  end
end
  • 事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。
  • テストを実行してみましょう。
bundle exec rspec spec/requests/towns_spec.rb
Finished in 0.50264 seconds (files took 3.08 seconds to load)
1 example, 0 failures

正常にテストが終了したようです。

今回は、test環境にデータが入った状態でテストを行いましたが、 先ほど言ったように事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。

【ごみ収集曜日APIシリーズ】ごみ収集日の検索をLineからできるようにした。

前回のLINE Notifyを使ってごみ収集日を自分にお知らせしてみた。に続き、ごみシリーズです。

会社で携わっているサービスをゴリゴリAPI化していく予定(やっていき)なので、その練習も兼ねて作成しました。

今回は、Messaging API(LineBot)を使って、メッセージから横浜市のゴミ収集日を検索できるようにしました。

現在は、ざっくりイメージとして実装してみた感じなので、機能強化・リファクタリングは今後行っていく予定です。

こんな感じです。

収集場所名を前方検索し、もし、町名がドンピシャで一致していたら、収集曜日を出力。複数ある場合は「もしかして?」と収集場所名・コードを一覧出力します。

収集場所コードでも検索ができるので、収集場所名がわからない場合は、ざっくり町名などを入れて調べ→コードで検索といった流れで調べます。

環境は、

で実装しました。今回作成したAPIは以下3つ

  • 収集場所コードから収集場所のスケジュールを返すAPI
  • 収集場所名を前方一致で検索し、一覧で返すAPI
  • 上記2つのAPIを使い、Lineでメッセージを受け取り、スケジュールや収集場所一覧を返すAPI

です。Restfulな感じで作りました。

今回は、データについては横浜市のページを見て手動で作っていますが、 今後は、横浜市のHPからスクレイピングしてデータを取り込み、それを返すようにしていきたいと思います。

実は、今回APIを実装するのが初めてだったのですが、API設計や命名規則な部分で悩みました。

  • Rest APIのURLの付け方
  • APIでどんなリクエストを受けるか、どんなレスポンス・JSONを返すか
  • Lineメッセージのルール作り

実はまだまだ納得がいっておりませんので、今後、バージョンアップする際には、特にこの辺を意識して取り組んでいきたいと思います。

では、また。

【ごみ収集曜日APIシリーズ】LINE Notifyを使ってごみ収集日を自分にお知らせしてみた。

Rubyの練習も兼ねて、LINE Notifyを使ってごみ収集日を自分にお知らせしてみました。

やったこと

  • LINE Notifyのマイページへログイン(スマホ版のLINEで登録したものです。)し、トークンを発行
  • メッセージの送信テスト
  • Rubyからメッセージを送信
  • ごみ収集日のお知らせテキストを作成し、メッセージを飛ばす処理を追加
  • 上記をcrontabへ登録

参考

developers.linecorp.com

LINE Notifyのマイページへログイン(スマホ版のLINEで登録したものです。)し、トークンを発行

特に問題なく発行出来たので割愛

メッセージの送信テスト

参考ページ コマンドラインから LINE にメッセージを送れる LINE Notifyにもある通り、curlから簡単に試すことができた

curl -X POST -H 'Authorization: Bearer [access_token]' -F 'message=foobar' https://  
notify-api.line.me/api/notify

※ [access_token]の部分を発行されたトークンに置き換えます。([]は必要なし)

Rubyからリクエストを送信

以下のように実装してみた。

line_notify.rb

# coding: utf-8
require 'net/http'
require 'uri'
require 'openssl'

class LineNotify
  def self.send_msg(msg, token)

    if token.nil? || token.empty?
      puts 'Tokenをセットしてください。'
      return
    end

    uri = URI('https://notify-api.line.me/api/notify')
    req = Net::HTTP::Post.new(uri.path)
    req.set_form_data('message' => msg)
    req['Authorization'] = 'Bearer ' + token

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true if uri.scheme == 'https'
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE

    res = http.start do |h|
      h.request(req)
    end
  end
end

irbでテスト

irb(main):001:0> require './line_notify'
=> true
irb(main):002:0> LineNotify::send_msg("テストです。", "ここはトークン")
=> #<Net::HTTPOK 200 OK readbody=true>

送れたみたい。

f:id:takapi86:20170102223433p:plain

ごみ収集日のお知らせテキストを作成し、メッセージを飛ばす処理を追加

schedule.rb

# coding: utf-8
require "date"
require 'json'
require 'yaml'
require './line_notify'

config = YAML.load_file("config.yml")

youbi = [:日,:月,:火,:水,:木,:金,:土]
wday = youbi[Date.today.wday]

gomi_schedule = {
  月: '燃やすごみ,燃えないごみ、スプレー缶、乾電池',
  火: '缶・びん・ペットボトル',
  水: 'プラスチック製容器包装',
  木: '燃えないごみ',
}

if !gomi_schedule[wday].nil?
  msg = "\n今日は"+ wday.to_s + "曜日【" + gomi_schedule[wday] + "】の収集日です。"
  LineNotify::send_msg(msg, config['token'])
end

config.yml

token: [ここにトークンを追加]

上記をcrontabへ登録

crontab -e

7時に通知が来るよう設定

0 7 * * * cd ~/ && ruby schedule.rb

きたぞっ!

f:id:takapi86:20170102224548p:plain

明日からゴミの出し忘れは無くなるに違いない。