これは Hubot Advent Calendar 2014 の 9 日目の記事です。
昨日は @udzura さんの 『carrierモジュールで雑にHubotからコマンドを打つ』でした。stream を中心としたプログラミングは Node.js らしい良い形なのですが、ぼく個人としてはあんまり使えてないですね。あと @bouzuya Advent Calendar 的な煽りが見えた気もしますが、気のせいでしょう。
また、今回は @bouzuya の Hubot 連載の第 7 回です。目次は、第 1 回の記事にあるので、そちらをどうぞ。
前回まで、そして今回は
前回は 簡単な Hubot スクリプトをもっとつくろう (おみくじ系) ということで、Response#random
を使って、ランダムにひとつを選んで返す Hubot スクリプトをつくりました。
今回は外部の npm パッケージを使いながらスクレイピングする Hubot スクリプトをつくりましょう。npm パッケージを自由に使えるようになれば、できることがさらに広がります。
スクリプトの完成イメージ
まず、今回つくろうとしている Hubot スクリプトのイメージを共有しましょう。
今回は「スクレイピング」ということで、Hubot Advent Calendar 2014 でも利用している「 Adventar 」に HTTP リクエストをして、その結果を解析しましょう。
実行イメージはこんな感じです。
bouzuya> hubot adventar hubot
hubot> Hubot http://www.adventar.org/calendars/384
さっそくつくる
まずは Hubot スクリプトのテンプレートを generator-hubot
で生成します。
$ mkdir hubot-adventar
$ cd hubot-adventar
$ yo hubot:script
...
もう慣れたものだと思います。
次に Node.js のスクレイピングでの定番パッケージである request
と cheerio
を使いましょう。以下のコマンドで npm リポジトリからそれらのパッケージをダウンロードし、package.json
に依存関係を追加できます。
$ npm install --save request cheerio
request@2.49.0 node_modules/request
├── caseless@0.8.0
├── json-stringify-safe@5.0.0
├── forever-agent@0.5.2
├── aws-sign2@0.5.0
├── stringstream@0.0.4
├── tunnel-agent@0.4.0
├── oauth-sign@0.5.0
├── node-uuid@1.4.2
├── qs@2.3.3
├── mime-types@1.0.2
├── combined-stream@0.0.7 (delayed-stream@0.0.5)
├── form-data@0.1.4 (mime@1.2.11, async@0.9.0)
├── tough-cookie@0.12.1 (punycode@1.3.2)
├── http-signature@0.10.0 (assert-plus@0.1.2, asn1@0.1.11, ctype@0.5.2)
├── bl@0.9.3 (readable-stream@1.0.33)
└── hawk@1.1.1 (cryptiles@0.2.2, sntp@0.2.4, boom@0.4.2, hoek@0.9.1)
cheerio@0.18.0 node_modules/cheerio
├── entities@1.1.1
├── lodash@2.4.1
├── dom-serializer@0.0.1 (domelementtype@1.1.3)
├── htmlparser2@3.8.2 (domelementtype@1.1.3, domutils@1.5.0, entities@1.0.0, domhandler@2.3.0, readable-stream@1.1.13)
└── CSSselect@0.4.1 (domutils@1.4.3, CSSwhat@0.4.7)
簡単ですね。npm はこのように簡単にパッケージを追加できます。dependencies
もきちんとソートしてくれます。
さて、書いていきましょう。では、こちらに完成したものがございます。
# Description
# A Hubot script for listing advent calendars in Adventar
#
# Configuration:
# None
#
# Commands:
# hubot adventar <query> - lists advent calendars in Adventar
#
# Author:
# bouzuya <m@bouzuya.net>
cheerio = require 'cheerio'
request = require 'request'
module.exports = (robot) ->
robot.respond /adventar(?: (\S+))?/, (msg) ->
query = msg.match[1]
# send HTTP request
baseUrl = 'http://www.adventar.org'
request baseUrl + '/', (_, res) ->
# parse response body
$ = cheerio.load res.body
calendars = []
$('.mod-calendarList .mod-calendarList-title a').each ->
a = $ @
url = baseUrl + a.attr('href')
name = a.text()
calendars.push { url, name }
# filter calendars
filtered = calendars.filter (c) ->
if query? then c.name.match(new RegExp(query, 'i')) else true
# format calendars
message = filtered
.map (c) ->
"#{c.name} #{c.url}"
.join '\n'
msg.send message
解説
ええと、解説します。
require
cheerio = require 'cheerio'
request = require 'request'
cheerio
/ request
という変数にそれぞれ npm パッケージを読み込んでいます。使いかたの詳細はそれぞれのリポジトリを見てください。
(?: (\S+))?
robot.respond /adventar(?: (\S+))?/, (msg) ->
Hubot スクリプトに限った話じゃないけど、個人的にこの (?: (\S+))?
や (?:\s+(\S+))?
のような正規表現をよく使います。(?:)
はマッチしても取り出しの際のグループには追加されません。
request
# send HTTP request
baseUrl = 'http://www.adventar.org'
request baseUrl + '/', (_, res) ->
素朴に HTTP リクエスト + コールバックの設定。Node.js での基本形です。今回は手抜きのためにコールバックの第一引数の err
を無視しています。
cheerio
# parse response body
$ = cheerio.load res.body
calendars = []
$('.mod-calendarList .mod-calendarList-title a').each ->
a = $ @
url = baseUrl + a.attr('href')
name = a.text()
calendars.push { url, name }
cheerio
を使うと jQuery のようなセレクタなどが使える。ぼくは基本的には上記のように情報を抜き出したらつめかえて、それ以降は cheerio
を使わない主義です。cheerio
でメソッドチェインするとわかりづらいので。
あとは特にないかな。普通の JavaScript な動きだし。いつもなら面倒なので全部チェインにしているけど、コメントいれたほうがわかりやすいかと思って、区切ってみました。
動かしてみる
じゃあ、動かしてみましょう。
$ HUBOT_SHELL_USER_NAME='bouzuya' PATH="./node_modules/hubot/node_modules/.bin:$PATH" $(npm bin)/hubot -a shell -n hubot -r src
hubot> hubot adventar hubot
hubot> Hubot http://www.adventar.org/calendars/384
hubot> hubot adventar
...
いろふ http://www.adventar.org/calendars/331
蒙古タンメン中本 http://www.adventar.org/calendars/330
ラーメン http://www.adventar.org/calendars/329
プリキュア http://www.adventar.org/calendars/328
しょぼちむ http://www.adventar.org/calendars/327
hubot>
よいのでは
まとめ
スクレイピングをする Hubot スクリプトをつくってみました。そんなに細かく説明していませんが、たぶん大丈夫でしょう。もうぼくの知っていることはほぼ書いたと言って良いですね。
ちなみに今回のサンプルは bouzuya/hubot-adventar にあります。うまく動かない、などがあれば参考にどうぞ。
最後に
こういう場面で紹介するのにちょうど良い API って何なのでしょう。できればトークンなどが不要で、それなりに知名度があって、可能なら画像を返してくれると Slack などで見ると非常に良いのですが。
次回どうしましょうか。Hubot で HTTP を待ち受ける話でもしましょうかね。
では。