前編でプレビューが表示されずに絶望しましたが、
仕方がないのでスクレイピングというものに挑戦します。
とはいってもだいたい落ちてるコードの切り貼りですが。
長くなりましたが8割は同じコードを載せているためです。
##cheerioのインストール
素のHTMLではとても切り出す気力がわかないのでセレクタっぽく扱えるらしいcheerio
を入れます。cheerio-httpcli
と迷いましたが、初めてなので基本の方からで。
npm i cheerio
npm list
普段からcomposer
も使っていないのでインストールは何でもドキドキします。
npm ERR! extraneous: cheerio@0.18.0
エラーでました。 --save
が無くpackage.json
に追記されていないみたいです。
慣れたと思ってコピペをやめるとこれですね。
手動追加もいいですが、せっかく簡単にしてくれているので入れ直します。
npm uninstall cheerio
npm install cheerio --save
##セレクタでの取得
取得内容はユーザーノートより上としましょう。
開発者ツールで要素を調査し抜き出します。
require
を忘れてはいけない(1敗)
cheerio = require 'cheerio'
module.exports = (robot) ->
robot.hear /php man (.+)$/, (msg) ->
baseUrl = "http://php.net/manual-lookup.php?lang=ja&scope=quickref&pattern="
baseUrl += msg.match[1]
robot.http(baseUrl).get() (err, res, body) ->
if res.headers.location?
# 一致する結果があった
msg.send res.headers.location
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
msg.send $('div.book').text()
else
msg.send "のっと☆ふぁうんど!"
イケてないですね。改行などが無いのはもちろん、関数を表示させるつもりがブックを表示させているのですから。
array
なんて間際らしいワードにするんじゃなかったと今更後悔しています。
しかたないので、ちょっとセレクタとマークダウンの勉強をして整形します。
##マークダウンで書いてみたが……
cheerio = require 'cheerio'
module.exports = (robot) ->
robot.hear /php man (.+)$/, (msg) ->
message = ''
baseUrl = "http://php.net/manual/ja/"
searchUrl = "http://php.net/manual-lookup.php?lang=ja&scope=quickref&pattern="
searchUrl += msg.match[1]
robot.http(searchUrl).get() (err, res, body) ->
if res.headers.location?
# 一致する結果があった
message += res.headers.location + "\n"
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '# ' + $('h1.title').text() + "\n\n"
#親のdivまで指定しなければchunklist_childrenも含んでしまう
$('div>ul.chunklist.chunklist_book>li').each ->
li = $ @
a = li.children('a')
link = '[' + a.text() + '](' + baseUrl + a.attr('href') + ')'
# liのテキストのみ取得するいい方法がわからなかった…
li_text = `li.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '* ' + link + li_text + "\n"
#入れ子は多分一回
li.children('ul').each ->
#孫要素は直接指定できない?
ui = $ @
ui.children('li').each ->
children = $ @
a = children.children('a')
children_text = `children.first().contents().filter(function() {
return this.type === 'text';
}).text()`
link = '[' + a.text() + '](' + baseUrl + a.attr('href') + ')'
message += '** ' + link + children_text + "\n"
#ここ以外だと出力されない…
msg.send message
else
msg.send "のっと☆ふぁうんど!"
一気に泥臭くなりました。
shellでマークダウンっぽい出力を確認ののちにコミット&プッシュで確認します。
…そのままです。
慌てて調べたらslackのデフォルトで使えるマークダウンは一部で、全て使うにはPostで作らないといけない模様。
試しでテストしてみればよかったのですが、Herokuに送るためにはコミットしなければならないのでどうしても完成形を目指してしまいます。
巻き戻しが簡単とは言え少し悩みます。
<URL|文字>
形式も試してみましたが効かないので、文字リンクは諦めます…
そこからURLを普通に出力し、リストの入れ子を半角スペースのインデントで表現しましたが…
- スペースが取り除かれ入れ子が表現できない
- ```を使うとスペースがそのまま出力されるが強調などが使えない
- 出力が長すぎる(arrayなど)ものは```が評価されない
などなどがあり、結局最初のリストは「*」入れ子のリストは「-*」にしました。み、見辛い……
cheerio = require 'cheerio'
module.exports = (robot) ->
robot.hear /php man (.+)$/, (msg) ->
message = ''
baseUrl = "http://php.net/manual/ja/"
searchUrl = "http://php.net/manual-lookup.php?lang=ja&scope=quickref&pattern="
searchUrl += msg.match[1]
robot.http(searchUrl).get() (err, res, body) ->
if res.headers.location?
# 一致する結果があった
message += res.headers.location + "\n"
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '*' + $('h1.title').text() + "*\n\n"
#親のdivまで指定しなければchunklist_childrenも含んでしまう
$('div>ul.chunklist.chunklist_book>li').each ->
li = $ @
a = li.children('a')
# liのテキストのみ取得するいい方法がわからなかった…
li_text = `li.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '* ' + a.text() + li_text + "\n"
message += '- ' + baseUrl + a.attr('href') + "\n"
#入れ子は多分一回
li.children('ul').each ->
#孫要素は直接指定できない?
ui = $ @
ui.children('li').each ->
children = $ @
a = children.children('a')
children_text = `children.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '-* ' + a.text() + children_text + "\n"
message += '-- ' + baseUrl + a.attr('href') + "\n"
#ここ以外だと出力されない…
msg.send message
else
msg.send "のっと☆ふぁうんど!"
まぁ、リスト型のページなんてそんなに来る頻度は高くないでしょう。
次は肝心な関数ページをスクレイピングします
##関数ページのスクレイピング
後は目新しい要素はありません
関数のページはHTMLの構造も変わります。
URLも変わるので、res.headers.location
に含まれている文字列でどの種類のページか判定します。
関数ページのURLは
http://php.net/manual/ja/function.strlen.php
/function.(関数名).php
なので、URLに/function.
があれば関数のページとします。
後はswitchで分岐してスクレイピングです。
bookで体感しましたが、長すぎる表示は邪魔なので簡単な説明のみ表示します。
cheerio = require 'cheerio'
module.exports = (robot) ->
robot.hear /php man (.+)$/, (msg) ->
message = ''
baseUrl = "http://php.net/manual/ja/"
searchUrl = "http://php.net/manual-lookup.php?lang=ja&scope=quickref&pattern="
searchUrl += msg.match[1]
robot.http(searchUrl).get() (err, res, body) ->
if res.headers.location?
# 一致する結果があった
message += res.headers.location + "\n"
switch true
when res.headers.location.indexOf('/book.') != -1
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '*' + $('h1.title').text() + "*\n\n"
#親のdivまで指定しなければchunklist_childrenも含んでしまう
$('div ul.chunklist.chunklist_book li').each ->
li = $ @
a = li.children('a')
# liのテキストのみ取得するいい方法がわからなかった…
li_text = `li.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '* ' + a.text() + li_text + "\n"
message += '- ' + baseUrl + a.attr('href') + "\n"
#入れ子は多分一回
li.children('ul').each ->
#孫要素は直接指定できない?
ui = $ @
ui.children('li').each ->
children = $ @
a = children.children('a')
children_text = `children.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '-* ' + a.text() + children_text + "\n"
message += '-- ' + baseUrl + a.attr('href') + "\n"
#ここ以外だと出力されない…
msg.send message
when res.headers.location.indexOf('/function.') != -1
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '*' + $('h1.refname').text() + '*' + "\n"
message += $('p.refpurpose').text() + "\n"
message += '*' + $('div.refsect1.description h3.title').text() + '*' + "\n"
message += '`' + $('div.methodsynopsis.dc-description').text().replace(/\n/g, '').replace(/\s+/g, ' ') + '`' + "\n"
message += $('p.para.rdfs-comment').text() + "\n"
msg.send message
when res.headers.location.indexOf('/class.') != -1
message += 'そのうち' + "\n"
msg.send message
when res.headers.location.indexOf('/language.') != -1
message += 'そのうち' + "\n"
msg.send message
when res.headers.location.indexOf('/reserved.') != -1
message += 'そのうち' + "\n"
msg.send message
else
message += '非対応のURL' + "\n"
msg.send message
else
msg.send "のっと☆ふぁうんど!"
先ほどまでのスクレイピングを/book.
に移動。
見つけたURLの形式は今後対応ということで型だけ作っておきます。(coffeescriptはno break
できない?)
$('div.methodsynopsis.dc-description')
は関数の構文ですが、出力時に改行やスペースが入ることがあったので正規表現で調整してみました。
装飾によっては説明文に改行などが入ったり、
var_dump()の説明文のHTMLがほかと違っていて対応していなかったりとありますが、概ね満足です。
おまけのもしかして
最後に、存在しない検索ワードを入れた場合のもしかしてページをスクレイピングします。これで誤字っても安心になるかも。
書き換え場所はmsg.send "のっと☆ふぁうんど!"
の所です。
cheerio = require 'cheerio'
module.exports = (robot) ->
robot.hear /php man (.+)$/, (msg) ->
message = ''
baseUrl = "http://php.net/manual/ja/"
searchUrl = "http://php.net/manual-lookup.php?lang=ja&scope=quickref&pattern="
searchUrl += msg.match[1]
robot.http(searchUrl).get() (err, res, body) ->
if res.headers.location?
# 一致する結果があった
message += res.headers.location + "\n"
switch true
when res.headers.location.indexOf('/book.') != -1
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '*' + $('h1.title').text() + "*\n\n"
#親のdivまで指定しなければchunklist_childrenも含んでしまう
$('div ul.chunklist.chunklist_book li').each ->
li = $ @
a = li.children('a')
# liのテキストのみ取得するいい方法がわからなかった…
li_text = `li.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '* ' + a.text() + li_text + "\n"
message += '- ' + baseUrl + a.attr('href') + "\n"
#入れ子は多分一回
li.children('ul').each ->
#孫要素は直接指定できない?
ui = $ @
ui.children('li').each ->
children = $ @
a = children.children('a')
children_text = `children.first().contents().filter(function() {
return this.type === 'text';
}).text()`
message += '-* ' + a.text() + children_text + "\n"
message += '-- ' + baseUrl + a.attr('href') + "\n"
#ここ以外だと出力されない…
msg.send message
when res.headers.location.indexOf('/function.') != -1
robot.http(res.headers.location).get() (err, res, body) ->
$ = cheerio.load body
message += '*' + $('h1.refname').text() + '*' + "\n"
message += $('p.refpurpose').text() + "\n"
message += '*' + $('div.refsect1.description h3.title').text() + '*' + "\n"
message += '`' + $('div.methodsynopsis.dc-description').text().replace(/\n/g, '').replace(/\s+/g, ' ') + '`' + "\n"
message += $('p.para.rdfs-comment').text() + "\n"
msg.send message
when res.headers.location.indexOf('/class.') != -1
message += 'そのうち' + "\n"
msg.send message
when res.headers.location.indexOf('/language.') != -1
message += 'そのうち' + "\n"
msg.send message
when res.headers.location.indexOf('/reserved.') != -1
message += 'そのうち' + "\n"
msg.send message
else
message += '非対応のURL' + "\n"
msg.send message
else
# 一致する結果がなかったので親しいと思われる関数リストを上から5件表示
message += 'http://php.net' + res.req.path + "\n"
$ = cheerio.load body
message += '*' + $('section#layout-content h1').text() + '*' + "\n"
message += $('section#layout-content p').text() + '上位5件を表示します' + "\n"
i = 0
$('ul#quickref_functions li').each ->
return false if i == 5
li = $ @
a = li.children('a')
message += '* ' + a.text() + ' - ' + 'http://php.net' + a.attr('href') + "\n"
i++
msg.send message
book
の反省を活かし上位5件までを表示します。
レスポンスからのURLの取得方法やeachの抜け方にもやっとしてます。
##感想
BOTが反応してくれて楽しかった(小並感)
コールバックや変数のスコープがイマイチわかっていない。
Shellで動くのにHeroku上では動かなかったり逆のパターンが一度ずつあって少し困った。
Herokuで動かない理由は全角スペースの混入のためだった。
Q.このコマンド使うの?
A.実際はPHPStormのクイックリファレンスやググる方が多そう。
でもコードの話をしているときにさっくり関数の説明を共有できるといいかも。
だからSlackの布教を頑張って(現在1名)
Q.各技術の理解が深まった?
A.そんなに。理解不要で導入出来るほどの記事の多さがメリットですし、第一その手軽さがなかったらチャレンジしてませんし。
でも「動作するモノ」が手元にあるってことは学習意欲を掻き立てられていいと思います。
Q.コード汚すぎない?
A.はい。