はじめに
これはあくあたん工房Advent Calendar 2018 の18日目の記事です. 僕はこれまでこのAdvent Calendarではずっとあくあたん工房の紹介をしてきました.少しでも興味があれば,他の記事も見て頂ければと思います.
さて,この記事は,
- 某botの開発の歴史: どういう経緯で作って,どんな技術を使ってきたのか
- 何がつらいのか: 開発面・運用面のつらい話
- これからどうしていくのか: 卒業までにやっておきたいと思っていること
の3本立てでお送りします.この記事を読んでいる弊学の人は一発でわかるであろう,某便利bot*1の作者は僕なのですが,色々悲惨でつらい感じなのでここで精算してスッキリしようという感じです.
某botの開発の歴史
そもそもbotを作ったきっかけ
- 大学の公式サービスが使いづらくて腹が立った
よくある話ですが,大抵大学のサービスっていうのはレガシーで,見づらくて,把握しづらくて,カオスです.
とにかくこういうのは待ってたって良くならないので,自分でなんとかするしか無いのです.というわけで作りました.
第一世代
Pythonを使用して作りました.当初の構成は簡単で,
- cronを利用して定期的にWebページをスクレイピング
- データベースで情報を管理
- 更新や新規情報の追加があればツイート
たったこれだけでした.この頃は確かまだPython 2.7を使ってRaspberry Piの上で動かしていて,ちょっと尖ったことと言えばSQLAlchemy*2を使ってSQLを書かずにやっていたことくらいでした.
やりたかったこととしては,
- 頻繁に追加される情報の取得
- 既存の情報の更新があった場合その差分の取得
- それらをツイートする
の3つなのですが,結局差分をとるところを実装しないまま終わってしまっています.
第二世代
まぁ人間しばらくすると満足いかなくなるもので,第一世代を運用していたときの僕は
など,たくさん不満を抱えていました.なのでしばらく後に現在稼働している第二世代を書きました.
特徴としては
- Python3に書き換えた
- 対象サイトへのログイン回りを自前で書き直した
mechanize
のPython3対応が見込めなかったため
- 全てDockerのエコシステムに載せた
- GitHubへpushしたらDocker HubでAutomated Build
- Dockerコンテナでデプロイ
- Fluentdにログを集約しSlackへ流すようにした
- UserStreamを監視してリアルタイムな返信機能を付加した*4
くらいで,コア部分はほぼ変更していません.エラーが出たときSlackからパッと見れるのは楽でした.ただ結局マイグレーションは手動という…
当然機能的にも大きく変わっておらず,ここでも更新分の差分をとっていません.なぜでしょうね.
さらに,まだまだ何も分かっていない初心者だった当時の自分の設計がかなり雑でもはや実装を読み解くのは容易ではなくなってしまいました. なぜか非同期処理にハマっていた節があり,どう考えても不要なところを非同期にしようとしていたりします.
完全に負の遺産ですね.
何がつらいのか
開発面のつらい話
対象サイト上のデータ
ここでは適当なデータを使います.分かる人は脳内で良い感じに補完してください.
まず,スクレイピング対象のデータ形式を示します.
科目名 | 担当者 | 曜日 | 時限 | 概要 | 詳細 | 掲載日 | 更新日 |
---|---|---|---|---|---|---|---|
講座A | A太郎 | 月 | 1 | 連絡 | 定規を持ってきてください | 2018/ 12/18 | 2018/ 12/18 |
講座B | B子 | 金 | 3 | 教室変更 | 001から002へ変更です | 2018/ 12/16 | 2018/ 12/18 |
細かい部分は省きましたが,概ねこんな感じのデータになっています.また,新しいデータの追加やデータの更新は以下の様な仕様で行われる事とします.
- 新規データはこのテーブルの一番上に追加される.
- このとき更新日と作成日は同じ値を取る
- 更新時には詳細の内容と更新日を登録する.掲載日や概要,科目の情報は変化しない.
- 同一科目の連絡が複数掲示されることもある
データの識別
データベースで管理するために.まずは各データを一意に識別する必要があります.つまり,どのデータがどのように更新されたかを識別するためには,更新の前と後でデータの対応付けができないといけないということです.
難しいのはこれらのデータそれぞれについて,実際の対象サイトが使用するデータベース上にあるはずの一意なidがどこにも示されていないため,データを識別することが容易でないということです.
そこで現在はこれらのデータを下記の様に2つのセクションに分けて考えています.
科目名 | 担当者 | 曜日 | 時限 | 概要 | 詳細 | 掲載日 | 更新日 |
---|---|---|---|---|---|---|---|
講座A | A太郎 | 月 | 1 | 連絡 | 定規を持ってきてください | 2018/ 12/18 | 2018/ 12/18 |
講座B | B子 | 金 | 3 | 教室変更 | 001から002へ変更です | 2018/ 12/16 | 2018/ 12/18 |
赤がデータの識別用,青はデータが更新されたかどうかを判別する用のセクションになります.授業の情報や初回の掲載日は変化しないと考えられるため,それらを使用して識別することにします.
これらのデータについて,各々の持つ情報を一つ一つ照合しても良いですが,ここではより簡単のため,これらのセクションをそれぞれ全て1つの文字列にまとめてハッシュ化して使用します.
同じ文字列からは常に同じハッシュが得られ,文字列が変化するとハッシュも変化するため,これらの値を比べることでデータの比較が可能になります.
実際に上記の講座Aの情報をハッシュにするとこうなります.
ハッシュ値 | |
---|---|
一意識別ハッシュ | 6351264e3a66aed80ccf31b3056a5d187d58f481 |
更新判別ハッシュ | e36c0ec693d6d6034f3e4cf1adec8736f6bece27 |
このハッシュ値を使用して以下の様な方法でデータベースの情報と照合し,更新をツイートすることにします.
更新検知の実際の流れ
さて,ハッシュを用いて簡単に判別することができることを示しましたが,これで本当にうまくいくのでしょうか?
実際には『同一の初回掲載日と概要を持つ同一科目の連絡が複数存在する場合』という悪夢のような状況が存在します(何を考えてるんだほんとに…).
この場合,同一の識別ハッシュを持つデータが複数存在することになり,どれを更新すべきなのかを判定できない状態になります.
当時のプログラム的には先に見つけた方を更新していたため,同一のデータが複数回更新され,毎回ツイートされるというバグが起きました.
これに対する良い解決手法を,僕は未だに思いついていません.そこでとんでもない雑手法によって現在は解決しています.
- 各データについて,最後にその情報を取得した日時を「データ取得日時」として記録する.
- データの更新が無かった場合にも,この「データ取得日時」だけは常に更新する
- ある識別ハッシュと一致するハッシュを持つデータの一覧を取得し以下のループを実行する
- 更新ハッシュを比較して同じ値なら「データ取得日時」を更新して終了
- そのデータの「データ取得日時」と現在時刻の差が5秒以内であれば何もせず次のループへ
- 1にも2にも該当しなければデータを更新しツイート
- 全データのうち「データ取得日時」が更新処理開始よりも前であるものは削除
5秒というのは何の根拠もなく適当に決めた値で,どう考えても実装が良くないなと思っています.
とはいえここしばらくはこれでうまくいっているようです.すごい.なんでや.
その他の考慮事項
他にもデータ処理の過程でいくつか対応を行っています.
- なぜか半角空白や全角空白,タブ文字が濫用されるので前後空白の除去と2つ以上の空白を1つの空白に置換すること
- なぜか半角カタカナや全角括弧が多用されるため,それらを正規化すること
- 詳細の内容にリンクが含まれることもあるため,それを取得すること
これを他のニュースだったり,休講だったりの内容にも適用しています.
運用面のつらい話
責任とコスト
サーバの運用費は自腹です.元々他の用途もあってサーバを稼動させているのでいいと言えばいいのですが,自分しか使っていないサービスならともかく,フォロワー数が4桁いるbotを止めるのは少し躊躇することもあり,サーバを気軽に止められないのが少しつらいです.
また,例えうまくいくことがわかっていても別のトラブルシュート*5にかかりきりになって移行が遅れたり,掃除してたらコンセント引っこ抜いちゃってサーバがダウンしたりするので*6,他人にも提供しているサービスを自宅で運用するのはあんまり嬉しくないです.
そもそも自分のために作っただけなんだから気にしなくてもいい,といえばそれはそうなんですが,現状を鑑みるにそれはあまりにも身勝手でしょう.経緯はどうあれ,それで知名度を上げようと画策したりもしたので,ある程度責任は持つつもりをしています.
モチベーションの低下
僕は今M1です.想定どおりに単位がとれれば来年からは授業がない(はず)なので,気になる情報は奨学金関連だけです*7.
正直言ってモチベーションは0です.お金にもならないですし,技術的に面白いことをちょっとやってみようかな,くらいの気持ちでどうにか前を向いています.
このbotのこれから
では最後に今後どうするかの話をして終わりにしたいと思います.
更新検知をやめる
開発のつらい話でも述べましたが,更新の検知をせず,全て新規情報が追加されたかどうかで判別すれば先に述べた問題は起きません.
2つに分けたりせず,全体でハッシュを取って識別すれば良いだけです.
内容の差分が見たかったため更新検知を目指していましたが,実際その機能がどれだけ必要かというと全くもって不要でしょう.
そもそもそこまでやっておいてdiff取る程度のことすらしていないことからも,いかに僕がその機能を必要としていないのかが窺い知れます.設計段階で気づいて欲しかった.
Pythonがつらいのでやめる
Pythonはすごく便利な言語ですが,素人のコードは絡まったミシン糸より厄介になりがちです.もう今となっては過去のコードを頑張って読み解く気力がありません.特に最近はDjangoばかり触っていたのでSQLAlchemyの使い方を忘れました.
また,Pythonは型の情報が得づらいことが読みづらいコードに拍車をかけており,最近ではtype hintsがちゃんと認知されてきましたが,当時の僕は中途半端にしか使えていません.
これらを踏まえ,Goで作り直そうと思っています.デプロイが容易であり,静的型と比較的厳格なformatterやlinterによって書くコードに一貫性を持たせることが出来ます.
結局頑張ってPythonでtype hintsを手動で付けるより,そもそも型のある言語でやったほうが良いのではないかというのが今のところの結論です.
オンプレがつらいのでやめる
Dockerコンテナ化して以降はそこまででもないですが,自宅サーバにデプロイするのはやはりあまり意味が無いなと考えていました.
最近サーバを入れ替えてGitlabのホストを辞めたこと,Jenkins等を建てたくないこと,sshのポートを開けるメリットが薄いので閉じていること,などの要因が重なりオンプレでは自動デプロイが面倒なので今後はクラウドでやりたいと思っています.
また,誰かがこれを必要としたときに簡単に使えるようになっていることが望ましいと思っているので,
- 登録・利用が簡単
- 料金体系が分かりやすい
- CI/CDが可能
- できるだけ無料でホストできる
- DBも使える*8
という要件を満たすherokuを検討しています.Google App EngineでGoを使うことも考えましたが,2nd generationへの移行がごちゃついていること,ロックインされがちであることから避けました.
新しい技術への挑戦
Goで書くとかクラウドに載せるとかではもうあんまりわくわくしないので,単なるbotではなくAPI化して,JSONではなくProtocol Bufferを使いたいと思っています.
ただherokuはgRPCに対応していない*9こと,要件的にも十分足るだろうという判断でtwirpの利用を検討しています.
が,研究と就活の狭間で身動きが取れない日々を送っているので後回しです…
一緒に開発やりたいっていう人を大募集中です.僕は一人ではもうモチベが持続しそうにありません.
卒業したら…
アカウントごと消すつもりをしています.不便になってもそれは僕のせいじゃなくて学務課のせいなので,「その公式っぽいTwitterアカウントは学祭の実況しかできないのか?」と偉い人のところへ乗り込んで説教してください.
まとめ
- クソサービスは待ってても良くならない.自分で改善していけ.
- クソサービスはデータ形式もクソ.学生に向かってなんだその半角カタカナは.
- Pythonを書く時はtype hintsを付けないと後々の自分が苦しむぞ
- Goはいいぞ
さて,あくあたん工房はこんな苦悩とは無縁の活動をしているので大変のびのびやっています.うちに入ったからこれを保守しないといけないなんていうことは全くないので,Golangをやりたい/やってる人,是非ご参加ください.Slackの #gophers
チャンネルがいつでもあなたをお待ちしています.
それでは.
*1:クソリプを飛ばしたりD進を煽ったりラーメンの判定をしない方のbotです
*3:毎回sshしてgit pullしてDBマイグレーションは自分でSQLを叩いていた.環境はpyenvで切っていたが,まだ使い方がよくわかっておらず何度もシステムのPythonから叩いて怒られた.
*4:なおこれはTwitterのUserStream廃止に伴って完全に死にました
*5:最近だとUEFI更新中に電源を落としてしまって起動すらできなくなったりしました
*6:全面的に僕が悪いので反省しています
*7:大学の就活情報をあてにしていないので…
*8:無料ではレコード数制限がありますが,削除されたものはDBから消していくようにすると1000レコードくらいで収まるのではないかと思います.