Seleniumを用いてユーザの過去のほぼ全ツイートを取得する

# 3行要約
- :thinking:あるユーザの全ツイートが取得したい
- :scream:TwitterのAPIだと3200件しか取得できない
- :thumbsup:Seleniumで高度な検索を使ってがんばった取得した

# はじめに
あるユーザのツイートをすべて取得したいと思ったのですが、ツイッターのAPIを調べてみると直近約3200ツイートしか取得できないということらしいので、Seleniumを使ってがんばって（ほぼ）すべてのツイートを取得するプログラムを作ったのでその方法について書きたいと思います。

# どうやったか
簡単に説明するとTwitterの高度な検索を用いて日付区間を指定して過去のツイートを無理矢理Seleniumで取得しました。流れとしては次のようになります。

1. 指定した日付区間のツイートを検索
- WebのTwitterは下端までスクロールすると改めて次のツイートを読み込み直すのですべて読み込み終わるまで何度もスクロール
- ソースコードからツイートのIDを抜き出す
- ツイートIDからTwitterのAPIでツイートの詳細を取得する

## 環境
|環境    |使ったもの                  |
|----------|--------------------------|
|IDE    |IntelliJ                  |
|OS  |Windows10         |
|言語|Kotlin|
|Chrome|67|
|Seleniumライブラリ|org.seleniumhq.selenium:selenium-java:3.5.3|

## 指定した日付区間のツイートを検索
Twitterの[高度な検索](https://twitter.com/search-advanced?lang=ja)では過去のツイートも検索することができるようです。
まずは、ここに取得したいユーザと区間を指定して検索します。1ヶ月で5000ツイートしてる人などいれば、きっと読み込みきってくれないので適当な値を入れました。

例えば、 あるユーザhogehogeABCさんの2018-06-01～2018-06-09の検索を行うと、次のようになります。

```
https://twitter.com/search?f=tweets&vertical=default&q=from%3AhogehogeABC%20since%3A2018-06-01%20until%3A2018-06-09&src=typd&lang=ja
```

あとは、ScreenNameの部分と日付の部分を変数にすればよいです。

```kotlin
driver.get("https://twitter.com/search?f=tweets&q=from%3A${screenName}%20since%3A${startDate}%20until%3A${endDate}&src=typd&lang=ja")
```

## すべてのツイートをすべて読み込む
WebのTwitterでは図のように下の端まで行くと自動的に読み込が行われ次のツイートが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/240643/377e7593-2291-109c-174b-8646b398da77.png)

そのため、下の端に移動、すべて読み込みが完了したかチェック、完了済みでなければ再度スクロール、と繰り返します。

まず、下の端へのスクロールはENDキーを使用しました。スクロールには若干時間がかかり、その後の読み込みも発生するのでsleepを入れる必要があります。

```kotlin

Actions(driver).apply { sendKeys(Keys.END) }.perform()
Thread.sleep(2000)
```

次に、すべて読み込みが終わったかどうかの判定を行います。判定は、このアイコンが表示されたかどうかでできました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/240643/61f190c3-a3f2-15d5-fb22-6cef40722495.png)

判定メソッドは次のようにしました。XPathでエレメントを指定していますが、XPathはブラウザの開発者ツールでエレメントを指定して簡単にコピーできます。また、検索結果がない場合もあるので先にチェックする必要があります。

```kotlin
private fun isLoadingEnd(): Boolean {
    // 検索結果がない場合は読み込み終了とする
    if (driver.pageSource.contains("入力した単語の検索結果はありません。単語の入力を間違えたか、不適切と思われる内容が表示されない")) {
        return true
    }

    // ツイッターアイコンが表示されているかでロードの終了を判定する
    return try {
        val element = driver.findElement(By.ByXPath("//*[@id=\"timeline\"]/div/div[2]/div[1]/div/div[1]/div/span"))
        element.isDisplayed
    } catch (e: Exception) {
        false
    }
}
```

これらを用いて、スクロール、判定を繰り返します。

## ソースコードからツイートIDを抜き出す

これはソースコードの通りです。

```kotlin
private fun getTweetIds(): List<Long> {
    val html = driver.pageSource
    val ids = ArrayList<Long>()

    val matcher = Pattern.compile("data-item-id=\"([0-9]+)\" id=\"stream-item-tweet-").matcher(html)
    while (matcher.find()) {
        val id = matcher.group(1)
        ids.add(id.toLong())
    }

    return ids
}
```

## Twitter4Jを使ってIDからツイートを取得

Twitter4jの`Twitter`インスタンスから`status`を簡単に取得できます。ただ、制限にすぐに引っかかるので待機処理も必用です。

```kotlin
ids.forEach {
    val status = twitter.showStatus(it)
    val rateLimit = status.rateLimitStatus

    // リミット状況
    val hourlylimit = rateLimit.limit                       // リミット最大
    val limit = rateLimit.remaining                         // 残りリミット数
    val secondsUntilReset = rateLimit.secondsUntilReset     // リセットまでの時間[s]

    // 制限がきそうな場合、制限リセットまで待機
    if (limit < 10) {
        Thread.sleep(secondsUntilReset * 1000L)
    }
}
```

## 以上を繰り返す
以上の処理を日付を変更しつつ繰り返すとうまくいきました！
必用に応じてsleepなど入れると安定したりします。

# 結果
9年間13000ツイートの人を10日単位で取得してみたところ40分で12000ツイート取得できました。1000ツイートほど取りこぼしているようです。パラメータの調整が必要なのか、そもそもTwitter側が値を正しく返していないのかはわからないです。ツイート数って反映遅かったりしますもんね。でもAPIだと3200件しかとれないので大幅に取得数は向上できました！

# おわりに
下端に移動したときに追加の読み込みが発生したますがJavaScriptでイベント着火？させたり、そもそも読み込みのAPIを直接コールすればもっと効率的にできると思うのですが、どうやるかわからずこの方法をとりました。他にいい方法がありましたら教えてください。