ツイートを「大量に」収集する実験

# ツイートを「大量に」収集する実験

　2016/10なので、ざっくり1年半くらい前、「ツイートを長時間収集する実験」という記事で、お目汚しさせていただきました。その後、仕事の多忙と作業<sub>(隠語)</sub>の多忙等でこちらに経過を書くタイミングが無かったのですが、その後の経過についてざっくりと。

## 1年前からの経過

- Pythonで作ったプログラムで、TwitterのPublic Streamからデータを収集し、MongoDBに格納する(そのあとのことは後から考える)のが目的。
- 環境はDocker、Python3、MongoDB、[Tweepy](https://github.com/tweepy/tweepy)。
- 最初の投稿で目標としていた、91<sub>(定数)</sub>の想定期間、2016/10/28～2017/01/15 の間のツイート取得は、おおむね順調に動作。総ツイート取得数**279万件(RT等含む)**。
- トラブル等で取得できなかった時間は、トータル5時間程度、総稼働時間に対する不稼働時間の割合は0.2%。
- トラブルとしては、
  - ライブラリがNullオブジェクト送ってきて例外エラー(毎回自動復帰まで5秒かかる)
  - Twitter側の障害で15分ほど停止
  - 何の前触れもなくサーバ(として利用してるNAS)が完全にフリーズ(サーバ起動まで4時間)
  - 当日<sub>(隠語)</sub>を中心に、リミットオーバーで一部データがカットされる  
  など。
- 92<sub>(定数)</sub>でも同じように、当日<sub>(隠語)</sub>の2カ月前から、当月中まで、3カ月収集実施。ソフト側トラブルへの対処も功を奏し、ざっくり**300万件**取得に成功。

 集計したりなんだったりの結果は、盆暮れ<sub>(隠語)</sub>は買い物<sub>(穏当な表現)</sub>で忙しいため、池袋<sub>(隠語)</sub>でちょぼちょぼ頒布<sub>(専門用語)</sub>しておりました。まあ、道楽レベルなので赤字です。

## 不満点・問題点

 そんな感じで1年やってきましたが、2点ほど、現状では解決ができない不満や問題にぶち当たることになりました。

- 常時動作が必要なため、心理的な圧迫感が強い

　費用節約(というか、クラウドだとどれだけ金がかかるかわからない)の都合上、自宅のNASをサーバ扱いして稼働させていますが、**自宅の見えるところで絶対止められない機器が動いている**というのは、想像以上に心理的にクるものがあります。高機能NASなので、ソフトウェアのセキュリティアップデートもそこそこの頻度であるのですが、稼働させている3カ月間はそれの適用もままなりません。特に最初のころは、動き続けているか気になって夜も眠れないような状況でした。<sub>あと、夏場は部屋がクソ熱い。</sub>

- 想定よりもとれる量が少ない  
 **300万**というツイート数は、数だけ見ると相当なものですが、  
 - RTがざっくり**9割**
 - BOTによるSPAMツイートがやたら多い。**1アカウントで取得総ツイート数の0.1%占めたりする奴**がいたりする
 - Mecabでテキストを形態素解析して、特定のフレーズが含まれるツイートを当日<sub>(隠語)</sub>10分単位でに集計、とかやろうとすると、さらに対象が狭まる
 
　RTが9割なのは冬夏<sub>(隠語)</sub>両方とも同じだったため、この界隈<sub>(隠語)</sub>では普通の状態のようです[^1]。そうなると、30万件/3カ月、特にツイートが集中するのは3日間ですが、特定フレーズだけを集計するとか、そういうことを始めると、「ピーク時の特定フレーズつぶやきが10件」とか、本<sub>(隠語)</sub>のネタにしづらい状態になります。

　結局何が原因かというと、**元のスクリプトでstreaming API使ってるから**という結論に達します。

- streaming APIは、**実行している間**、リアルタイムに対象となるツイートがサーバから送られてきます。**取得期間中、ずっと動かすのは前提**なわけで、上記の項目1は、streaming APIを利用している限り、解決できない問題である、と言えます。

- 項目2の問題は、streming APIそのものの仕様に問題があります。  
streaming APIで取得できるキーワードは、**前後をスペースで区切られている必要がある**という仕様上の制限があります。
たとえば、「有明」<sub>(隠語)</sub>をキーワードにする場合、「今日は**有明**に行った」では拾ってもらえず、「今日は␣**有明**␣に行った」のように、キーワードが独立している必要がある、ということです。
これが英語とかならばスペースで分かたれているのがふつうなので問題はないのですが、日本語ではそうはいきません。当日<sub>(隠語)</sub>の現地<sub>(隠語)</sub>のようなあわただしさ<sub>(控えめな表現)</sub>ならば、そんな面倒なマネしながらツイートする人はさらに減るでしょう。

　これらの問題の解決のため、streaming APIではなくREST APIを利用する新しくスクリプトを組むことにしました。

## REST APIでツイート大量取得

　前置きが長くなりましたが、要するに今回やることは、**REST APIを使って、指定キーワードを含むツイートを、とにかくたくさん取得する**です。

　Twitterの"search/tweets"APIは、実行すると、過去方向に遡るようにして指定の検索条件のツイートを取得します。1回の実行で取得できるツイートの最大件数は100件です(執筆日現在)。
　そのため、
1. "search/tweets"APIでツイート取得実施
2. 取得したツイートのIDの中で、一番若いIDを保存
3. 次の"search/tweets"APIの条件に、「開始位置=(2)で取得したID - 1」を指定して取得実施
4. (2)に戻る。以下、取得件数0件になるか、前回実行時の開始ID(=今回の終了位置)まで取得を繰り返す
5. 実行の一番最初(1)で得られた一番新しいID(=次回取得実行時の終了位置)を保存してプログラム終了。

　というループで、取得を繰り返します。

![getting.png](https://qiita-image-store.s3.amazonaws.com/0/139416/ab1dc3c6-3d05-e37a-b933-3e5c994fa743.png)
イメージとしてはこんな感じ。

　なお、今回作ったプログラムの、中身整理した版をアップしてみました。
[https://github.com/yuhkan/restCrawler](https://github.com/yuhkan/restCrawler)

### 課題と対処

　上の手段で、おおむね1週間分くらいまでさかのぼってツイートを得ることができますが、課題は結構あります。

* APIの実行回数制限から来る、総取得量の制限

　一般的な認証を使う場合、"search/tweets"APIの実行回数は、15分毎に180回までとなっています。そのため、1日に取得できるツイート数の上限は(仮に何の障害もなかったとして)**1,728,000件/日**になります。

　普通の使い方をする分にはだいたい十分ではありますが、Stream APIでのツイート取得で、91<sub>(定数)</sub>実績で**最大600,000件/日**だったことを考えると、より多数のツイートの取得が見込まれるREST APIでの取得は、**たかが3倍程度**では怪しい可能性があります。

　仮定の話として、仮にRESTで取得されるツイート量がStreamで得られるツイート数の**10倍**とするとき、取得が必要な量に対して取得可能量が足りない状態に陥り、「欠測」が生じてしまいます。

![死のイメージ.png](https://qiita-image-store.s3.amazonaws.com/0/139416/57c9b5f7-a00c-b2e3-e0a7-0bf1f1862ff3.png)
実際の92<sub>(定数)</sub>における実施結果に基づいた計算過程。<sub>一部誤りがありますが、大意に影響は無いです。</sub>

　そこで今回、通常の認証方法に代えて**アプリケーション単独認証(Application-only authentication)**という手段を使うことにしました[^2]。
　アプリケーション単独認証は、ツイートやDMなどのいくつかの処理が行えない代わりに、いくつかのAPIの実行回数制限が緩和されています。

|       認証の方法     | search/tweets | Stream | ツイート、DM |
|:----|:----:|:----:|:--------:|
| 通常認証 | 180回/15分 | ○ | ○ |
| アプリケーション<br>単独認証|450回/15分|×|×|

　これにより、通常認証時の2.5倍、4,320,000件/日まで1日の取得可能件数を伸ばすことができます。Stream取得ピーク比**7.2倍**です。

　10倍にはやや届かないまでも、おおよそ安全圏と言える域になったような気がします。さらに万が一を考え、アプリを2つ登録し、いざというとき並行実行を行う体制を整えました[^3]。

* APIの時間ごとの実行回数制限

　上記のとおり、APIの実行回数は、15分毎に180 or 450回となっており、最初の実行から15分毎に回数がリセットされます。
回数を超えるとエラーが返り、度を越して繰り返されるとアカウントが凍結される……という噂。あまり良い話ではないので、防ぐようにしたいです。

最初の実装時には、最初の実行時間を保持しつつリミットまで取得したら回数リセットまでスリープ、という処理を入れていたのですが、後からチェックしていたら、利用しているライブラリのTweepyには、実行制限のリミット回避を自動で行うオプションがありました。

面倒くさいことは全部ライブラリに任せるのが正しいので、自力実装した回避処理はバッサリカット。……めんどくさかったのに……

* エラー処理

　データを取得してそのままMongoDBに格納するだけのプログラムのはずなのですが、Stream APIを使った旧版でも、なぜか妙なところでエラーが発生することがありました。
　Stream版では、エラーで終了したらそのまま再起動すれば良かったのですが、REST版の場合、エラーが発生すると中途半端に取得したデータが残されるという問題があります。

　時間の問題や技術(手腕)的なあれこれの問題もあり、今回は「**エラー終了したら、前回実施以降取得した分は全部破棄してやり直す**」方針をとることに。取得失敗したところから～とかやっていたら、面倒なことになっていたでしょう。なお、ライブラリ自体がリトライを行うオプションもありましたが、あまり信用できない感じでした。

　……ただし、最初のバージョンは巻き戻し処理を手動で行っていたため、妙なしくじりから「欠測」期間が発生してしまいました。

### 実験結果

　93<sub>(定数)</sub>のデータ取得期間を、2017/10/27～2018/01/15と定めて、stream版とREST番を両方とも実行し、イベント<sub>(隠語)</sub>に関連するキーワードを含むツイートの収集を行いました。

　多少の欠測や、期間前後の「先走り」「おつり」等の要素を含みますが、Stream版が「**3,140,382件**」、REST版が「**12,921,118件**」という結果になりました。取得量はざっくり**4.1倍**。10倍の想定は大げさではありましたが、それでもかなりの量を取得できてにっこりです。

### あとがき

　細かなトラブルもいろいろあり、一部は期間中書き直しながら実施したにもかかわらず、想定よりもかなり良い結果を得られたことに満足しています。

　今後(すでに？)出来るというTwitterの有料APIを使うと、1か月とか、Twitter開闢のときまで遡れるとかいう話のようですが、金のない自分としては現状の手段がそのまま使え続けることを祈るのみです。

　94<sub>(定数)</sub>でも同じように、stream版とREST版を同時に動かし、冬<sub>(隠語)</sub>と夏<sub>(隠語)</sub>とでどの程度の差が発生するかを見てみようと思います(既に実行開始してます)。

　<del>なお、取得して得られたデータの集計結果等について、この週末池袋<sub>(隠語)</sub>で頒布されてたりするので、暇な方は探しに来てください(何。</del>終了しました。

---
[^1]: 界隈に限った話だけでもなく、全日本的に見てもそんな感じ、という話を聞いたことがある。
[^2]: アプリケーション「限定」認証、と呼ばれる事もある。ここではひとまず「単独」認証とする。
[^3]: 年が明けてからの開発アカウント停止祭りの様相を見ると、やらずに済んで良かった、と思います。