- 更新日: 2016年12月6日
- Ruby
twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー
Ruby ライブラリの twitter gem を使って、URLを開いて画像とテキストを同時に投稿するコードを書いていて遭遇したエラーが、表題の “The IO object for media must respond to to_io” です。gem twitter が投げる例外メッセージで、update_with_media の引数として渡すIOオブジェクトに to_io メソッドがない場合に発生します。
先日作っていた Twitter bot の続きの作業中でした。
RailsでTwitter botを作成 | EasyRamble
https://github.com/sferik/twitter
— 環境 —
rails 4.1
ruby 2.1.2
twitter 5.13.0
表題エラーが発生したコード(省略版)
当初以下のようなコードを書いていまして、media_url には画像ファイルのURLを渡す。
lib/tasks/twitter.rake
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
require 'twitter' require 'open-uri' begin client = get_twitter_client tweet = "Hello Twitter!" media_url = "http://example.com/img/photo.jpg" media = open(media_url) client.update_with_media(tweet, media) rescue => e Rails.logger.error "<<twitter.rake::tweet.update_with_media ERROR : #{e.message}>>" ensure media.close end |
この時、media_url の画像ファイルのサイズが10kb以下と小さい場合に、表題の例外エラーが発生します。
この理由は、ruby の open-uri の open メソッドが、対象ファイルが10kb以下の場合は Tempfile ではなくて StringIO のオブジェクトを返し、gem twitterの update_with_media が to_io メソッドを持たない StringIO オブジェクトを受け付けないため。
以下に情報があります。open-uri と twitter gem のソースも読んだけど、実際そうなってました。
開いたファイルオブジェクトは StringIO もしくは Tempfile ですが…
if size of image is less than 10kb then open(photo_url) will give you StringIO object, if size is more then 10kb then – File object which is saved in tmp/ folder. File object respond’s to to_io method and StringIO object doesn’t.
StringIO は IO オブジェクトのように扱える文字列。open-uri の open が10kb以下の場合に StringIO を返すのは、サイズが小さい場合はわざわざ tempfile としてファイルに書き出さずに、オンメモリで処理するためでしょうかね。
irb でも確認。
1 2 3 4 5 6 7 8 9 10 11 |
$ irb irb(main):001:0> require "tempfile" => true irb(main):002:0> require "stringio" => true irb(main):003:0> Tempfile.new("tmp").respond_to? :to_io => true irb(main):004:0> StringIO.new.respond_to? :to_io => false |
ということで、10kb以下の小さい画像ファイルは StringIO オブジェクトになるので、このままでは update_with_media で投稿できない。
StringIOの場合、Tempfileのオブジェクトを作成(上手くいかず)
そこで、StringIO じゃなくて Tempfile のオブジェクトを作成すれば良いだろうと考えました。
StringIO to Tempfile? – Ruby Forum
あたりを参考にして、open したファイルが StringIO オブジェクトで返った場合、Tempfile のオブジェクトを作成し、それを update_with_media に渡す以下のようなコード(省略版)を書いた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require 'twitter' require 'open-uri' begin client = get_twitter_client tweet = "Hello Twitter!" media_url = "http://example.com/img/photo.jpg" media = open(media_url) if media.is_a?(StringIO) ext = File.extname(media_url) name = File.basename(media_url, ext) tf = Tempfile.open([name, ext]) tf.binmode tf.write(media.read) client.update_with_media(tweet, tf) tf.close else client.update_with_media(tweet, media) media.close end rescue => e Rails.logger.error "<<twitter.rake::tweet.update_with_media ERROR : #{e.message}>>" end |
しかし tempfile の生成までは確認できたのですが、今度は次の例外エラーが発生して、またしても投稿できず…
1 2 3 |
<<twitter.rake::tweet.update_with_media ERROR : Net::ReadTimeout>> |
投稿の際に tempfile が読みこめないのだろうか… 原因はよく分からず、もう1つの代替案を思いついたので深追いはしませんでした。
【追記 2016/12/06】
rince さんから、
tf.write(media.read)
の後に
tf.rewind
を追加してあげるとうまく行きそうです。
というコメントを頂きました。情報ありがとうございます。tempfile を使う方法で上手くいかない場合は、ご参考にされてください。
【追記ここまで】
open-uri の OpenURI::Buffer::StringMax を変更するモンキーパッチ
open-uri のソースを読んだところ、OpenURI::Buffer::StringMax = 10240 と設定してあります。これが10kb以下の場合に、open が StringIO オブジェクトを返すための設定値、10240バイト = 10kb。
https://github.com/ruby/ruby/blob/trunk/lib/open-uri.rb
385行目辺り。その以降でサイズが StringMax を超える場合、tempfile オブジェクトを生成するようになってる。
ということで、以下を参考にして OpenURI::Buffer::StringMax の設定値を 0 に変更するモンキーパッチを行う。
モンキーパッチで OpenURI::Buffer::StringMax の設定値を変更したコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
require 'twitter' require 'open-uri' # The Twitter gem won't take StringIO so don't allow downloaded files to be created as StringIO. Force a tempfile to be created. OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax') OpenURI::Buffer.const_set 'StringMax', 0 begin client = get_twitter_client tweet = "Hello Twitter!" media_url = "http://example.com/img/photo.jpg" media = open(media_url) client.update_with_media(tweet, media) rescue => e Rails.logger.error "<<twitter.rake::tweet.update_with_media ERROR : #{e.message}>>" ensure media.close end |
このコードで無事に10kb以下の画像ファイルでも tempfile を生成するようにでき、update_with_media で投稿できました。
あまり乱用はしたくない方法ですけど、苦労した割には最終的な変更はモンキーパッチのための2行追加だけという…。プログラミングではまった時って、まあこんなもんです。まだまだRuby力が足りません。
- Ruby の関連記事
- Gemの作り方(Ruby Gem)
- ローカル開発中のgemをGemfileに書いてインストール
- 熊本地震の余震が夜に多いのは本当か?Rubyプログラムで検証してみた
- El Capitanでgemのnative extensionビルド失敗に対応
- Rubyで親クラスから子クラスの定数を参照
- MacabをRubyで使う
- rbenv/ruby-buildでRuby最新バージョンをインストール
- Rubyでクラスインスタンス変数にインスタンスメソッドからアクセス
- 距離1kmあたりの緯度・経度の度数を計算(日本・北緯35度)
- Google Maps Geocoding APIで住所から緯度・経度を取得するRubyコード
- 初回公開日: 2014年11月23日
- 2件のコメント
ちょうど同じ問題に引っかかって参考にさせていただきました。
ありがとうございます。
> StringIOの場合、Tempfileのオブジェクトを作成(上手くいかず)
tf.write(media.read)
の後に
tf.rewind
を追加してあげるとうまく行きそうです。
rince さん、コメントありがとうございます。
あ〜なるほどです。rewind 使うと良いのですね。情報ありがとうございます!