Ruby
Twitter
TwitterAPI

Twitter Streaming APIのGET statuses/sampleをRubyのnet/httpsで取得する

More than 1 year has passed since last update.

最近は、隙あらばIOのことばかり考えている。2015年のシルバーウィークの間に何かやろうと思って、題材をTwitter Streaming APIとし(触るのは随分と久しぶりだ)、GET statuses/sampleを取得するRubyのプログラムを書いて動かした。

sample_stream.rb
require "uri"
require "oauth"
require "net/https"

class Connection
  def initialize(uri, consumer, access_token)
    @https = Net::HTTP.new(uri.host, uri.port)
    @https.use_ssl = true
    @https.verify_mode = OpenSSL::SSL::VERIFY_PEER
    @request = Net::HTTP::Get.new(
        uri.request_uri,
        "Accept-Encoding" => "identity")
    @request.oauth!(@https, consumer, access_token)
  end

  def start(&block)
    @https.start do |http|
      http.request(@request) do |response|
        response.read_body(&block)
      end
    end rescue self
    self
  end
end

uri = URI.parse("https://stream.twitter.com/1.1/statuses/sample.json")
consumer = OAuth::Consumer.new(
    "YOUR_CONSUMER_KEY",
    "YOUR_CONSUMER_SECRET",
    site: "https://twitter.com")
access_token = OAuth::AccessToken.new(
    consumer,
    "YOUR_ACCESS_TOKEN",
    "YOUR_ACCESS_TOKEN_SECRET")

while connection = Connection.new(uri, consumer, access_token)
  connection.start do |str|
    next unless str.bytesize > 0
    $stdout.write(str)
  end
end
hiragana_tweet.rb
require "json"
require "time"

$stdout.sync = true

$stdin.each do |line|
  str = line.chomp
  next unless str.length > 0
  begin
    obj = JSON.parse(str)
  rescue JSON::ParserError
    next
  end
  next unless text = obj["text"]
  next unless text[/\p{Hiragana}/]
  time = Time.parse(obj["created_at"]).getlocal
  puts text
  puts "[#{time}] #{obj["id"]}"
end
ruby sample_stream.rb | ruby hiragana_tweet.rb

JSONをHashに変換して、平仮名を含むツイートだけを出力するようにしている。平仮名を含んでいれば日本語だろう、ぐらいの考えで、そのようにした。

このストリームには、JSONの文字列がドバーッと流れてくる。ここで#read_bodyを使うのだけども、ブロック引数の文字列はJSONオブジェクトの単位であるわけではなく、JSONオブジェクトの途中でぶった切られていたりする。流れてくるデータを繋げてみると、JSONオブジェクトは改行区切りで送られてきていることがわかる。というわけで、まずは単に標準出力へ#writeするプログラムを作り、あとでパイプラインを繋ぐなり何なりすればよいだろうと考えた。$stdout.sync = trueとしたのは、出力をファイルにリダイレクトしたときに、流れをtail -fで見れて面白いと思ったからだ。上のようにシェルで実行すれば、実際にベルトコンベアのように動作する。パイプラインでなくても、IOにしてしまえばEnumerator::Lazyを使ってプログラムの中でも様々な処理を繋げるわけだし、とても便利なインターフェースだ。

Connectionクラスは、自分なりに考えたnet/http使用時の抽象化だ。車輪の再発明かもしれないが、net/httpを使うサンプルコードを調べる度に強烈なネストを含むプログラムを見せられるのには辟易気味なので、敢えて書いた。初期化時の設定については、いくらか調べた結果、このようにしている。Accept-Encodingあたりの理解は十分ではない。ここらへんについても、Rubyでは便利なgemが出ているので、それを使えばいいのだろうと思う。

理由はよくわからないが、何かのタイミングで認証が切れて、ストリームが終わることがあるようなので、そのつどwhileで接続を張り直している。ストリームが終わる直前には、JSONではなく、「認証されてないよ」という401な内容のHTMLが流れてくる1。もちろんJSON::ParserErrorになるので、nextしている。HTMLの断片を全てnextすると、接続は切れる。

こういったことを実現するための便利なgemはたくさんあるが、一度自分で書いてみたかったのだった。それに、この手のケースで散見する「gemを使えばサクッと書ける」「とりあえず動かしてみた」みたいな言説を、この期に及んでこれ以上、自分が繰り返す必要もないだろう。


  1. (追記)何も流れてこない場合もある。この場合、どうしてストリームが終了しているのか、よくわからない。