0

色々なPromiseライブラリで1つずつ順番に処理する

最初、Qでなぜかcatchでエラーが捕まえられないぞ?と書いていたけど、require(‘q’).Promiseを使うんだと教えてもらった。ありがとうございます。



色々なNodeのライブラリがそれぞれで好きなPromiseライブラリ使ってるけど、それらのライブラリ達をいっしょにアプリ内で混ぜてつなげても大丈夫なのか?ちゃんと相互にthenでチェインしたりエラー捕まえたりできるのか?
という事と、async.eachSeriesのようにURLのリストを1つずつ順番に処理完了するのを待ちながら処理していくのはPromiseでどうやって書くのかな?
というのが気になっていたのでちょっと調べた。


勉強用リポジトリ
https://github.com/shokai/promise-study

環境

node v0.10.29 + coffee-script v1.8.0
なのでまだPromiseが標準ライブラリに入ってないNode環境


試したPromiseライブラリ

このへんが有名そうだったのでREADMEやサンプルなどを読んで、試した。
でもdeferredはインタフェースがthenableじゃないっぽかったのですぐあきらめた。


結論

es6-promiseとbluebirdとQを混ぜこぜでthen/catchでチェインさせてもちゃんと動いた。

then/catchしたいだけならes6-promise使って、
async.jsでやるような高機能な並列・並行処理の制御がしたければbluebirdやQに付いている便利な関数を使えばいいと思う。


試したコード
## いろいろなPromiseライブラリを使ってみる

{Promise} = require 'es6-promise'
# {Promise} = require 'q'
# Promise = require 'bluebird'

checkOdd = (num) ->
return new Promise (resolve) ->
if typeof num isnt 'number'
throw new Error "#{num} is not number"
resolve num % 2 is 1

for i in [0,1,2,3,null,5,"かずどん",7,8]
do (i) ->
checkOdd i
.then (res) ->
if res
console.log "#{i} is odd"
else
console.log "#{i} is not odd"
.catch (err) ->
console.error err

実行結果


色々なPromiseライブラリをつなげる、1つずつ処理する

HTTP Getして、HTMLからtitleを取り出して、それをmacのsayコマンドで読む。
という3つのPromiseをそれぞれ別々のPromiseライブラリで作る。
そして3つ繋げて1セットの処理として、URLリストを順番に処理していく。
RSSを順番に見ていくクローラー的な処理を想定していて、1つ終わったら3秒待ってから次のURLを見に行くようにした。

試しに書いてみたコード
ちなみにURLリストを全部同時に処理するversionもある


下の方でBluebird.eachを使って1つずつURLを処理していく。
thenのチェインが最後まで走ったら3秒待ってから次のURLを処理するし、どこか途中でエラーが起きたらcatchして5秒待って次のURLの処理に行く。
とにかく同時に複数のHTTPリクエストは送らない。

こういうのをasync.eachSeriesで書くとけっこう頭が疲れるコードになると思うけどPromise使ったらすんなり書けた。

es6-promiseとbluebirdとQを混ぜて使っているけどちゃんと動いた。

## HTMLを(1つずつ)取得してtitleを取り出してsayで読み上げる
## いろいろなPromiseライブラリを混ぜて使ってみる
## HTTPリクエストするのは3000 msecごと
## 途中でエラーがあったら5000 msec待ってから、次のHTTPリクエストする

request = require 'request'
cheerio = require 'cheerio'
{exec} = require 'child_process'

process.env.DEBUG ||= '*'
debug = require('debug')('promise-study')


{Promise} = require 'es6-promise'
Q = require('q')
Bluebird = require 'bluebird'

urls = [
'http://shokai.org'
'そんなURLはない' # URLじゃない文字列。requestの例外を発生させるため
'https://github.com'
'https://github.com/robots.txt' # HTMLが返ってこないURL。titleタグ取得のエラーを起こすため
'https://google.co.jp'
]

# HTML本文を取得するPromise
# URLが間違っていたりすると失敗する
getHtml = (url) ->
debug "getHtml(#{url})"
return new Q.Promise (resolve, reject) -> # Qを使う
request url, (err, res, body) ->
if err or res.statusCode isnt 200
return reject(err or "statusCode: #{res.statusCode}")
resolve body

# HTMLからタイトルを取得するPromise
# HTMLじゃなければ失敗する
getTitle = (html) ->
debug "getTitle(html)"
return new Bluebird (resolve, reject) -> # Bluebirdを使う
$ = cheerio.load html
if title = $('title').text()
return resolve title
reject 'title not found'

# 音声読み上げするPromise
speech = (txt) ->
debug "speech(#{txt})"
return new Promise (resolve, reject) -> # es6-promiseを使う
exec "say #{txt}", (err, stdout, stderr) ->
return reject(txt) if err
resolve(txt)

# URLリストをBluebird.eachで1つずつ処理する
Bluebird.each urls, (url) ->
getHtml url
.then getTitle
.then speech
.then (title) ->
return new Q.Promise (resolve) -> # Qを使う
debug "wait 3000 msec"
# 3秒待ってから次のURLの処理へ
setTimeout ->
debug "wait done"
debug "!!OK #{url} - #{title}"
resolve()
, 3000
.catch (err) ->
return new Promise (resolve, reject) -> # es6-promiseを使う
debug "!!ERROR #{url} - #{err}"
debug "wait 5000 msec for Error"
# どこかでエラーあったら5秒待つ
setTimeout ->
debug "wait done"
resolve()
, 5000


実行結果

ちゃんと3秒/5秒待って次、と順番に処理できている。
requestにURLじゃない文字列を渡した時の例外も、try catch書かずにpromiseのcatchで捕捉できている。

0

RSS新着まとめてチェックしてSlackに投げるやつ作った

作った。

https://github.com/masuilab/rss-slackbot

主にgithubと、研究室の人のblogの更新をSlackに通知するのに使ってる。

Slackに最初からあるRSS integrationは、entryのdescriptionを埋め込んでしまうのでgithubのRSSを読み込ませるとデカくなって邪魔だった。

githubとかはURLをpostすれば適当にかっこよくslackがチャットにembedしてくれるので、titleとURLだけpostする感じにした。

あとたくさんfeedを管理するのもwebで1つずつ登録したりとか面倒なのでgithubでプルリクとかでやりたい。
RSS追加したい人はconfig.json編集してください。


docker/dokkuで動かしたかったので、crontabとかDBとか使うのやめた。
titleとURLなんて大したサイズ量じゃないから全部オンメモリでいいやという事でsetIntervalで定期的に見に行くだけにしてみた。

0

if a == b

typo

if a == b
のつもりで
if a = b
と書くと、常に真になり、文法も間違っていないので普通に実行されてエラーでない。

このtypoに気づくのに1時間以上かかった。

coffee-scriptだったので今後は==はやめてisを使うようにしたい。
if a is b
isも==もcoffeeではコンパイルすると===になる。怖いからもう==使いたくない。

ちなみにisntは!==になる。


Rubyでis, isnt


class Object
alias_method :is, :==
alias_method :isnt, :!=
end

puts 1.is 1 # => true
puts 1.is 2 # => false
puts 1.isnt 2 # => true
puts 1 is 1 # = > syntax error

Rubyでは==や!=もObjectに定義されたメソッドなのでis/isntできるかと思ったけど、スペース空けると文法エラーになる

もしくはif文と同じ行で代入してたら警告する機能とかほしい

0

あいまいテキスト検索 AsearchをNodeに移植した

asearchという文字列が似ているかどうか判定するrubygemがある。
増井先生が作ったもので、gyazzのページ名サジェストなどに使われている。

それをnodeに移植した。
https://npmjs.org/package/asearch
https://github.com/shokai/node-asearch


特徴

  • はやい
  • byte列を比較しているだけ
  • 結果はtrue/falseで返ってくる
  • あいまい度は指定できる(0〜3まで)
  • 他のライブラリに依存していない、pure javascript
たぶんブラウザでも動くのであとで試してみる。


インストール


npm install asearch


使い方

Asearch = require 'asearch'

a = new Asearch 'abcde'

console.log a.match 'abcde' # => true
console.log a.match 'AbCdE' # => true
console.log a.match 'abcd' # => false
console.log a.match 'abcd', 1 # => true
console.log a.match 'ab de', 1 # => true
console.log a.match 'abe', 1 # => false
console.log a.match 'abe', 2 # => true

typoを判定
a = new Asearch 'cheese burger'

console.log a.match 'cheese burger' # => true
console.log a.match 'chess burger', 2 # => true
console.log a.match 'chess', 2 # => false


2バイト文字もok
a = new Asearch '漢字文字列'

console.log a.match '漢字文字列' # => true
console.log a.match '漢字文字烈' # => false
console.log a.match '漢字文字烈', 2 # => true

このように手っ取り早く2つの文字列が似ているかどうか判定できて便利。


Nodeでビット演算

RubyからJavaScriptへ移植するにあたってビット演算がちょっと違った。

右シフト >> はjavascriptでは符号なし右シフト >>> にしないと符号が反転してしまう事がある。10分ぐらい気付かなかった。
|= とか >>>= とかも使える。
coffee-scriptだとさらに0b1010のような2進数記法も使えて便利。

RubyのString#unpackはNodeにもjspackという実装があるが、2byte文字に対応していなかったので自前実装した。

0

node-methodmissingを高速化した

methodmissingというnpmがあって、rubyのmethodmissingと同じく存在しなメソッドを呼び出した時にno method errorにするのではなく、メソッド名と引数を横取りできる。


使用例

mm = require 'methodmissing'

class Foo
baz: "bazbaz"

foo = new Foo()

## methodmissingを適用
foo = mm foo, (func_name, args) ->
console.log "missing-method '#{func_name}' called"
console.log args


## 普通のメソッド呼び出し
console.log foo.baz

## 存在しないメソッドを呼び出す
foo.kazusuke("niku", "beer", "rice")

実行結果
bazbaz
missing-method 'kazusuke' called
{ '0': 'niku', '1': 'beer', '2': 'rice' }


存在しないメソッドが呼び出された時に、動的にメソッドが存在するフリをできるのはWebAPIのラッパーを作る時などに便利。


高速化


ベンチマークを取ってみたら、
存在しないメソッド呼び出しをキャプチャーするのは6〜7倍ぐらい速くなった。普通のメソッド呼び出しと比べて10倍遅い程度。
また、methodmissing適用した後のオブジェクトは通常のメソッド呼び出しが100倍ぐらい遅くなっていたのを、9倍遅い程度に抑えられた。

(methodmissingでメソッドを呼び出すコストが通常のメソッド呼び出しに比べて10倍ぐらい遅くなるだけで、呼び出されたメソッド全体の速度が遅くなるわけではないので、ループで何万回も呼び出す等でない限りこのコストは無視していいと思う)


といっても修正したの1行だけなんだけど、けっこうこういうので変わるものだな

存在しないプロパティを呼び出した時の動作が遅いのがまだ気になる。