読者です 読者をやめる 読者になる 読者になる

読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

青空文庫の作品を定期的に分割配信するTwitter bot @novels_botを作りました。

Ruby

気になっていて、何度も挑戦したけどどうしても読み切れない小説とかありませんか?私も含めてそんな人のために、本を一段落ずつ毎日配信するTwitter botを作りました。まだα版で色々問題あるかと存じますが、是非、お試しいただいて、ご意見、ご感想を教えていただけたら幸いです。


きっと知識と経験のある人なら1時間ぐらいでできる内容かと思いますが、なにぶん不慣れなため3日かかりました。以下、制作の際の備忘録です。

プラットフォーム

例によってherokuを利用させていただきました。言語はRubyです。今回、ウェブではないので、Rails/Sinatraは使いませんでした。(データベース管理のためSinatraのrake部分だけ利用しました。)

普段の言語がJavaScript/CoffeeScriptになっているので、Node.js系に移りたいと思っているのですが、プラットフォームを乗り換えるのは言語変えるより敷居が高いですね。

制約

無料で使える書籍としてとりあえず青空文庫のテキストフォーマットに限定しました。具体的には、日本語エンコードシフトJISのzipファイルで、先頭行がタイトル、二行目が著者、その後、-------------…で表記コメントがくくられたファイルです。

ソフトウエアアーキテクチャ

アーキテクチャというほど大げさなプログラムではありませんが、一応。

  • 配信管理用データベース
    • 作品URL, 作品タイトル、配信しおり、配信先のスクリーンネーム、配信が終わったかどうかのフラグ、一式のテーブルです。
  • cron/scheduler配信プログラム
    • 登録された配信を行います。具体的にはURLが示すzipファイルを読み込んでしおりの位置から一段落分、メンションツイートします。
  • Twitter streaming APIをモニタし続けるworker
    • 登録、続きの即配信、配信停止のコマンドを受け付けます。

細かい習得技術

Twitterモニタリング

Twitter streaming APIは接続を維持するプッシュ型のAPIです。REST APIと違い回数制限がなく、ポーリングの必要もないので低負荷、低遅延のクライアントが設計できます。
Rubyではtweetstream gemが、twitter gem上に構築されていて開発アクティビティも高そうだったので、これを採用させていただきました。

日本語zipファイルの扱い

zipを扱うのに、zipruby gemを使いました。(rubyzipはJavaAPIという噂だったので。)zipファイルをブロック付けてopenすると、引数にar(Archive?)が渡されてこれにファイル名を渡してfopenすると中のファイルが読めます。

Ruby 1.9系ではopenする時にオプションで"r:Shift_JIS"という感じにエンコードを指定すると、その日本語エンコードでファイルを読んだ後、スクリプトエンコードに変換してくれます。ところが、上記のfopenではこれが使えませんでした。

なので、ファイルを全部読み込んだ後、ライブラリのkconvを使ってtoutf8で8ビット文字列をUTF-8に変換しました。

Railsなしでデータベースをどう使うか

ActiveRecordに少しは慣れているというかそれ以外には何も知らないので、ActiveRecordを使うことにしました。

herokuでは、Railsでない場合、Heroku Postgresというアドオンを用意してくれています。(以前はshared-databaseというアドオンがありましたが、これが置き換わったようです。)heroku createを実行したディレクトリで、

heroku addons:add heroku-postgresql

を実行すると、生成されたデータベース名(HEROKU_POSTGRESQL_XXXX)が通知され、アプリケーションで利用可能になります。データベース名は管理の際に必要です。

migrationするにもアプリで使うにもデータベースに接続する必要があります。herokuはconfig/database.ymlをユーザーが用意したgitの内容とは別に生成するので、データベースに接続する方法はローカルとherokuと異なります。四苦八苦して以下のようになりました。

if ENV['RAILS_ENV'] == 'production'
    require 'uri'

    db = URI.parse(ENV['DATABASE_URL'] || 'postgres://localhost/mydb')

    ActiveRecord::Base.establish_connection(
      :adapter  => db.scheme == 'postgres' ? 'postgresql' : db.scheme,
      :host     => db.host,
      :port     => db.port,
      :username => db.user,
      :password => db.password,
      :database => db.path[1..-1],
      :encoding => 'utf8'
    )
else
    require 'yaml'
    ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))[ENV['RAILS_ENV']])
end

RAILS_ENVなんて引きずったりして恥ずかしいです。直す予定です。


migrationについてはあれこれ検索してrequire 'sinatra/activerecord/rake'を使いました。本質的には、SinatraのFAQの内容なので、gemでsinatra-activerecordをbundleするより直接Rakefileに記述した方が筋がよかったと思っています。これも直す予定です。

Heroku Scheduler

https://devcenter.heroku.com/articles/scheduler
Heroku Schdulerはcronの代わりとなるアドオンです。コンソールから起動するコマンドを作って、ウェブでスケジュールを登録すると設定に合わせて定期的に実行されます。これで配信コマンドを定期的に実行しています。
heroku-postgresqlのアドオンの際には必要なかったのですが、このアドオンをインストールするにはクレジットカード登録が必要でした。(このアドオン自体は無料です。)

苦労したところ

herokuでのデータベースのmigrate

bundleした各gemのバージョンとローカルのgemのバージョンに不一致があって、rake db:migrateでうまく行くのにbundle exec db:migrateはうまく行かない状態でした。これをherokuでの問題と勘違いして、「herokuでdb:migrateできない」とえらく時間を取りました。

感想

プログラムの仕様をイメージした瞬間、実装可能なことは容易にわかるので、システムを使うための苦労がストレスでした。3日で済んでよかったです。ドツボにはまって4日、5日と経ったら投げ出していたと思います。慣れている人なら1〜2時間程度の内容でしょうか。
実装できたお陰で、色々拡張も思いつきます。英語化Gutenberg拡張とかタイトルからのURL検索機能とか、配信をさかのぼる「戻る」機能とか共有購読とか。評判が悪くなければぼちぼち拡張したいと思います。