あそびたい

こころにうつりゆくよしなしごとを

twitter4jとScalaでネトストしたい!!

好きな人のツイートは全部見たいって思うことありますよね。
ツイ消しも画像も含めて全部見たいって思いますよね。
ということで、ガチネトストプログラムを作りました。

仕様

  • ツイートは全てtxtファイルに保存
  • 画像もローカルに保存
  • 複数のアカウントに対応

以上の仕様に対応するように作成します。
今回はScala 2.11.8を使用しようと思います。

ディレクトリ構成

sbtを使っていくので、ディレクトリ構成は以下のようになります

/
|- src/
   |- main/
      |- scala/
         |- main.scala
         |- connect.scala
         |- stream.scala
|- files/
|- build.sbt
|- twitter4j.properties

build.sbt

build.sbtに必要なライブラリの依存関係を書いていきます。
とはいえ、twitter4j以外使わないので以下のようになります。

lazy val root = (project in file(".")).
  settings(
    inThisBuild(List(
      scalaVersion := "2.11.8",
      version      := "0.1.0-SNAPSHOT"
    )),
    name := "stalker",
    libraryDependencies ++= Seq(
      "org.twitter4j" % "twitter4j-core" % "4.0.4"
        , "org.twitter4j" % "twitter4j-stream" % "4.0.4"
    )
  )

twitter4j.properties

次に、twitter4jの設定を書いていきます。

debug=true
oauth.consumerKey=YOUR_CONSUMERKEY
oauth.consumerSecret=YOUR_CONSUMERSECRET
oauth.accessToken=YOUR_ACCESSTOKEN
oauth.accessTokenSecret=YOUR_ACCESSTOKENSECRET
twitter4j.loggerFactory=twitter4j.NullLoggerFactory

自分のconsumerKeyその他は頑張って探してください。
普通にググれば取り方が出て来るはずなので。

本体

コードそのものを書いていきます。

connect.scala

まずは、screen name(@以下の部分)からID(アカウントに固有の数字)を求めましょう。
そうすることで、@以下を変えられたとしても継続してストーキングすることができるようになります。

package connect

import twitter4j._

object TwitterConnector{
  val factory = new TwitterFactory()
  val twitter = factory.getInstance()
  def getId(names : List[String]) :List[Long] ={
    return names.map(x => twitter.showUser(x).getId())
  }
}

これで終わりです。ライブラリって便利ですね。
StringのListの形で引数を与えると、Id(Long)のListの形で返って来るようになっています。
本当はError処理とかするべきなんですが、面倒なので割愛します。

streaming.scala

次はstreamingでツイートを取得しましょう。
好きな人が呟いた瞬間にツイートを取って来ることができるようになります。

package stream

import java.io._
import java.net.URL
import twitter4j._
import scala.sys.process._
import scala.language.postfixOps


class StalkerListener extends StatusListener{
  override def onDeletionNotice(statusDeletionNotice :StatusDeletionNotice) ={
    // ツイートが削除された時に発動します
    // 今回は無視
  }

  override def onScrubGeo(userId :Long, upToStatusId :Long) ={
    // 今回は無視
  }

  override def onStatus(status :Status) ={
    // ツイートされた時に発動します
    val user = status.getUser()
    val file = new File(new File(".").getCanonicalPath, s"files/${user.getId}.txt")
    s"echo ${status.getText()}" #>> file !

    val medias = status.getMediaEntities().map(x => x.getMediaURL()).toList
    medias.zipWithIndex.foreach{case(x:String, i:Int) =>
      val stream = new URL(x).openStream
      val buf = Stream.continually(stream.read).takeWhile( -1 != ).map(_.byteValue).toArray
      val nameOnly = x.drop(x.lastIndexOf('/'))
      val fileName = nameOnly.split('.').mkString(i.toString ++ ".")
      val dir = (new File(".").getCanonicalPath).toString ++ s"/files/${user.getId}/"
      s"mkdir -p ${dir}"!
      val imageFile = new File(dir, s"${fileName}")
      val bw = new BufferedOutputStream(new FileOutputStream(imageFile))
      stream.close()
      bw.write(buf)
      bw.close
    }
  }

  override def onTrackLimitationNotice(numberOfLimitedStatuses :Int) ={
    // 今回は無視します
  }

  override def onException(e :Exception) ={
    // 例外が起こった場合に通知されます
    // 今回はスタックトレースでも出しておきます
    e.printStackTrace();
  }

  override def onStallWarning(e: StallWarning) = {
    // 変わったらしい
  }
}

streaming APIを使うときにはListenerというものが必要になるので、それの定義を行なっています。
監視しているstreamにツイートがされたときに、そのツイート引数としてonStatus関数が呼ばれます。
そのツイート主のIDを取ったtxtファイルを作成し、その中にツイートの内容をリダイレクトすることで書き込んでいます。
JavaのFileとかを使って追記しても良かったんですがめんどくさかったのでscala.sys.processを使って、リダイレクトして書き込んでいます。

後半部は画像ファイルを落として来る処理ですね。
動画が上がったときに関してはテストしていないので、どうなるのかわかりませんが、画像はいい感じに保存されるようになっています。

main.scala

さて、あとはstreamを監視するだけですね。

import java.io._

import twitter4j._
import scala.io._
import scala.sys.process._
import scala.language.postfixOps

import stream._
import connect._


object Main{
  def main(args: Array[String]) :Unit = {
    // val newName = ["hoge","fuga"]
    // val firstIds = TwitterConnector.getId(newName)

    val idFile = new File(new File(".").getCanonicalPath,"files/ids.txt")
    println(idFile)

    // firstIds.foreach{firstId =>
    //   s"echo ${firstId}" #>> idFile !
    // }

    val idSource = Source.fromFile(idFile.getPath())
    val ids = idSource.getLines.map{x => x.toLong}.toArray
    idSource.close

    val twitterStream : TwitterStream = new TwitterStreamFactory().getInstance();

    val listener = new StalkerListener()
    twitterStream.addListener(listener)
    val fq = new FilterQuery(ids: _*)
    println(ids.mkString(","))
    println("start Streaming")
    twitterStream.filter(fq)

  }
}

コメントアウトを全て外せば、newNameに含まれているscreenNameからIDを取得して、検索対象に含むようになります。
twitterStreamにはuser,filterなど複数あるのですが、今回はIDを指定してツイートを取得したいので、filterを用いています。
詳しくは公式のドキュメントを。

使い方

これで完成です。
あとはrootディレクトリにて
sbt run
しておくだけで、files/(id).txtにツイートが収集できます。
画像はfiles/images/(id)/以下に収集されます。
ただし、仕様上filterだと非鍵垢のツイートしか取得できないので、鍵垢に対してはuser streamを用いた上でonStatusではじくようにしたら良いのではないでしょうか。

最後に

これを用いた際に生じる不都合については一切責任は追いませんので、ご容赦ください。

それでは、楽しいネトストライフを!