イノたまごラボ・あのぶる の「こんなの作ったよ!」

「イノたまごラボ」はひとり同人サークルのようなものです。今のところ同人誌は作っていませんが、ソフトウェアからイベントまで、心惹かれたものを細々と。

#rgjp10th を支える技術・ツイート収集バッチ実装編

この記事はRails Girls Japan Advent Calendar 2022の15日目の記事です。

qiita.com

前回の記事は13日目、yutokyokutyoさんの「Rails Girls Kagoshima 1stにコーチとして参加しました」でした。 先日のGatheringではお忙しい中スポンサーセッションありがとうございました!

yutokyokutyo.hatenablog.com

また、この記事はRails Girls Japan Advent Calendar 2022 1日目8日目の記事の続編となります。 もしかしたらそちらを先にご覧いただくとより楽しんでいただけるかもしれません。

自己紹介

だいたい仙台でソフトウェアエンジニアをしている あのぶる と申します。
Rails Girls関連のロールとしては以下のようなものを持っています。

  • Sendai 1st、2ndのオーガナイザー
  • Sendai, More!の運営
  • Rails Girls Japanメンバー
  • Rails Girls Gathering Japan 2022スタッフ

そのほかのRubyコミュニティ関連のロールとして、Sendai.rbのスタッフもしています。

前回の記事でようやく #rgjp10thお祝いツイートまとめが公式サイトに掲載されました🎉
ツイートの募集が本格的に始まってからちょっと時間が空いてからの実装だったので、最初のデータ投入時は手作業でツイートを集めていましたが、さすがにそれをずっと続けるわけにもいきません。

私も一応プログラマーなので怠惰にやりたいと思っていますし、何より手作業をしたとして、私が作業出来るのはせいぜい1日に1回程度。用事があったり体調が悪くてマシンに向かうことが出来なければ丸一日スキップせざるを得ません。せっかくツイートしてもらったものを瞬時に、とは言わないまでも、もうちょっと早く反映されて欲しいと思うのは人情と言うものです。このままでは気になって夜も寝られないかも。

ということで、このフェーズでやった作業は「Google Cloud Functionsでバッチの実装」と「実装したバッチの定期実行設定」の2つです。作業時間は多分4時間くらい。

実は打ち合わせのときに作業予定時間を聞かれて「まぁでもだいたい8時間もあれば出来るんじゃないですかね~」みたいなことを言ってたのに、全体としては結局微妙にオーバーしているっていう。*1

言わずと知れたGoogle Cloudのサービスなので誰かしら同じようなことはやっているはず!ということで、Google Cloud側の設定はこの記事を参考にしつつ進めていきました。

dev.classmethod.jp

Google Cloud Functionsでバッチの実装

先ほどの記事の前半部分を参考にしつつ、まずは関数(の設定)を作成していきます。
もしこの記事を参考にされる場合の注意なのですが、途中、プロジェクトの状態によってはいくつかAPIの有効化を求められるかもしれません。内容を確認して都度有効化していってください。

実は比較的最近、Cloud Functionsの第二世代の一般提供が開始されています。

cloud.google.com

参考記事と大きく異なる部分がこの世代選択と、それに伴うトリガーの設定手順。
今回だとどちらでも大差ないのかなと思いますが、せっかくなのでと(?)第二世代を選びました。ちなみにどちらを選んでも実行環境としてRuby3.0が使えますし、月200万回までの実行は無料枠内なので、関数1個だけなら毎秒実行にして3週間以上放っておくとかしなければ費用請求されないんじゃないかなと思います。
トリガーの設定方法はこの画像の「EVENTRACトリガーを追加」を選んでイベントプロバイダとしてCloud Pub/Subを選び、トピックを設定する、という流れになります。サービスアカウントへの権限付与を求められたら付与しちゃいます。

ところで設定をしていて「トピック」って何?って思うかもしれません。というか私が思ったのですが、今から作るバッチ(関数)とスケジューラを後で繋ぐのがこのトピックというもの。 この時点では名前を付けるだけで何もしませんが、一連の処理の目的みたいなのを名前として付けてあげると後から分かりやすいのかなと思いました。納得してみるとまぁ確かにトピックっていう他ないよねという感じではあります。でも慣れるまで難しいかも。

構成を設定したらランタイムをRuby3.0に設定して、Hello World状態ですが一旦設定を保存するためにデプロイしちゃうと落ち着いて実装できてよさそう。

今回は駆け足で作業したので省いてしまったものの、ちゃんと作業するならちゃんと開発環境は用意しておいた方がよいです。中の処理部分だけローカルのファイルにコピーして実行して、みたいなややこしいことをしていました。これが木こりのジレンマと言うやつですね。
もともとRubyの開発環境があるところからであれば、「Google Cloud CLI をインストールする」「Ruby 用 Cloud クライアント ライブラリをインストールします。」の二つだけやればよさそうです。

cloud.google.com

という言い訳たちは置いといて、実装上のポイントとしては以下のとおり。

タイムゾーンについて

ある意味当たり前だと思うけど、Cloud FunctionsのタイムゾーンUTCっぽい*2ので時間を扱う場合はそのつもりで処理する。

3時間おきに実行する前提のコードになっているんですが、これ、収集対象期間をペイロード(引数)で指定するべきなんですよね、本当は……

# Cloud Functions上ではどうやらUTCらしい
start_time = (Time.new - 60 * 60 * 3).strftime('%Y-%m-%dT%H')
end_time = (Time.new).strftime('%Y-%m-%dT%H')

ツイート検索時の時間指定もUTCなので変換を考えなくてよいのは大変ありがたい。

ツイート検索について

今回はこんな感じの検索クエリでツイートを取得しています。

query = "https://api.twitter.com/2/tweets/search/recent?query=%23rgjp10th+-is%3Aretweet&user.fields=username&start_time=#{start_time}%3A00%3A00.000Z&end_time=#{end_time}%3A00%3A00.009Z&expansions=author_id"

ポイントは以下のとおり。

  • ツイートは直近1週間までしか検索できないらしいので、実行頻度と合わせて注意する
  • -is:retweetを条件に入れておかないとリツイートされた回数だけリストに取れてくる
  • 設計編で書いた通りなのですがツイートURLそのものを取る方法はなく、user.fields=username expansions=author_id あたりを使ってユーザー名を取れるようにしておき、ツイートIDとユーザーIDとユーザー名から自力で組み立てる必要がある
    • ツイートのリスト内にユーザー名が入ってくるわけではなく、ユーザー情報が別のツリーで来るため、後続の処理でユーザーIDでユーザー名を引っ張れるMapHash*3を作る必要がある

Firebase Realtime Databaseへのデータ投入について

RubyでFirebase Realtime Databaseにアクセスする手順はこちらの記事を参考にしました。

bagelee.com

で、データの投入部分。

  index = length # lengthには現在のデータ件数が格納されている
  results[:data].reverse.each do |result|
    firebase.update("tweets", { index.to_s.intern => "https://twitter.com/#{users[result[:author_id]]}/status/#{result[:id]}" })
    index += 1
  end

usersというのがツイート検索クエリの方で出てきた、ユーザーIDでユーザー名を引っ張るためのMapHash。

こちらも公式サイト実装編で書いたとおり、Realtime Databaseの中身が配列と言うよりはキーに連番が振られたオブジェクトという感じで、ちゃんとソートさせるには少なくともnumericな昇順でキーを振っていかないといけないため、最初に現在のデータ件数を取得してそれをもとにIDを振るようにしています。

閑話・Google Cloudのリージョンについて

cloud.google.com

そういえば説明し忘れたなと思ったのですが、Google Cloudの各種サービスで日本国内のリージョンを選びたい場合はasia-northeast1(東京)とasia-northeast2(大阪)の二択になります。ちなみに上のドキュメントで何故か言及のないFirebase Realtime Databaseに関してはus-central1(アイオワ)・europe-west1(ベルギー)・asia-southeast1(シンガポール)の三択の模様。物理的な距離で言えばもちろんシンガポールが一番近いんですが、ネットワーク的にはどうなんでしょうね。

正直今回ならどこでも大差がなさそうで、何ならTwitterのデータを取るのでいっそus-west1(オレゴン)とかus-west2(ロサンゼルス)の方が都合良さそうな気もしましたが、まぁ好みで設定すればいいんじゃないかなと思います。

ザっと眺めた感じ、どこのリージョンにも必ずあるサービスというのはいくつかあるみたいですが、逆にサービス全部入り!という感じのリージョンがあるわけではなさそうです。 あのサービスは東~北東アジアだと東京リージョンにしかないけど、こっちのサービスは日本からの最寄り*4が台湾リージョンになる、みたいな。

実装したバッチの定期実行設定

Cloud Schedulerのコンソールを開き、先ほど関数とトピックを作ったプロジェクトになっていることを確認したら、こちらも参考記事を見ながらサクサクと進めていきます。ジョブは請求先アカウント単位で3つまで無料で使うことが出来ます。

今回はコードの方で3時間おきに実行するように書いていたのでその通りに設定しました。ちょうど例示されているのでそれをベースに、ただ、特に今回みたいに外部のサービスにアクセスするようなバッチの場合、あんまり正時*5に実行するのはお行儀が良くないとされている*6ので、3 */3 * * * みたいに少しずらすのがよさそうです。この設定だと3時間おきの3分に実行されます。設定を記述するとすぐ下に実行タイミングが表示されるので、ちょっと不思議な翻訳で若干混乱しますが意図した内容になっているかを確認します。

設定が完了すると最初のジョブ一覧の画面に戻ってきて、今作ったジョブが表示されました。 右端の「操作」の縦三点メニューからジョブを強制実行できるようになったので、実際に動かしてみるとよいです。

良さそうであればジョブ一覧の「次の実行」の日時が意図した時間になっていることを確認して、その時間までドキドキしながら待つだけです。お疲れさまでした!

まとめ

ということで、どうにか自動実行が出来るようになったので、無事に安心して寝られる日々を取り戻しました。それでも先週くらいまでは1日2~3回くらい公式サイトを巡回していましたし、純粋にツイートを読みたいというのもあるので今も定期的に見に行くようにはしてます。

最後に何度も申し訳ないのですが、収集バッチは今日も元気に動いていますので、もしまだでしたら#rgjp10th の記念ツイートもよろしくお願いします😊


明日は一緒にGatheringのスタッフをしたsiroemkさん🥰Instagramアカウント担当をはじめ、イベント運営お疲れさまでした!

siroemk.hatenablog.com

*1:4時間+公式サイト掲載がPR提出まで5時間で、合計9時間かかった

*2:JSTから9時間前にずれている

*3:JSとごっちゃになってましたすみません…

*4:物理的な

*5:各時間の0分ぴったり

*6:いわゆる「ごとう日」に銀行が混むのと似た理屈で、アクセスが集中しがちなため