give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Vimコマンドを定期的に解説してくれるTwitterボットを作りました

はじめに

昨日、初めてBe VimmerというTwitterボットを開発しました。
このエントリではそのプログラムと制作過程を紹介しようと思います。

Be Vimmerとは?

定期的にVimコマンドとその説明をランダムにツイートするボットプログラムです。
日本語版、英語版、中国語版の3種類があります。


be_vimmer_jp


be_vimmer_en


be_vimmer_cn


情報源は各言語のVim Documentationから拝借しています。
例えば日本語版ではこちらのページです。


更新頻度は2012年4月15日の時点では2時間おきに3ツイートとなっています。
ただしこの頻度は今後様子を見ながら変えていくかもしれません。

プログラムの目的、および開発の動機

Vimのコマンドをたくさん覚えて立派なVimmerになりたい!と考えているプログラマがターゲットです。
自分から積極的に勉強しようと思わなくても、何気なくタイムラインを見ているとコマンドの説明が流れてきます。
それを見て「あっ、これは知らなかった。便利そう!」と新たな発見を得たり、手元のVimで実際にそのコマンドを試したりできます。


つまりVimコマンドを覚えるきっかけをプログラマ自身がPULLするのではなく、ボットがPUSHしてくれるイメージです。
ほとんどのコマンドを網羅しているので(1000件以上!)、中級者〜上級者でも知らないコマンドがきっとあるはずです。


ちなみにこれは以前書いた

10. 定期的に復習を繰り返す
チートシートやVim関連情報は定期的に(月1回とか)復習することをお勧めします。
Vimに慣れてきたとしても、いつのまにか自分の知っているコマンドだけで作業をしてしまう可能性があるためです。

僕がサクラエディタからVimに乗り換えるまで - give IT a try

というプラクティスを実践するための仕掛けでもあります。
つまり他人のためというより、まずは自分のためだったりするわけです(^^;


あと、このアイデアは日本人だけでなく世界中のプログラマにも役に立つんじゃないかと思い、他言語バージョンも作ろうと思いました。
普通は英語版を作っておしまいですが、中国語を勉強していることもあって面白半分に中国語版も作ってみました。

制作過程

だいたいこんな感じで作っていきました。

  1. 情報収集と大雑把な設計
  2. テキストデータを取り込みやすく整形する
  3. Railsプロジェクトの作成とデータの取り込み
  4. コマンド一覧画面の作成
  5. Cronプログラムの作成
  6. HerokuへのデプロイとSchedulerの登録
  7. プログラムの拡張(多言語化)
  8. リファクタリング、そして完成

以下はその詳細です。

1.情報収集と大雑把な設計

これまでに何度かRails + Herokuで簡単なアプリを作ったりしたことがあったので、今回も同じ組み合わせで行こうと思いました。
こちらのページ(大変お世話になりました)でほぼ同じようなことをされている方がいたので、「これなら大丈夫だろう」と確信しました。


また、データの量が多いのと、将来的にWebからデータを追加したり修正したりできることも想定して、データはデータベースに取り込むことにしました。
Railsだし、データはActiveRecordとして扱った方が便利です。

2. テキストデータを取り込みやすく整形する

情報源はネットにあるから、すぐ取り込めるわーと思っていたのですが、これがいきなり予想以上の大変さでした。
テキストファイルはこんな感じで保存されています。

==============================================================================
1. 挿入モード                                           *insert-index*

タグ            文字            挿入モードでの動作      ~
-----------------------------------------------------------------------
|i_CTRL-@|      CTRL-@          直前に挿入したテキストを挿入し、挿入モードを
                                終了する。
|i_CTRL-A|      CTRL-A          直前に挿入したテキストを挿入する。
                CTRL-B          未使用 |i_CTRL-B-gone|
|i_CTRL-C|      CTRL-C          'insertmode' がセットされていなければ、短縮入
                                力をチェックせずに挿入モードを終了する。
|i_CTRL-D|      CTRL-D          現在の行から shiftwidth 分のインデントを削除
                                する。
|i_CTRL-E|      CTRL-E          下の行のカーソルと同じ位置にある文字を挿入する
                CTRL-F          未使用 (しかし標準設定では'cinkeys'の設定によ
                                りカーソル行を再インデントするのに使用される)


CSVみたいな単純なフォーマットなら良かったんですが、80文字ぐらいで改行が入るし、各カラムも一見固定幅のように見えて、コマンドによっては幅が違うものもあったりして、簡単には整形できませんでした。
結局、正規表現を利用したアドホックなRubyスクリプトと手作業による補正を加えてようやく取り込みやすい形に整形できました。


整形後のテキスト

###挿入モード
CTRL-@          直前に挿入したテキストを挿入し、挿入モードを 終了する。
CTRL-A          直前に挿入したテキストを挿入する。
CTRL-B          未使用 |i_CTRL-B-gone|
CTRL-C          'insertmode' がセットされていなければ、短縮入 力をチェックせずに挿入モードを終了する。
CTRL-D          現在の行から shiftwidth 分のインデントを削除 する。
CTRL-E          下の行のカーソルと同じ位置にある文字を挿入する
CTRL-F          未使用 (しかし標準設定では'cinkeys'の設定によ りカーソル行を再インデントするのに使用される)


アドホックな整形用Rubyスクリプト

lines = []
File.open "en2.txt" do |file|
    while line = file.gets
      #if /^ {30,}/ =~ line
      #  last_line += " " + line.strip
      #else
      #  lines << last_line
      #  last_line = line.chomp
      #end
      lines << line.sub(/^ +|\|[^|]+\| +/, "")
    end
end

lines.each do |line| puts line end
3.Railsプロジェクトの作成とデータの取り込み

テキストをいい感じに整形できたら、あとはデータベースへの取り込みです。
新規にRailsプロジェクトを作成し、Modelをジェネレートして、db/seeds.rbに取り込み用のプログラムを書きます。
コマンド部分と説明部分をうまく分割するための正規表現の作成に少し時間がかかりましたが、それ以外は比較的簡単に終わりました。
あ、でも「# coding: utf-8」を忘れて、何度やってもシンタックスエラーが出るという初歩的なミスもやってましたね。。。

# coding: utf-8
@lines = <<-EOF.split "\n"
###挿入モード
CTRL-@          直前に挿入したテキストを挿入し、挿入モードを 終了する。
CTRL-A          直前に挿入したテキストを挿入する。
CTRL-B          未使用 |i_CTRL-B-gone|
###ノーマルモード
CTRL-@             未使用
CTRL-A          2  カーソル位置/カーソルより後ろにある数字に N を加える。
CTRL-B          1  ウィンドウを N 画面上へスクロール。
CTRL-C             現在の(検索)コマンドを中断する。
###ノーマルモード
g CTRL-A           MEM_PROFILEを定義してコンパイルしたときのみメモリプロファイルをダンプする
g CTRL-G           現在のカーソル位置に関する情報を表示。
g CTRL-H           セレクトモードで矩形選択を開始
EOF

@lines.each do |line|
  if /^###/ =~ line
    label = line.sub /###/, ""
    @mode = Mode.where(label: label).first_or_create!
  elsif m = /^(?<command>([\x21-\x7e]+ ?)+)(?<desc>.+)/.match(line)
    command = m["command"].strip
    desc = m["desc"].strip
    desc = desc.sub(/^\d +/, "")
    puts "Mode: #{@mode.label} Create Command:#{command} - #{desc}"
    @mode.vim_commands.create! command: command, description: desc
  end
end


ちなみにRails Projectの作成にはRails Wizardという外部サービスを利用しました。
ネット上で簡単に作成スクリプトをこしらえてくれるので、とっても便利です。

4.コマンド一覧画面の作成

ボットプログラムなので基本的にUIは不用なのですが、何かあった時はすぐデータが確認できるようにコマンド一覧画面を作りました。
また、手軽に「それっぽく」見せるためにTwitter Bootstrap for Railsを適用しました。
Bootstrapは簡単、便利で素敵ですね〜。まあ誰が作っても同じように見えてしまうという問題点もありますが・・・。


5.Cronプログラムの作成

続いて今回のメインプログロムであるCronプログラムの作成に入ります。
とはいえ、前述のページに詳しい作り方が書いてあったので、そちらをありがたく参考にさせてもらいました。

# encoding: UTF-8
require 'twitter'
require 'pit'
namespace :cron do
  desc "Random tweets"
  task :random_tweets => :environment do
    pit = Pit.get(
      'be_vimmer_jp',
      :require => {
        'twitter.consumer_key' => '',
        'twitter.consumer_secret' => '',
        'twitter.oauth_token' => '',
        'twitter.oauth_token_secret' => '',
    })
    Twitter.configure do |config|
      config.consumer_key       = pit["twitter.consumer_key"]
      config.consumer_secret    = pit["twitter.consumer_secret"]
      config.oauth_token        = pit["twitter.oauth_token"]
      config.oauth_token_secret = pit["twitter.oauth_token_secret"]
    end
    count = VimCommand.count
    3.times do 
      id = rand(count)
      command = VimCommand.find id
      tweet = build_tweet command
      puts tweet
      update tweet
    end
  end

  def build_tweet(command)
    "#{command.command}#{command.description} [#{command.mode.label}] #Vim"
  end

  def update(tweet)
    begin
      Twitter.update(tweet.chomp)
    rescue => ex
      p ex
    end
  end
end


ベタにConsumer Keyとかをプログラムに書いてしまうとGithubに上げるのが怖いので、Pitというライブラリを使ってConsumer Key等を管理しています。
Pitについて詳しく知りたい方はこちらのページをどうぞ。


あと、Twitterでアプリを登録したり、Consumer Key等を取得したりする場合は、ボット用のアカウントでログインするので注意が必要です。
さらに登録したアプリはツイートの読み込みだけでなく、書き込みもできるように設定しておく必要があります。こちらも要チェックです。


ついでに付け加えると、ボット用のアカウントを作成するとき、普段使っているアカウントと同じメールアドレスを使うと登録できません。
もしGmailを使っているなら手軽に受信専用のダミーアドレスを指定することができます。
たとえば普段使っているアドレスが「hoge@gmail.com」であれば、「hoge+piyo.fuga@gmail.com」のように「+(任意の文字列)」をアットマークの前に付けることができます。
このアドレスで登録しても、普段のGmailアカウントでメールを受信できます。

6.HerokuへのデプロイとSchedulerの登録

続いてHerokuへのデプロイです。
普通のRailsアプリなら何度かデプロイしたことがあるのですが、Cronの登録は初めてです。
そして案の定、ハマりました。(T T)
ローカルPCではPitがうまく使えていたのですが、Herokuでは使えませんでした。
というわけで、Herokuでは環境変数を使うようにプログラムを修正しました。

  pit = Pit.get(
    'be_vimmer_jp',
    :require => {
      'twitter.consumer_key'       => ENV["twitter.consumer_key"],
      'twitter.consumer_secret'    => ENV["twitter.consumer_secret"],
      'twitter.oauth_token'        => ENV["twitter.oauth_token"],
      'twitter.oauth_token_secret' => ENV["twitter.oauth_token_secret"],
  })
  pit["twitter.consumer_key"] ||= ENV["twitter.consumer_key"]
  pit["twitter.consumer_secret"] ||= ENV["twitter.consumer_secret"]
  pit["twitter.oauth_token"] ||= ENV["twitter.oauth_token"]
  pit["twitter.oauth_token_secret"] ||= ENV["twitter.oauth_token_secret"]
  p pit
  Twitter.configure do |config|
    config.consumer_key       = pit["twitter.consumer_key"]
    config.consumer_secret    = pit["twitter.consumer_secret"]
    config.oauth_token        = pit["twitter.oauth_token"]
    config.oauth_token_secret = pit["twitter.oauth_token_secret"]
  end

参考にしていた解説ページではHeroku Cronというサービスを使っていましたが、このサービスは近日中に廃止されるようです。
現在はHeroku Schedulerというサービスが提供されているのでそちらを利用します。
1時間単位のタスクも無料で登録できるので、絶対お得です。
ただし、タスクが月合計6時間以上稼働しているようだと課金されるらしいです。


さて、これで無事に日本語バージョンがリリースできました。やった!

7.プログラムの拡張(多言語化)

続いて英語版と中国語版を作っていきます。が、基本的な考え方、作成手順は同じです。
もちろんプロジェクトを丸ごとコピペして作ると全然DRYじゃなくなるので、日本語版を拡張して多言語対応させていきます。
今回はテーブルのカラムを増やしたり、パラメータを使ったりして、言語を切り替えられるようにしましたが、詳細に関する説明はここでは省略します。


あ、文章で説明する代わりにGithubのDiffを載せておくので、興味がある人はコードを読んでみてください。
日本語版と他言語版のDiff


はい、これで英語版と中国語版もできあがり!(笑)

8.リファクタリング、そして完成

最後にソースコードのリファクタリングを行います。
ソースコードを眺めて、気になった箇所を修正します。


できあがったソースコードはGithubに上げました。
https://github.com/JunichiIto/be_vimmer


リファクタリング前後のDiffだけを抜き出すとこんな感じです。
リファクタリング前後のDiff


あのー、ところですいません。ここで白状しますが今回はユニットテストを書いておりません・・・。
まあ実験がてら個人的に作ったプログラムだということで、お許しくださいm(_ _)m


で、Herokuにデプロイしたコマンド一覧画面はこちらで確認できます。
http://bevimmer.herokuapp.com/


ちなみにGithubには大量のソースコードが上がっていますが、まともに自分で書いたのはdb/seeds.rbやlib/tasksのrakeタスクぐらいで、大半はRailsがジェネレートしたコードです。
Railsやその他のフレームワークを多用すると、どこまでがその人が書いたコードで、どこまでがフレームワークの助けによるものなのかが、ちょっと分かりにくくなりますね。使い慣れていない人にとっては特に。

作成時間

作成にかかった時間はこんな感じです。

  • テキスト整形: 2h
  • 日本語版開発〜デプロイ: 4h
  • 英語版・中国語版開発: 4h
  • リファクタリング: 1h

合計11時間ぐらいですね。
作り慣れている人なら瞬殺レベルのプログラムかもしれませんが、初めてということで何卒ご容赦ください〜(^^;


でも、




って思ってから翌日にはできあがってた、って考えたらまあまあ優秀な方じゃないんでしょうかね?ね、ね???

開発費用

ゼロです。
Heroku最高!Twitterも最高!RubyもRailsもVimも、このアプリに関わったプロダクトのすべての関係者の方々に感謝感謝です!!

リリース後の反応

今のところ宣伝らしい宣伝はしていませんが、日本語版はリリースして1日で60人以上のフォロワーさんができました。


一方、英語版は3人で、中国語版はゼロです(- -;
でも英語版は「素晴らしい!」というフィードバックを頂きました〜。嬉しいですね。



さいごに

年始のブログで

でも2012年も新しく始まったことだし、今年はそうした「作品」を何か残せたらいいなと思います。

これまでに読んできた技術書を振り返る - give IT a try

と抱負を述べていたんですが、これでささやかながらも最初の一歩は踏み出せたかなと思っています。
アイデアと時間次第のところはありますが、今後も何か思いついたら第2、第3の作品を作ってみたいですね。

あわせて読みたい

僕が書いたVim関連のブログです。よかったらどうぞ。

僕がサクラエディタからVimに乗り換えるまで - give IT a try

社内でVimコマンド古今東西ゲームをやってみた - give IT a try


ついでに英語版のリリースノートも書いてみました。内容はあっさりですけどね。
A programmer in Japan: Released the Be Vimmer Twitter bot