Edited at

スクレイピングする Hubot スクリプトをつくろう

More than 3 years have passed since last update.

これは 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 のスクレイピングでの定番パッケージである requestcheerio を使いましょう。以下のコマンドで 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 もきちんとソートしてくれます。

さて、書いていきましょう。では、こちらに完成したものがございます。


src/adventar.coffee

# 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 を待ち受ける話でもしましょうかね。

では。