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


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

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

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

つまり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が入ってるならワンライナーでインストールできます。

brew install redis

macOS(Sierra) に 5分 で Redis の環境を作る手順 | Syntax Error.

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

npm install redis

redis

redisの型

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

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

動作確認

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

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

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

※非同期処理の為にasyncを使用しています

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で描き直してみます。

 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だったら削除しないという感じに書き換えてみます。個別処理に失敗したというイメージです。

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の部分を外してもう一回動かしてみます。

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

コマンドリファレンス — redis 2.0.3 documentation

Node.js databases: Using Redis for fun and profit