タスクリストの管理にredisを使ってみる



> 本題だけ知りたい方はこのブロックを読み飛ばしてください

私はほぼ毎日某サイトからスクレイピングしてデータを取得し、DBに突っ込んでいます。多い日には300ページ以上から取得することもあります。

スクレイピングする時のマナーとして「無闇にアクセスしない」ということを意識してるのでリクエストの間隔を２秒程度は空けることにしています。

つまり300ページから取得する場合少なくとも300×2=600秒=10分かかります。

最初に300ページのURLを並べたタスクリストを作り、それを順番に回します。

しかし途中で通信が切れたりURLが間違えていた場合エラーになり、やり直すことになります。

再度処理を行うときにはタスクリストの成功した項目を省いて未取得のURLのみを処理することになります。



以前はDBにLastUpdateという項目があったのでそれを目安に再取得していたのですが、DBの設計を変更した結果同じ方法は使えなくなりました。

これを解決する手段、つまりタスクリストの管理を行う方法を考えてみました。

- 別にタスクリスト専用のtableを作ってDBで管理する

- タスクリスト専用のファイルを使って読み書きする

  ​
こんなところでしょうか。

しかしタスクリストは使い捨てです。DBでもファイルでも破棄するのが手間です。というか一旦DBに保存したものを削除するのは怖いです。

そこで名前は聞いていたけど使ったことのないredisを試してみることにしました。


## やりたいこと
- **fetchWithUrl()**という関数があるとします。引数にURLを渡すとスクレイピングしてDB保存する関数です
- **urlList**というタスクリストがあるとします。複数のURLが入っている配列です。
- **urlList**をloopして **fetchWithUrl()**に渡していきます。
- **fetchWithUrl()**が成功したら **urlList**からurlを削除します。
- この一連のプロセスが異常終了した時の為に **urlList**を半永続化させたい

この **半永続化**のためにredisを使います。

## セットアップ

例えばmacでhomebrewが入ってるならワンライナーでインストールできます。  




```shell
brew install redis
```



> [macOS\(Sierra\) に 5分 で Redis の環境を作る手順 \| Syntax Error\.](https://utano.jp/entry/2016/10/redis_install_for_maxos_sierra/)



私はnodejsを利用していますが、moduleのインストールも1行です。



```shell
npm install redis
```

> [redis](https://www.npmjs.com/package/redis)



## redisの型

redisはKey-Value Store、つまり`key: value`という形で値を設定できます。

そのvalue部分は以下の型が使えます。

- [文字列型](http://redis.shibu.jp/commandreference/strings.html)
- [リスト型(文字列の配列)](http://redis.shibu.jp/commandreference/lists.html)
- [セット型(重複なしの配列)](http://redis.shibu.jp/commandreference/sets.html)
- [ソート済みセット型(順番ありのセット型)](http://redis.shibu.jp/commandreference/sortedsets.html)
- [ハッシュ型(連想配列)](http://redis.shibu.jp/commandreference/hashes.html)



## 動作確認

今回はタスクリストとしてセット型というヤツを使ってみようと思います。手順は以下の通り。

1. URL一覧をsetに追加する
2. setのコピーを取得してloopする
3. 処理が成功ならsetからitemを削除する

簡単なtestを以下に記します。(nodejsでcoffee-scriptです)

※非同期処理の為に[async](http://caolan.github.io/async/docs.html#)を使用しています


```coffeescript
async_ = require 'async'

describe "redis", ->
  it "test",(done) ->
    key = 'testSet'
    redis = require('redis').createClient()
    redis.sadd key,['a','b','c'],(err)->
      throw err if err
      redis.sinter key,(err,arr)->
        throw err if err
        console.log arr
        async_.eachSeries arr, (item, next) ->
          redis.srem key,item,(err)->
            redis.sinter key,(err,newArr)->
              throw err if err
              console.log newArr
              next()
        , (err)->
          done()
          
# [ 'a', 'b', 'c' ]
# [ 'b', 'c' ]
# [ 'c' ]
# []
```

ネストがひどいのでasync.waterfallで描き直してみます。

```coffeescript
 async_ = require 'async'
  
 describe "redis", ->
  it "test",(done) ->
    key = 'testSet'
    redis = require('redis').createClient()
    async_.waterfall [
      (next)-> redis.sadd key,['a','b','c'],(err,res)->
        assert.isNull(err)
        next()
      (next)-> redis.sinter key,(err,arr)->
        assert.isNull(err)
        console.log arr
        next(null,arr)
      (arr,next)-> async_.eachSeries arr, (item, next_) ->
        redis.srem key,item,(err)->
          redis.sinter key,(err,newArr)->
            assert.isNull(err)
            console.log newArr
            next_()
      ,(err)->next()
    ],(err)->done()

# [ 'a', 'b', 'c' ]
# [ 'b', 'c' ]
# [ 'c' ]
# []
```







上記では以下のredisメソッドを使用しています。

### sadd(*key*, *member*,callback)

keyにmemberを追加する(型はset)

### sinter(key,callback)

keyを指定してsetを取得します

### srem(key,member,callback)

keyとmemberを指定してsetの要素を削除します

---

最初にsaddで['a','b','c']を追加(これがタスクリストになる)、

それを取得してasync_.eachSeriesで回す。(タスクリストからアイテムを渡し個別処理を行う)

個別処理が無事に終わればsremでアイテムを削除する、というイメージです。



次に **itemがbだったら削除しない**という感じに書き換えてみます。個別処理に失敗したというイメージです。

```coffeescript
describe "redis", ->

  it "test",(done) ->
    key = 'testSet'
    redis = require('redis').createClient()
    async_.waterfall [
      (next)-> redis.sadd key,['a','b','c'],(err,res)->
        assert.isNull(err)
        next()
      (next)-> redis.sinter key,(err,arr)->
        assert.isNull(err)
        console.log arr
        next(null,arr)
      (arr,next)-> async_.eachSeries arr, (item, next_) ->
        # ここを修正
        if item isnt 'b'
          redis.srem key,item,(err)->
            redis.sinter key,(err,newArr)->
              assert.isNull(err)
              console.log newArr
              next_()
        else
          next_()
      ,(err)->next()
    ],(err)->done()

# [ 'a', 'b', 'c' ]
# [ 'b', 'c' ]
# [ 'b' ]
```



bの場合sremが行われないので、タスクリストに残りました。

---

上記のコードを実行して一旦プロセスは終了しているので、タスクリストが変数だとしたら消えていますが、redisなので残っているはずです。

上記のコードからsaddの部分とsremの部分を外してもう一回動かしてみます。



```coffeescript
describe "redis", ->

  it "test",(done) ->
    key = 'testSet'
    redis = require('redis').createClient()
    async_.waterfall [
      # (next)-> redis.sadd key,['a','b','c'],(err,res)->
      #   assert.isNull(err)
      #   next()
      (next)-> redis.sinter key,(err,arr)->
        assert.isNull(err)
        console.log arr
        next(null,arr)
      # (arr,next)-> async_.eachSeries arr, (item, next_) ->
      #   if item isnt 'b'
      #     redis.srem key,item,(err)->
      #       redis.sinter key,(err,newArr)->
      #         assert.isNull(err)
      #         console.log newArr
      #         next_()
      #   else
      #     next_()
      # ,(err)->next()
    ],(err)->done()

# [ 'b' ]
```



bはちゃんと残っていました。

この要領でタスクリストに使えそうです。



## まとめ



redisはtwitterでも使われているというニュースを聞いて久しいですが、使いどころがわからなくて今まで使ったことがありませんでした。

今回は[プロセスが終了しても維持できる変数]くらいの感じで使ってみたのですが意外に便利です。

特に`redis = require('redis').createClient()`の1行でセットアップ完了というのが気に入りました。

しかし、もしかしたらredisよりも新しくて便利な技術があるかもしれませんので、ご存知の方は教えて頂けたら嬉しいです。



## 参考サイト

[NodeRedis/node\_redis: redis client for node](https://github.com/NodeRedis/node_redis)

[コマンドリファレンス — redis 2\.0\.3 documentation](http://redis.shibu.jp/commandreference/)

[Node\.js databases: Using Redis for fun and profit](https://blog.yld.io/2016/11/07/node-js-databases-using-redis-for-fun-and-profit/#.WYW-D9Pyhbs)