rubyでslackの天気botを作った
投稿間隔が空いてしまいましたが、slackのbotを作ったのでその事について描こうと思います。
汚いコードはこちら
環境
利用したサービス
お天気Webサービス仕様 - Weather Hacks - livedoor 天気情報weather.livedoor.com
やったこと
slackでチャンネルにボットを置いて、天気
と都道府県名
が同時に投稿されたとき、該当する都道府県の天気をAPIで取得して、整形した文字列をslackのbotが投稿する。
使ったサービスの紹介+α程度の記事なので、詳しくは作ったbotのコードをご覧ください。
内容
天気APIの取得と解析
今回使ったWeather Hacks
というAPIは、今日明日の天気をJSON形式で提供しているサービスです。認証も必要なく、URLはベースのpathと末尾の都市IDだけ、HTTPリクエストはgetのみと、わりと簡単に情報を得ることができます。
#群馬県前橋市のJSONを取得する例 require "uri" require 'net/http' require "json" MAEBASHI = "http://weather.livedoor.com/forecast/webservice/json/v1?city=100010" #URLをパースしてURIインスタンスを返す uri = URI.parse(MAEBASHI) #引数のURLへのgetリクエストのレスポンスを文字列で返す(引数はURIインスタンスのみ) res = Net::HTTP.get(uri) #文字列をJSON形式でパースして、ハッシュ(と配列)で返す weather = Json.parse(res) #仕様に書いてあるプロパティ名がハッシュのキーとなる #ハッシュの途中で今日か明日か選択する配列が出てくるので注意 #例 weather["forecasts"][0]["date"] #=>"2019-12-20" weather["forecasts"][0]["telop"] #=>"晴のち曇"
加えて都市IDと都道府県名でCSVを作成して、全ての都道府県に対応する処理を作ってみました。
... BASE = 'http://weather.livedoor.com/forecast/webservice/json/v1?city='.freeze ... # 都道府県に応じたAPIのURLを作って配列で返す def generate(text) hash.each_with_object([]) {|(k, v), ary| ary << (BASE + v) if text.include? k.chop } end # csvを読み込んで{'都道府県' => '都道府県ID'}ハッシュで返す def hash @hash ||= CSV.read(listfile).each_with_object({}) {|ary, h| h[ary[0]] = ary[1] } end # 絶対パス生成 def listfile @listfile ||= File.expand_path("api_list.csv", __dir__) end end ...
上のコードは文字列に都道府県名が含まれるなら、その都道府県IDとベースのURLを連結したURLを配列で返します。slackで投稿された文字列を引数にします。
slackAPI
slackのAPIは難しく、まだ理解できていない状態です。
RTM API
とEvents API
があり、今回使用したのはRTM API
です。
このRTM API
はWebSocket
を用いてリアルタイム通信を行いますが、WebSocket
や非同期処理で用いるeventmachine
は全く理解ができていないので、理解できる範囲で紹介しようと思います。
slack APIを使うには新しいアプリを作る必要があります。
- slack apiの
Create New App
からアプリを作成します。 - 作ったAppの
Bot User
でbotを作成します。 OAuth & Permissions
のInstall App to Workspace
でワークスペースにアプリをインストールします。- botが追加されたワークスペース内で、任意のチャンネルにbotを追加します
これでOAuth & Permissions
にAPIを使える様になるBot User OAuth Access Token
トークンが取得できます。
トークンはコードに平文で置かずに、環境変数等に入れて外部に漏れないようにしてください。平文に置いて、手違いでgithubにpushしたらすぐにトークンが無効となりました。
以下がWebSocketでリアルタイム通信を行う手順です。
#トークンを環境変数に入れておく(ここではSLACK_API_TOKEN) require 'http' require 'json' require 'eventmachine' require 'faye/websocket' RTM_API = "https://slack.com/api/rtm.start" #RTM APIのURLにトークンをつけてpostリクエストを送る #戻り値はHTTP::Responseインスタンスを返す response = HTTP.post(RTM_API, params: {token: ENV['SLACK_API_TOKEN']}) #HTTP::ResponseインスタンスのbodyのJSONをパースする rc = JSON.parse(response.body) #JSONからWebSocketで用いるwss通信のアドレスを得る。 url = rc['url'] #非同期処理を行うためにeventmachineの起動 EM.run do # Web Socketインスタンスの立ち上げ ws = Faye::WebSocket::Client.new(url) #onメソッドは引数に一致するイベントによって呼ばれる。 #ブロックパラメータはレスポンスのJSONを含むFaye::WebSocket::API::**インスタンスが代入される # 接続が確立した時の処理 ws.on :open do #処理 end # RTM APIから情報を受け取った時の処理 ws.on :message do |event| #レスポンスのJSONを得る data = JSON.parse(event.data) #チャンネルに投稿する ws.send({type: 'message',text: "メッセージ",channel: data['channel']}.to_json) #等の処理 end # 接続が切断した時の処理 ws.on :close do EM.stop #eventmachineの終了等の処理 end end
参考
Rakeタスクをレビューしてもらった
前回の記事でまとめた情報を元にRakeタスクを作りました。
起動すると、はてなブックマークのランキングをスクレイピングし、タイトルを出力し、記事を選択してブラウザで開くという物です
これに対し
というissueをもらったので
それを踏まえて書き直したのが、こちら
これをビデオチャットでレビューしてもらいました。 個人的にはそこそこできたと思っていたのですが、かなり深刻な問題点を指摘・修正してもらいました。
レビューまとめ
実際はもっと細かく指摘されましたが、自分なりにまとめてみました。振り返られるのがpull requestのコードと記憶のみなので無意識に素通りしている所はあるかも知れません。
クラスの区分を考える
私が作ったタスクはclassがRankingScraping
しかなく、スクレイピング・タイトルとURL格納・タイトル出力・記事の選択とブラウザ移行が一つのクラスに記述されていました。
このクラス名はScraper
の様な名詞に、行う処理はスクレイピングと出力のみにし、要素(タイトル・url)の格納とコレクション等は別のクラスを作った方が良いです。
紐付いた値はまとめて管理する
タイトルはtitle_ary
、URLはurl_ary
という配列に格納していましたが、これらは本来セットとなる要素です。取り回し辛い上に、そもそもRubyは配列の順番を保証していません。
これらには新しいクラスを作り、そのインスタンスのインスタンス変数にタイトルとURLとランキングを格納という方法が良いです。
これらのコレクションクラスも必要です。
固定の値はクラス内部に隠す
今回はスクレイピングしたいURLやxpathやcssセレクタを外部からもらう様なことはせず常に固定です。にも関わらず、固定のURLやxpathを変数としてRakefike内に記述していました。これらは値を扱うクラスに定数として置くのが好ましいです。
メソッド名は簡潔に
get_elementやchoice_articleなど冗長なメソッド名が多いです。elementやselectなど可能なら一語で書くのがrubyの慣習らしいです。rubyの慣習にそったコーディングか判断してくれるRubocop
というgemもあるので今度使ってみようと思います。
例外処理は外側に書く
クラスのメソッド内で例外処理を書いていました。結果的に握り潰してしまうことになるので、実行ファイルであるRakefileに記述しました。また、例外処理でプログラムの実行を終了する場合のexit
の戻り値はエラーを意味する1
にします。エラーでない場合は0
ですが、それがデフォルトなので引数なしでも正常終了扱いとなります。
既存の機能を活用する
先ほど言及したコレクションクラスを定義する時にはEnumerable
を使います。
Enumerable
はmix-inしeachを定義するだけでコレクションクラスが簡単に作れる、Arrayクラスもincludeしている繰り返しの汎用モジュールです。rubyはこういった便利な汎用モジュールがあるのでクラス作成にそれを使わない手はないです。
感想
基本的なプログラミングと設計の力がないこと、クラスを使ってオブジェクト指向的にプログラミングする難しさを再認識しました。クラスに関しては数をこなさないと身につかないと言っていたので、これからはもっとコードを書いて少しづつでも身に付けられたらと思っています。
nokogiriでスクレイピング
今回はいつものドキュメントの焼き直しみたいなのではなく、実際に作ってみたことを書いてみようと思います。
目標
はてなブックマークのテクノロジー部門ランキングをスクレイピングし、タイトルとURLを拾う
html取得
まずhtmlを取得してみます。
require 'nokogiri' #スクレイピングのライブラリ require 'open-uri' #urlを開くライブラリ charset = nil #htmlと文字コードを取得 html = open(url) do |f| charset = f.charset f.read end #取得したhtmlを操作しやすいNokogiriオブジェクトにパース doc = Nokogiri::HTML.parse(html, nil, charset) #=> 出力結果は量が多いためカット
取得し、パースもできました。
しかし、目的のタグのtitle
やhref
(URL)といった属性を抽出する方法がわかりません。
NokogiriオブジェクトはXpath
やcssセレクタ
で要素を指定して抽出できるそうなのでその要素を探します。
chrome で解析
スクレイピングを行うために、Google Chromeデベロッパーツールを使い、抽出の際目印になりそうな要素を探していきます。
chrome上でF12
若しくは⌘+option+i
で起動します。
左のマウスカーソルのようなアイコンをクリックすると、マウスカーソルの下にあるテキストや画像がhtmlのどの部分に該当するかわかります。 タイトルとURLが抽出できそうなclassが見つかりました。
スクレイピング
html取得の項目のコードの続き
#entrylist-contents-titleというclassを持つh3タグの中のaタグという意味 x = '//h3 [@class="entrylist-contents-title"]/a' c = 'h3.entrylist-contents-title a'
これを用いて抽出したaタグを一つずつ処理します。
#Xpath doc.xpath(x).each do |node| puts node[:title] puts node[:href] end #cssセレクタ doc.css(c).each do |node| puts node[:title] puts node[:href] end #出力は同じ。繰り返しの1番目のみ。 #=>韓国トップの囲碁棋士引退「努力してもAIには勝てない」 | NHKニュース #=>https://www3.nhk.or.jp/news/html/20191129/k10012195501000.html
Xpath
とcssセレクタ
はとてもはまりました。
調べてもクリティカルな情報が見つからず、他のブログのサンプルコード結果から予測して試行錯誤しながらXpath
やcssセレクタ
を書いていました。
特にハッシュのように要素を抽出する方法に巡り合えなくて、代替案として文字列に変換して正規表現で抽出するコードを書いたりもしました。
同値性と同一性
Rubyでは等しいか比較をする時
基本的にレシーバとなるオブジェクトのクラスにオーバーライド(再定義)された
Object#== Object#=== Object#equal?
を用います。
それぞれ等しいならtrue
、異なるならfalse
となる点は同じですが、判断基準が異なります。
==
==
は同値性を判断しています。
a = 'abc' b = 'abc' a == b #=>true
同値性とは文字の通り、値が等しいとtrue
となります。
上記は値は等しいですが、別のオブジェクトです
a.object_id #=>70337627390140 b.object_id #=>70337627471920
また、Integer
クラス(整数)やFloat
クラス(浮動小数点数)等の数値、シンボルは値が等しいと、同じオブジェクトとなります。
int = 1 int2 = 1 int.object_id #=> 3 int2.object_id #=> 3
Object#equal?
Object#equal?
は同一性を判断しています。
==
の例の文字列も同一性はありません
a.equal? b #=>false
同一のオブジェクトを参照している時のみtrue
となります。
c = a
a.equal? c #=>true
rubyにおいて複製などをする際、オブジェクトの要素はコピーせずにそのまま参照することがあります。
ary = %w[1 2 3] #=>["1", "2", "3"] #cloneはレシーバのオブジェクトのコピーを出力 ary2 = ary.clone ary[0].equal? ary2[0] #=> true
ary3 = %w[a b] #=> ["a", "b"] #Array#*は要素を引数の数だけ繰り返す re = ary3 * 2 #=> ["a", "b", "a", "b"] re[0].equal? re[2] #=>true ary3[0].equal? re[0] #=>true
同じオブジェクトを破壊的メソッドを使用した時、参照している全てに影響してしまいます。
#上の続き ary[0] #=>"1" #succは次の数字や文字を出力。succ!は破壊的メソッド ary[0].succ! #=>"2" ary #=>["2", "2", "3"] ary2 #=>["2", "2", "3"]
===
===
は基本的に同値性で判断しますが、case文のバックで用いられるため、クラスによって==
とは異なる内容の再定義がされている場合があります。
等式は可換(左右を入れ替えても結果が同じ)ですが、上記の理由で===
では非可換となる場合があります。
/[a-z]+/ === 'string' #=>true 'string' === /[a-z]+/ #=>false
これはRegexp
クラス(正規表現)ではRegexp#===
として正規表現にマッチすればtrue
となる再定義されている。しかしString
クラスはString#==
と同じ再定義しているため可換性がなくなっています。
参考
論理演算子のショートサーキット
&&, ||, and, or, not, !
&&
とand
は二項とも真なら真の論理積
、
||
とor
は二項の片方が真なら真の論理和
と呼ばれます。 not
,!
の論理否定は今回は扱いません。
and
とor
は&&
と||
と比べて処理の優先度が低いという違いがあります。
#Rubyではfalse,nil以外のオブジェクトが全てtrue(真)と扱われる 0 && 1 #=> 1 0 || 1 #=> 0 false && true #=> false #(0 && 1) or 2 と同義 0 && 1 or 2 #=> 1
なぜ以上のような戻り値になるかというと、
1.最後に評価された値が戻り値になる
2.評価が確定したら右辺を評価しない、ショートサーキットという評価法を用いている
この二点によるものです。
ショートサーキットについて具体例を示すと
&&
の場合は
false && true #=>false
左辺のfalse
によって真になることはないため、右辺は評価せずfalse
が最後に評価された値となります。
||
の場合は
true || false #=> true
左辺のtrue
で真が確定するため、右辺は評価せずtrue
が最後に評価された値となります。
制御フロー
論理演算子はこの性質を利用して制御フローに使用することもできます。
#Aが真ならBを実行する A && B #Aが偽ならBを実行する A || B
例(send_emailというメソッドKernelには存在しません)
#ユーザーが存在するならメールを送る @user && send_mail(@user) #ユーザーが存在しないなら処理を抜ける @user || return(@user)
左辺がnil
,false
なら右辺を代入する自己代入演算子の||=
も、この性質を利用したものです。
上の式は下の式のシンタックスシュガーとなります。
a ||= 1 a = (a || 1)
括弧がついている理由は、代入=
より優先度の低い演算子の後に代入を行うためです。
演算子メソッド
以下のような演算子は他の言語でも同じような結果になると思います。
10 + 10 #=> 20 10 * 2 #=> 20
ではこれはどうでしょうか
'dog' + 'cat' #=> "dogcat" 'dog' * 3 #=> "dogdogdog"
上記のように同じ演算子なのに振る舞いが異なるのは、それぞれの演算子がInteger
クラス・String
クラスのインスタンスメソッドのためです。
再定義が可能な演算子全てがメソッドで実装されています。
上記の構文は以下のようなメソッドのシンタックスシュガー(同じ動きでわかりやすい構文)です。
#違いをわかりやすくするために()を用いています 10.+(10) #=> 20 10.*(2) #=> 20 'dog'.+('cat') #=> "dogcat" 'dog'.*(3) #=> "dogdogdog"
演算子メソッドを再定義は以下のようにします。
#二項演算子 def 演算子(引数) #オブジェクト + 引数(+なら) #処理 end #単項演算子 def 演算子@ #+オブジェクト(+なら) #処理 end
Rubyはオープンクラスなので、組み込みのライブラリにメソッドの追加や変更が簡単に行えます。
既存のクラスを再定義するだけで、同名の部分は変更し、そうでない部分は追加されます。
例
#Stringクラスに-メソッドは存在しません class String def -(string) unless string.class == String return self end self.gsub(string, '') end end #完全一致した文字列を取り除く 'string' - 'ring' #=>"st"
参考
attr_accessorとは
初めまして、zooと申します。
約2ヶ月前にRubyの勉強を始めた初学者ですが、週一回を目標にRubyを中心にアウトプットしていきたいと思います。
今回はattr_accessorについてです。
Module#attr_accessor
Rubyには生成したインスタンスから直接インスタンス変数を読み書きできません。
そんなprivateなインスタンス変数の読み書きするゲッターメソッド・セッターメソッドを自動で定義してくれるメソッドです。定義をするだけなので戻り値はnil
となります
定義は、インスタンス変数名と同名のシンボルを引数とします。複数の引数がある場合はカンマで繋げます。
基本はクラス直下ですが、メソッド内でも一回呼び出せばどこで定義しても反映されます。
クラス直下ではレシーバを明示的に書かなくても自動的にクラス自身になりますが、メソッド内では明示的にレシーバを書かないとインスタンス自身をレシーバにしてしまうので注意してください。
attr_accessor :インスタンス変数名 def メソッド名 クラス名.attr_accessor :インスタンス変数名 end
具体的な使用方法
class User attr_accessor :name def initialize(name) @name = name end user = User.new('dog') user.name #=> "dog" user.name = 'cat' user.name #=> "cat"
ゲッターメソッドのみを定義するattr_reader
セッターメソッドのみを定義するattr_writer
も同じく存在します
また、attr_accessor
は以下と同義です。
class User def initialize(name) @name =name end #ゲッターメソッド #Rubyのメソッドの戻り値は最後に評価された値なので、@nameが戻り値となります def name @name end #セッターメソッド #Rubyでは=を最後につけてメソッドを定義すると、変数の代入のようにメソッドを呼び出せます。変わるのは見た目だけです。 def name=(value) @name = value end end #結果は同じ user = User.new('dog') user.name #=> "dog" user.name = 'cat' user.name #=> "cat"
Moduleクラスのサブクラスの、Classクラスのオブジェクトである新規に作成する全てのクラスがこのメソッドを使えます。
... User.class #=>Class
参考