TVerをクローリングしてつぶやいてみる

# TVer
TV局がやっと重い腰を上げたようです。
テレビは好きなのですがいちいちサイトにアクセスするのも面倒なのでちょっと作ってみました。

http://tver.jp/

http://blog.livedoor.jp/itsoku/archives/46709256.html

# イメージ画像
![2015-10-30_01h35_06.png](https://qiita-image-store.s3.amazonaws.com/0/39243/067eed8d-b6b5-46d4-711c-d582a0fbda99.png)

# クローリング
クローリングには以前からつかってみたかったkimonoを使用します。
https://www.kimonolabs.com/

## kimonoの使い方
http://qiita.com/her0m31/items/b844ff74a9d488c9dc81

# 取得結果
kimonoで画面をポチポチして少しばかりセレクターを書き換えてあげて
以下の様なjsonを取得します。

```json
{
  "name": "new",
  "count": 25,
  "frequency": "Manual Crawl",
  "version": 3,
  "newdata": true,
  "lastrunstatus": "success",
  "thisversionstatus": "success",
  "thisversionrun": "Tue Oct 27 2015 15:07:24 GMT+0000 (UTC)",
  "results": {
    "collection1": [
      {
        "title": "遺産争族",
        "episode": "第1話",
        "sub": "第1話10月22日(木)放送分",
        "index": 1,
        "url": "http://tver.jp/"
      },
      {
        "title": "マツコの知らない世界",
        "episode": "#44",
        "sub": "#4410月27日(火)放送分",
        "index": 2,
        "url": "http://tver.jp/"
      },
      {
        "title": "TERRACE HOUSE BOYS ＆ GIRLS IN THE CITY",
        "episode": "2015/10/26放送 TERRACE HOUSE BOYS ＆ GIRLS IN THE CITY 3rd WEEK",
        "sub": "2015/10/26放送 TERRACE HOUSE BOYS ＆ GIRLS IN THE CITY 3rd WEEK10月26日(月)放送分",
        "index": 3,
        "url": "http://tver.jp/"
      },
      {
        "title": "ゴッドタン",
        "episode": "オアシズ＆ドランクドラゴン＆麒麟の衝撃エピソード！コンビ愛確かめ選手権",
        "sub": "オアシズ＆ドランクドラゴン＆麒麟の衝撃エピソード！コンビ愛確かめ選手権10月24日(土)放送分",
        "index": 4,
        "url": "http://tver.jp/"
      },
      {
        "title": "有吉反省会",
        "episode": "2015/10/24放送分",
        "sub": "2015/10/24放送分10月24日(土)放送分",
        "index": 5,
        "url": "http://tver.jp/"
      },
      {
        "title": "結婚式の前日に",
        "episode": "第3話",
        "sub": "第3話10月27日(火)放送分",
        "index": 6,
        "url": "http://tver.jp/"
      },
      {
        "title": "空から日本を見てみよう plus",
        "episode": "巨大なハッピーターンにドクターマリオのカプセル!?大自然の中の露天風呂に潜入",
        "sub": "巨大なハッピーターンにドクターマリオのカプセル!?大自然の中の露天風呂に潜入10月27日(火)放送分",
        "index": 7,
        "url": "http://tver.jp/"
      },
      {
        "title": "太田上田",
        "episode": "大須スケートリンク編①",
        "sub": "大須スケートリンク編①10月26日(月)放送分",
        "index": 8,
        "url": "http://tver.jp/"
      },
      {
        "title": "PON!～ママモコモてれび～",
        "episode": "新しいかごめかごめ",
        "sub": "新しいかごめかごめ10月27日(火)放送分",
        "index": 9,
        "url": "http://tver.jp/"
      },
      {
        "title": "この差って何ですか？",
        "episode": "#16",
        "sub": "#1610月25日(日)放送分",
        "index": 10,
        "url": "http://tver.jp/"
      },
      {
        "title": "世にも不思議なランキング「なんで？なんで？なんで？」",
        "episode": "#18",
        "sub": "#1810月26日(月)放送分",
        "index": 11,
        "url": "http://tver.jp/"
      },
      {
        "title": "ZIP!～MOCO'Sキッチン～",
        "episode": "もこみち流　 チキンのミックスサラダ",
        "sub": "もこみち流　 チキンのミックスサラダ10月27日(火)放送分",
        "index": 12,
        "url": "http://tver.jp/"
      },
      {
        "title": "ZIP!～グッド・モーニング!!! ドロンジョ～",
        "episode": "極秘作戦の行方は……？",
        "sub": "極秘作戦の行方は……？10月27日(火)放送分",
        "index": 13,
        "url": "http://tver.jp/"
      },
      {
        "title": "吉田類の酒場放浪記",
        "episode": "#685",
        "sub": "#68510月26日(月)放送分",
        "index": 14,
        "url": "http://tver.jp/"
      },
      {
        "title": "【BSフジ】小山薫堂 東京会議",
        "episode": "2015/10/24放送 小山薫堂 東京会議",
        "sub": "2015/10/24放送 小山薫堂 東京会議10月24日(土)放送分",
        "index": 15,
        "url": "http://tver.jp/"
      },
      {
        "title": "Sing!Sing!Sing!",
        "episode": "#70",
        "sub": "#7010月24日(土)放送分",
        "index": 16,
        "url": "http://tver.jp/"
      },
      {
        "title": "がっちりマンデー！！",
        "episode": "#562",
        "sub": "#56210月25日(日)放送分",
        "index": 17,
        "url": "http://tver.jp/"
      },
      {
        "title": "チャージ730！コーナーセレクション～ランチ de チャージ～",
        "episode": "つけ麺戦国時代！ゆずの香り広がるドロドロ系ＶＳ真鯛スープの淡麗系",
        "sub": "つけ麺戦国時代！ゆずの香り広がるドロドロ系ＶＳ真鯛スープの淡麗系10月26日(月)放送分",
        "index": 18,
        "url": "http://tver.jp/"
      },
      {
        "title": "旅ずきんちゃん",
        "episode": "＃131",
        "sub": "＃13110月25日(日)放送分",
        "index": 19,
        "url": "http://tver.jp/"
      },
      {
        "title": "下町ロケット",
        "episode": "第2話",
        "sub": "第2話10月25日(日)放送分",
        "index": 20,
        "url": "http://tver.jp/"
      },
      {
        "title": "イチから住～前略、移住しました～",
        "episode": "2015年10月25日放送",
        "sub": "2015年10月25日放送10月25日(日)放送分",
        "index": 21,
        "url": "http://tver.jp/"
      },
      {
        "title": "正直さんぽ",
        "episode": "2015/10/24放送 正直女子さんぽ　所沢",
        "sub": "2015/10/24放送 正直女子さんぽ　所沢10月24日(土)放送分",
        "index": 22,
        "url": "http://tver.jp/"
      },
      {
        "title": "SENSORS",
        "episode": "10月24日(土)放送分",
        "sub": "10月24日(土)放送分10月24日(土)放送分",
        "index": 23,
        "url": "http://tver.jp/"
      },
      {
        "title": "チェンジ3",
        "episode": "2015年10月24日放送",
        "sub": "2015年10月24日放送10月24日(土)放送分",
        "index": 24,
        "url": "http://tver.jp/"
      },
      {
        "title": "ブラマヨとゆかいな仲間たち アツアツっ！",
        "episode": "2015年10月24日放送",
        "sub": "2015年10月24日放送10月24日(土)放送分",
        "index": 25,
        "url": "http://tver.jp/"
      }
    ]
  }
}
```

# 取得したjsonのパース
json4sで取得したjsonをパースしてcase classにしちゃいます。

```scala
case class TvInfo(title: String, episode: String, sub: String) 
```

## パースするところ（コメントで説明書いてます）
```scala
import java.util.Locale

import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormat

import scala.io.Source
import org.json4s._
import org.json4s.jackson.JsonMethods._

/**
 * Created by FScoward on 2015/10/28.
 */
case class TvInfo(title: String, episode: String, sub: String) {

  override def toString = {
    s"${trim(title)} - ${trim(episode)} - ${trim(sub)}"
  }

  // tweet文字数制限にひっかからないようにしています。
  def trim(s: String) = {
    s.length > 15 match {
      case true => s"${s.substring(0, 15)}...(略)"
      case false => s
    }
  }
}
class TVerService {
  def getTvInfo = {
    Source.fromURL("https://www.kimonolabs.com/api/【秘密だよ】", "UTF-8").mkString
  }

  def convert(json: String) = {
    implicit val formats = DefaultFormats
    val parsedJson = parse(json)
    // 取得時間はjson中に一回だけ出てくるものなので、ここで先に取得しちゃいます。
    val thisVersionRun = parsedJson \\ "thisversionrun"
    // あとはforで回してcase classに格納します。
    val tvInfoList: List[TvInfo] = for {
      JObject(tvInfo) <- parsedJson
      JField("title", JString(title)) <- tvInfo
      JField("episode", JString(episode)) <- tvInfo
      JField("sub", JString(sub)) <- tvInfo
    } yield TvInfo(title, episode, sub.replace(episode, ""))

    (timeFormat(compact(thisVersionRun)), tvInfoList)
  }

  // そのまま表示してもいいですが不親切なので、少し見やすい形式に変換してやります。
  def timeFormat(time: String) = {
    // Tue Oct 27 2015 15:07:24 GMT+0000 (UTC)
    // EEE MMM dd yyyy HH:mm:ss zZ '(UTC)'
    val formatter = DateTimeFormat.forPattern("EEE MMM dd yyyy HH:mm:ss zZ '(UTC)'").withZone(DateTimeZone.forID("GMT")).withLocale(Locale.US)
    formatter.parseDateTime(time.replace("\"", "")).toString("yyyy/mm/dd HH:mm (EEE)")
  }

}
```

## 補足

```scala
TvInfo(title, episode, sub.replace(episode, ""))
```
としているのは、クローリングのセレクターでsubの中にepisodeの内容も入るようになってしまっているためです。（subだけの取り方がわからん(´；ω；｀)）

# どうせなら自動で動かしたい
akkaとcommons-daemonを利用します。
http://akka.io/
http://commons.apache.org/proper/commons-daemon/

```scala:Executor.scala
class Executor extends Daemon {

  override def init(dc: DaemonContext): Unit = {
  }

  override def stop() = {
  }
  override def destroy = {
  }

  override def start = {
    val system = ActorSystem("mySystem")
    val tweetActor = system.actorOf(Props[TweetActorImpl], "tweetActor")
    tweetActor ! "tv"
  }
}
```

## Actor定義

```scala:TweetActor.scala
import akka.actor.Actor
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global


/**
 * Created by FScoward on 2015/10/13.
 */
class TweetActorImpl extends TweetActor {
  val tweetService = new TweetService
  val animeService = new AnimeService
}
trait TweetActor extends Actor {
  val tweetService: TweetService
  val animeService: AnimeService

  def receive = {
    case text: String if text == "tv" => {
      val t = new TVerService()
      val (time, tvInfoList) = t.convert(t.getTvInfo)
      // ↓がつぶやく処理
      tvInfoList.foreach(tv => tweetService.tweet(s"${tv.toString} - updated: $time"))
      // ↓一日一回ぶん回す感じで
      context.system.scheduler.scheduleOnce(1 days , self, "tv")
    }
    case _ => println("Nothing...")
  }
}
```

## Tweet
Twitter4jを使用しています。
非常に簡素な作りとなっています。

```scala:TweetService.scala
import twitter4j.{TwitterFactory, Paging}
import collection.JavaConversions._

/**
 * Created by FScoward on 2015/10/27.
 */
class TweetService {
  val twitter = TwitterFactory.getSingleton()

  def tweet(text: String) = {
    twitter.updateStatus(s"$text")
  }
}
```

# おわりに
kimono使うと簡単にクローリング出来ていいですね～

<a rel="nofollow" href="http://www.amazon.co.jp/gp/product/4797380357/ref=as_li_ss_tl?ie=UTF8&camp=247&creative=7399&creativeASIN=4797380357&linkCode=as2&tag=coward0d-22">Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例</a><img src="http://ir-jp.amazon-adsystem.com/e/ir?t=coward0d-22&l=as2&o=9&a=4797380357" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" />