「幻水総選挙2016」の裏側

「幻水総選挙2016」

2016/06/17(金)~2016/06/18(土)に、「幻水総選挙2016」というファンイベントがTwitter上で開催されました(開票発表まで含めると2016/06/26(日)まで)。詳細は以下のアカウントと周辺情報をご覧ください。今回はこのイベントの裏側(主に技術的な面)について少し書きます。

環境

開発および運用の環境は以下のとおりです。

やること

やるべきことは以下の通りでした。それぞれを順を追って説明します。

  • ツイート(特定ハッシュタグ付きの投票ツイート)の収集
  • 収集したツイートの表示(Webページ)
  • 票数の集計
  • 結果発表ページの作成

ツイート(特定ハッシュタグ付きの投票ツイート)の収集

ツイートの収集には Twitter の API を利用しました。具体的には、PHP と cron を用いて一定間隔で API を叩き、戻ってきたツイートを DB(MySQL) に格納するという感じです。いくつか留意点を箇条書きで書きます。

  • API の実行回数上限には常に気をつけること
  • GET search/tweets では添付画像が2枚以上取得できないので、GET search/tweets はあくまでツイートの id を取得するために用いること
  • 時系列で漏れがないことを前提として、最新の取得済みツイートの id を別途ファイルなどに保存しておいて、それを利用して取得ツイートの範囲を絞ること(API の使用回数軽減)
  • ツイート本体の取得は、収集した id を基にして GET statuses/lookup で行うこと(添付画像が2枚以上でも取得できる)
  • ツイート本体の取得漏れを考慮し、定期的に GET search/tweets で取得した既存 id を基にしてツイートが取得できているかを確認すること
  • DB の構造は十分に考慮したうえで設計すること(特に「添付画像」のような、添字の上限がツイートによってまちまちのフィールドについて
  • (必要であれば)削除または非公開ツイートの判定を組み込むこと(定期的に回す)
  • (今回は不要だったが)RT数 や Like の数は動的に変わるので、必要ならば定期的に更新すること
  • (今回は未実装だが)ユーザーの情報(bio や アイコン画像 など)も動的に変わるので、必要ならば定期的に更新すること
  • id を GET search/tweets で別扱いで収集する方式にすれば、あとからいくらでもツイート本文を取得しなおしてリカバリできる。GET search/tweets では取得できるツイートが直近一週間までに限られる*1ため、とにかく id だけはとっとと収集しておくこと
  • GET search/tweets と GET statuses/lookup の API は上限回数がそれぞれ別々なので、とりあえずは GET search/tweets を上限に引っかからないように、漏れがないように確実に回すこと。後者は id さえあればいくらでも後からリカバリできる

収集したツイートの表示(Webページ)

一度 DB に保存してさえしまえば、それを Webページ に表示するのはほぼ Webデザイン の領域です(SQL を叩きまくるので SQL の技量ももちろん必要ですが)。ここでは特に「スマホファースト」を心がけました。PCでの見栄えよりもスマホでの見栄えを重視しました。

自分が得意でないということもあり、この点では何人かのテスターの方に見てもらって意見を頂き、修正を重ねました。

票数の集計

票数の集計は現状では基本的に「手動」です。すなわち、各々のツイートを見てそこに書いてある内容を人間の頭で認識し、Excelシート(スプレッドシート)に書き加えていく方式です。

これはある程度は仕方ない面もあります。なぜなら、投票にツイート(Twitter)を用いている以上、そこでの書式はあまり厳しく制限できないからです。封神総選挙ではある程度書式を統一しているようですが、投票全てがその書式に沿った投票になるとは限りません。その場合、書式を逸脱している投票を無効とするわけにもいかないでしょうから*2、はじめから書式を統一するのは意義が薄いということになります。

なので、手動集計は仕方がないととらえ、その上で効率よく集計する方法を考える必要があります。一案として、専用(に近い)アプリケーションソフトを開発し用いる、ということを考えましたが、当然ながら開発が間に合いません。なので、今回は数人で手動でがんばって集計をしました。

ただ、集計は投票時間を基準にして100ツイートずつの部分に分割し分担して行ったのですが、その部分を最終的に統合する箇所においては Google Apps Script*3 を使って効率化しました。同じキャラのセルの投票数を次々と合計していって、最終的にひとまとめにして出力するようなスクリプトになります。

結果発表ページの作成

結果発表のページも先ほどの「収集したツイートの表示(Webページ)」と同様、基本的にはWebデザインの範疇です。ただ、ここではさらに加速した「スマホファースト」のため(と、自分の経験のため)、「Bootstrap」というフレームワークを用いました。厳密には Bootstrap をもとにした、Honoka という、Bootstrap を基にしたテーマを用いました。

さらに「グラフ」の表示に Chart.js というライブラリ、「表」の表示に DataTables というライブラリを用いました。

レスポンシブ対応で鬼のように苦労しましたが、その分、得られた知識も少なくなかったと思います。

困ったこと

ツイートの収集を進めていくうちに、「ツイートをしたのにシステムに登録されていない(収集に失敗している)」という事例が発生しました。最初は API の回数上限に引っかかって本文取得ができないだけだと思い、それなら id は収集できているので、すぐにリカバリをすればいいだけの話です。

しかし事情は違いました。確かに所定のハッシュタグ付きでツイートしていて、公開アカウントであるにも関わらず、そのツイート(の id)が取得できていないのです。手動で取得しようとしましたが、取得できません(ひっかかりません)。

Twitter から返ってくる結果そのものから漏れていたのです。

私たちはとある大前提に立ってツイートを収集しています。それは「Twitter 側に検索で問い合わせ、返ってくる結果は、漏れがなく、全ての該当ツイートを網羅した完璧なものである」という前提です。これが崩れていました。

簡単にウェブで他事例を検索してみると、様々な憶測とともにこのような事例は散見されました。特定のユーザやツイートは検索に引っかからないように Twitter 側があえてそうしているとか、アカウントの公開と非公開の切替のタイミングや頻度によっても引っかからないとか、様々ありました。

ただ理由はどうあれ、検索結果を期待通りに戻してくれないことについてはどうしようもありません。これについては「自分のツイートが登録されているかの確認を促す」(ことを告知する)という方法を取りました。「DB に投稿が反映されているかどうかを検索できる Webページ」を予め作っておいたことが功を奏しました。

この「Twitter 検索の結果に漏れはない」という大前提は、その立場に立ってしまうと危険だと思います。特に収集の規模が大きくなればなるほど、「大前提」に反した事例が一定の頻度で出てきてしまうことは避けられないでしょう。となると対応策としてはやはり、既存の収集情報を検索できるシステム(ページ)を作ったり、あるいは定期的に収集状況を公開することではないかと思います。

大失敗

2016/06/17(金)の投票開始直前に、「リアルタイム集計所」で表示する全てのツイートが非公開になってしまうという重篤なバグが露見しました。このバグの原因は次のとおりです。

Twitter の API 実行回数上限に引っかかってしまった場合の例外処理を書いていなかった

どういうことかというと、順を追って説明します。

まず、DBの設計として、各々のツイートに対して「削除フラグ(厳密には非公開ツイートも含む)」というフィールドを作っていました。このフィールドに「1」が立っているツイートについては、ツイートが収集されていたとしても、Webページ上の表示対象にならないようにしていました。

そして、取得済みのツイートに対し、定期的に「削除チェック」を走らせていました。この「削除チェック」により、取得できなかったツイートについては、「削除フラグ」に「1」を立てるというものです。

「削除チェック」のアルゴリズムは次のとおりです。例えば、{1, 2, 3, 4, 5} に対して「削除チェック」を行ったとして、戻り値が {1, 3, 5} の場合は {2, 4} が削除されたとみなし、{2, 4} に「削除フラグ」を立てます。

ここで、API の上限に達している時は、戻り値が {}、すなわち「空」なのです。ゆえに、{1, 2, 3, 4, 5} に対し、存在しているツイートが「無し」ということになり、{1, 2, 3, 4, 5} の全てに「削除フラグ」が立ってしまうことになります(本来は存在しているツイートなのにも関わらず)。

投票開始前にこの「削除チェック」の実行頻度を高くしました(投票で一気にツイートが増えると考えられたため)。そのため、もの凄い勢いで API を叩き、結果 API の上限に達し、そして一気に削除フラグが全ツイートに立ちまくったということになります。

このバグのために、「リアルタイム集計所」は12時間近く(6/17の20:00~6/18の08:00)メンテナンスを行い、バグ発見と修正の対応に追われました。この時ほど冷や汗を感じ背筋が凍ったときはありませんでした。焦れば焦るほどバグの原因からは遠のき、時間だけが過ぎていく…まさに地獄の12時間でした。

イベントが終わって

色々なことがありましたが、つい先ほど投票結果の発表も終わり、「幻水総選挙2016」は幕を閉じました。

これだけ大きなイベントの根幹に関われたことを誇りに思いますし、得られるものも多かったです(バグはつらかったですが…)。こういう機会がないとなかなかできないような経験もあり、そういう点から考えると、少しでも自分の力が発揮できるならば積極的にイベントには関わっていったほうがいいと思います。得難いものが得られる数少ないチャンスだと思います。

来年以降も開催されるかどうかは現時点では分かりませんが、もし開催されるならばさらにブラッシュアップしたシステムを作って、いろいろなことを便利にし、いろいろなことに挑戦したいと考えています。

*1:"Also note that the search results at twitter.com may return historical results while the Search API usually only serves tweets from the past week."

*2:ここらへんは主催者の裁量に依存しますね

*3:Google スプレッドシート 版のマクロのようなもの

Powered by はてなブログ