1. クライアントサイドで Canvas API - Web API | MDN 等を使って画像を処理する
  2. canvas.toDataURL()で BASE64 形式の画像データを取得
  3. このデータをサーバー側にPOSTして、サーバーで Twitter API を叩いて画像を添付したツイートをする

というようなシステムを作ろうとして、3の画像添付ツイートを行う部分で少し詰まったので、解決方法をまとめておきます。 なお、 Twitter の API key などを取得する部分は割愛させていただきます。

環境

  • Python 3.7
  • python-twitter 3.5

昔はtweepyをよく使っていたんですが、今回使おうとしたらドキュメントが古くて実態に即していない気がしたので、今回はpython-twitterを使ってみました。

bear/python-twitter: A Python wrapper around the Twitter API.

ドキュメントを確認してみる

python-twitterのドキュメントはこちらにあります。

import twitter
api = twitter.Api(consumer_key=[consumer key],
                  consumer_secret=[consumer secret],
                  access_token_key=[access token],
                  access_token_secret=[access token secret])

というようにしてインスタンスを取得してから、PostUpdateを参考にして

api.PostUpdate(status=[ツイート文面], media=[添付したい画像])

とすれば、画像を添付したツイートができそうな気がします。

ドキュメントによれば、PostUpdateのパラメータmedia

media (int, str, fp, optional) A URL, a local file, or a file-like object (something with a read() method), or a list of any combination of the above.

とあります。 通常の用途であれば画像ファイルへのパスを指定することが多いと思いますが、今回はクライアント側から BASE64 形式の画像データが送られてきた、というケースなので、画像ファイルパスを指定する方法ではダメです。 そこで、 file-like object (something with a read() method) に着目してみます。

file-like object って何?

Python の公式ドキュメントをあたってみます。

io --- ストリームを扱うコアツール — Python 3.7.3 ドキュメント

によれば、

I/O には主に3つの種類があります; テキスト I/O, バイナリ I/O, raw I/O です。(中略)これらのいずれかのカテゴリに属する具象オブジェクトは全て file object と呼ばれます。他によく使われる用語として ストリーム と file-like オブジェクト があります。

ということなので、この3つが file-like object と呼ばれているもののようです。 身近な例でいうと

f = open('hoge.txt', 'r')

としたときの ffile-like object ということですね。

どうすれば BASE64 データから file-like object を作れるの?

結論から説明すると

  1. BASE64 文字列をバイナリに変換する
  2. BASE64 バイナリをデコードする
  3. io.BytesIO() する

というような手順を踏みます。

1. BASE64 文字列をバイナリに変換する

クライアント側からは canvas.toDataURL() で出力された

data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBD(以下略)

という文字列が渡されていて、変数imageに入っているとします。 先頭についている data:image/jpeg;base64,は識別子のようなものでデータの内容には関係しないので、ここを取り除いた文字列をバイナリに変換します。

b64_encoded_binary = image.split(',')[1].encode()

2. BASE64 バイナリをデコードする

import base64
decoded_binary = base64.b64decode(b64_encoded_binary)

3. io.BytesIO() する

import io
f = io.BytesIO(decoded_binary)

これで f が画像データの file-like object となります。

まとめると

import base64
import io

b64_encoded_binary = image.split(',')[1].encode()
decoded_binary = base64.b64decode(b64_encoded_binary)
f = io.BytesIO(decoded_binary)

この fapi.PostUpdate に渡せばOK!!!! …………ではありません。もう一山ありました。

PostUpdatemedia パラメータに渡される file-like object は open() で開かれたものを前提としているっぽい

ここまでで作成した f を使って

api.PostUpdate(status=[ツイート文面], media=f)

として試してみると、エラーが出ると思います。ファイルモード(バイナリモード?テキストモード?)が無いよ〜とかのエラーです。

PostUpdate sourcePostUpdateのソースを確認したところ、どうやらPostUpdatemediaパラメータに渡される file-like object には

  • f.mode
  • f.name

の2つの属性がなければならないようです。 しかし、io.BytesIO()で作成したものにはこれらの属性がない、ということでエラーになっているらしい。

ないなら無理矢理つけちゃおうということで、

f.mode = 'rb'  # 読み込み専用のバイナリモードであるというように擬態する
f.name = 'hoge.jpg'  # 拡張子さえ合っていれば問題ないと思います

としてみました。これで画像添付ツイートができるようになりました!

結論

こう書けば動きます。

import twitter
import base64
import io

# image が BASE64 形式の画像データ
b64_encoded_binary = image.split(',')[1].encode()
decoded_binary = base64.b64decode(b64_encoded_binary)
f = io.BytesIO(decoded_binary)
f.mode = 'rb'  # 読み込み専用のバイナリモードであるというように擬態する
f.name = 'hoge.jpg'  # 拡張子さえ合っていれば問題ないと思います

api = twitter.Api(consumer_key=[consumer key],
                  consumer_secret=[consumer secret],
                  access_token_key=[access token],
                  access_token_secret=[access token secret])

api.PostUpdate(status=[ツイート文面], media=f)

軽く宣伝

このエントリで書いた内容は、一人開発RTAで僕が作った

出演者ヅラ - あなたも某サマーライブ2019の出演者として発表されてみませんか?

に使われています。クライアント側で Canvas を使用してフレームの画像を合成し、それを AWS Lambda に送ってツイートするという感じになっています。

もしよろしければ使って遊んでみてください。

元ネタ

「出演者ヅラ」を使ってみるとこうなります