NoiminのNoise

競技プログラミング (多め) とWeb (たまに) ,自然言語処理 (ブログではまだ)。

新AtCoder Performancesのグラフ付きツイート機能について (+おまけ)

この記事はAtCoder関連サービス Advent Calendar 2018 3日目の記事です.

adventar.org

開発秘話というほど大した話はありませんが,サービスの概要とグラフ付きツイート機能の実現方法について雑に述べます. 私が1人で開発した旧 AtCoder Performances とは異なり,新 AtCoder Performancesはfuurinくんとの共同開発なので,fuurinくんの話がめっちゃ出てきますが惚気話ではないのでご容赦ください. 書いてみたら言うほどfuurinくんの話をしていない

目次

AtCoder Performances とは

AtCoder Performances (新Ver.)

AtCoder のコンテストに出場するたびに各ユーザに付与されるコンテスト1回ごとの相対的な成績を表す数値,パフォーマンスをグラフ化したり,パフォーマンスの簡単な stats を計算できる web アプリケーションです.

去年の7月に旧 Ver. を公開して,今年の7月に新 Ver. を公開しました.

新旧の主な違いは図の太字部分です.

f:id:noimin:20181128162410p:plain

改修のきっかけ

実際に使っていただければ分かりますが,旧 AtCoder Performances は今の Performances に比べたらだいぶ機能が限られています.ライバルはたかだか1人までしか指定できませんし,ツイートも自分の成績の stats のみです.まあ実はシンプル故にデザインは旧 Ver. の方が気に入っていたりするのですが,それはまた別のお話.

旧 Ver. の地点で予想以上に多くの方に使っていただけたのは嬉しいのですが,Twitterを見ていると Performances のグラフをスクショしてツイートしてくださっている方々をちょくちょく見かけました.これは私が実装すべきでは,と思いつつもなかなか踏み切れずにいましたが,ある時fuurinくんと「Django 勉強したいよね〜」みたいな話になり,「だったら Performances を一緒に Django で作り直して,その上画像付きツイート機能を作ってしまえば私とfuurinくんとユーザさんでwin-win-winでは?」という結論になったので,Performances を Django で実装し直すことになりました.Django を選んだのは, Python のWebフレームワークで私が今まで使っていた Flask と比べられることが多く,どんなものなのか気になっていたから,くらいの理由です.

ディレクトリ構成

自分たちの手で作ったファイル・フォルダ以外はかなり省略がありますが,概ね次のような構成になっています.改修のきっかけにも書いた通り,言語は PythonフレームワークDjango です.

多分ごく普通の Django のプロジェクトです.アプリ名が atcoderperformances でプロジェクト名が atcoderapps になっているのは今後他の関連アプリを作るかもしれない……と想定してのことですが,今のところその予定はありません.

Performances 特有の部分を挙げるとすれば,atcoderperformances/utils の中にグラフ描画関連のソースコードをまとめて置いているところですかね.

atcoderapps
├── Procfile
├── atcoderapps
│   ├── __init__.py
│   ├── backends.py
│   ├── configs
│   │   └── twitter.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── atcoderperformances
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations
│   ├── models.py
│   ├── static
│   │   ├── admin
│   │   ├── atcoderperformances.css
│   │   ├── atcoderperformances.js
│   │   ├── bootstrap
│   │   ├── debug_toolbar
│   │   ├── images
│   │   │   ├── favicon.ico
│   │   │   ├── favicon.png
│   │   │   ├── twitter
│   │   │   │   └── tweet_button.png
│   │   │   ├── vs.ico
│   │   │   └── vs.png
│   │   ├── jquery
│   │   └── popper
│   ├── templates
│   │   └── atcoderperformances
│   │       ├── _base.html
│   │       ├── index.html
│   │       └── show_graph.html
│   ├── templatetags
│   │   ├── __init__.py
│   │   ├── lookup.py
│   │   ├── nan_to_no_data.py
│   │   └── performance_to_color.py
│   ├── tests.py
│   ├── tmp
│   ├── urls.py
│   ├── utils
│   │   ├── atcoder_figure.py
│   │   ├── atcoder_history.py
│   │   ├── base64_graph.py
│   │   └── error_reporter.py
│   └── views.py
├── db.sqlite3
├── manage.py
├── requirements.txt
└── runtime.txt

グラフ描画

旧 Ver. ではGoogle Chart APIによるグラフ描画をしていたのですがそれをやめて,サーバ内でグラフ描画のプログラムを動かしています.大まかな手順としては,

  1. フォームから受け取った AtCoder ID を元にhttps://beta.atcoder.jp/users/(username)/history/jsonからパフォーマンス情報を取ってくる
  2. matplotlib でパフォーマンス情報についてのグラフを描画する
  3. 描画したグラフを png にして,さらにそれを base64 形式に変換したものをコンテキストとして保持しておく
  4. テンプレートに画像を埋め込む.

のようになります.

1 については,旧 Ver. ではhttps://atcoder.jp/user/(username)スクレイピングして情報を取ってきていたのですが,chokudaiさんのツイートからjsonを直接取得できる機能 (すぬけさん作) があると知ったのでこの方法に変更しました.

2 では,matplotlib でふつーにグラフを作っています.「負のパフォーマンスを考慮しない」という初歩的ミスをだいぶ長い間やらかしていましたが (すみません) ,おかげさまで今では修正済みで,-4000以上のパフォーマンスを想定しています.他に工夫した点としては,y軸の範囲の決め方が挙げられます.その人が取ったことのあるパフォーマンスの範囲に合わせて見やすくしつつ,マーカーがグラフのふちギリギリに表示されて見づらくなるということがないように,パフォーマンス400ぶん以上の余裕を持ってy軸の範囲を決めています (AtCoderFigure クラスの update_y_limit)

gist.github.com

3 では,matplotlib で作成したグラフをhtmlに埋め込むため,png を中継して base64 形式に変換しています. base64 形式に変換するメリットとしては,ローカルにファイルを保存することなくコンテキストとして直接テンプレートに画像を渡せることが挙げられます.変換は,base64 モジュールで行うことができます.

gist.github.com

4 については,

<img src="data:image/png;base64,{{ base64 形式で画像を保持しているコンテキストでの変数名 }}" alt="画像の代替テキスト">

の形で テンプレートにbase64 形式の画像を埋め込むことができます.

画像付きツイート

テキストだけのツイートであればTwitter Publishでツイートボタンのスクリプトを生成して変数を埋め込めば良いです.例えば,テンプレートエンジンJinja2を使っているPerformancesでは

<a href="http://twitter.com/share?url=https://atcoderapps.herokuapp.com/atcoderperformances/&text={{ username }}%27s+Performances+%28{{ since }}%E3%80%9C{{ until }}%29+Average%3A+{{ row.1 }}+SD%3A+{{ row.2 }}+Max%3A+{{ row.3 }}+Min%3A+{{ row.4 }}&hashtags=AtCoder_Performances" title="Tweet your performances' statistics" target="_blank"><img src="{% static 'images/twitter/tweet_button.png' %}" width="20px" height="20px"></a>

のようにしてツイートボタンを設置しました.images/twitter/tweet_button.pngはツイートボタンの画像のパスです.

しかしちょっと難しいのが画像付きツイート機能.上記のスクリプトでは画像を投稿できないので,TwitterAPIを使って

  1. 画像を twitter に投稿し,media_id を得る
  2. media_id と関連づけて tweet を投稿

という手順を踏む必要があります.tweetと同時に新しい画像を関連づけて投稿するというのは残念ながら出来なさそうです.しかもこれらの操作を行うにはユーザのtwitterアカウントでログインしてもらう必要があります.

SNSサービスへのログイン・ログアウト関連はsocial_djangoがよしなにしてくれるので思ったよりは楽でした.social_django関連はKyou13さんのQiita記事を参考にしました.とてもわかりやすくまとまっていると思います.

今回の場合は,ツイートには requests_oauthlib モジュールの OAuth1Session を使ったので,1および2の前に次のようにしてセッションを定義します.

session = OAuth1Session(
            SOCIAL_AUTH_TWITTER_KEY,
            SOCIAL_AUTH_TWITTER_SECRET,
            user.access_token['oauth_token'],
            user.access_token['oauth_token_secret']
        )

1 は,pngの画像を一度ローカルに保存して,それをtwitterに投稿するようにしました.せっかくグラフの表示のときはローカルに保存しないで済んだのでこちらもローカルに保存せずにやりたかったのですが,まだ方法がわかってないです.png画像の投稿は,今回の場合は次のようにすれば良いです.

with open("画像ファイルのパス", "rb") as f:
    files = {"media" : f}
    media_request = session.post("https://upload.twitter.com/1.1/media/upload.json", files=files)

(参考: POST media/upload — Twitter Developers)

1 で正しく応答が返ってくれば,応答のjsonの中に media_id があるはずです.それを使って2 では画像付きツイートを投稿します.

params = {"status": "ツイートしたい文字列", "media_ids": [media_id]}
tweet_request = session.post("https://api.twitter.com/1.1/statuses/update.json", params=params)

(参考: POST statuses/update — Twitter Developers)

今回はツイートしたい文字列としてユーザ・ライバルの AtCoder ID や AtCoder Performances のURL,ハッシュタグを設定しました.ステータスコード200 (OK) の応答が返ってきたら無事ツイート完了です.ローカルに保存した画像はos.remove()などで削除しておきます.

twitter投稿関連のソースコード全体を見たい方はこちらをどうぞ.

おまけ: AWSから電話がかかってきた

AtCoder Performances では,静的ファイルのホスティングAWS S3を使用しています.

開発中に, AWS のアクセスキーが書いてあるファイルをパブリックなリポジトリに push してしまい,それに気づかずいたら AWS からメールが来ました.それも重要なメールだと気づかず放置していたら電話がかかってきました.もし電話がかかってこなかったらしばらく気づかないままだったに違いないので,本当に危なかった.そしてAWSの方すみませんでした🙇🙇🙇

英語で突然電話がかかって来た (ように見えた) ので,電話を受け取ったfuurinくんはすごく驚いた様子で私に連絡をよこしてきましたね…….(私だったらびっくりして要件を聞き取ることができるのかも怪しかったでしょう.) その後リポジトリをすぐに削除し,対策を打ってからまたリポジトリを作ったところ今度は感謝のメールが届きました.

Heroku の場合は Settings > Config Vars でアクセスキー等をサーバ上の環境変数として設定できます.アクセスキーやシークレットキーはべた書きせず,環境変数として設定しましょう. f:id:noimin:20181201230811p:plain ↑ Reveal Config Varsをクリックすると環境変数が編集・追加できます.

あとがき

開発したときのことを全く覚えておらず,リファクタリングをfuurinくんに丸投げしたこともあり思い出すのが大変でした.勝手に Advent Calendar に登録して勝手に記事を書いている私の質問に逐一答えてくれたfuurinくんに圧倒的感謝.

それと,最後まで書いてみて,Performances とか関係なく Python でグラフ画像を作って画像付きツイートをする方法を解説する記事とかにした方が良かったかもしれないと思いました. (需要があって気がむいたら書くかも)

AtCoder関連サービス Advent Calendar 2018,4日目はのみさんによる記事です."この日までに拡張機能を作りたい(願望)" とあります.どんな拡張機能なのかとっても気になります.

こんな記事ですが最後までご覧いただきありがとうございました.今後ともよろしくお願いいたします!