rauthでTwitterに画像投稿しようと思ったら大変だった話、……は入手したバージョンが古いのが原因だった

2015/08/12: 内容を修正しました

今ちょっとPython3でTwitter API叩くもの(ライブラリとかではない)を作っています。 Twitter APIPythonで叩くにあたり、自分はrauthというOAuthライブラリを使っています。

github.com

理由としては以下のQiitaの記事を読んだことが大きいです。 他にいくつかOAuthライブラリを見つけたのですが、一番書いてて気持ちよさそうだったのでこれにしました。

qiita.com

さて、上記の記事にあるように、テキストだけの投稿は簡単にできました。

が、画像投稿に苦労してしまったので、 ここではどういう問題が発生したかと、その対処について書いていきたいと思います。

初めに結論

以下長ったらしく記述していきますが、 基本的にはGitHubからrauth最新版をインストールしているという条件のもと、次の記述で画像の投稿が可能になります。

2015/08/12: コードの内容を修正しました

# 変数reqに適切なOAuth1Session型のインスタンスが入っていると仮定します

media = {"media": open(filePath, "rb").read()}
res = req.post(
    "https://upload.twitter.com/1.1/media/upload.json",
    files=media)

media_ids = [res.json()["media_id"]]

req.post(
    "https://api.twitter.com/1.1/statuses/update.json",
    data={"status": "Hoge Fuga", "media_ids": media_ids})

APIをどう呼ぶか

まずさしあたっての問題は、どう画像投稿のためのAPIを呼び出すか、ということでした。

2015/08/11現在、media/uploadというAPIを使って1つずつ画像をアップロードするという方法が主流です。 このAPIは例えばstatuses/updateとは異なり、multipart/form-dataでデータを送り、かつOAuthのSignatureの作り方も異なります。

ググってみると、rauthとは違いますが、同様にRequestsを利用しているライブラリでの呼び出し例が見つかりました。

qiita.com

とりあえずはこのような方法を試してみることにしました。 実際、基本的にはこの方法でよいことが分かりました(multipart/form-dataでデータをPOSTする場合のSignatureの作り方はTwitter特別という訳ではなく決まっているもよう[要検証])。

Python3のopenの返り値が違ってた

Python2を使ってる人は無視して結構です。

実際に上の記事と同様な呼び出しを行うと、見事にエラーが発生しました(エラーメッセージ取り忘れました)。

問題の原因は、Requestsのドキュメントにも書かれている、multipart/form-dataでデータを送信するための記述でした。 Requestsでは

files = {"file": open(filepath, "rb")}

のようなオブジェクトを用意し、postメソッドを呼び出す際に

req.post(url, files=files)

とすることでmultipart/form-dataでファイルの内容をバイナリで送信してくれます。

……、rauthではこの呼び出しをラップし、Signature生成のための処理などを挟んだあとに本物の(Requestsの)メソッドを呼び出すのですが、 その際にkwargsの内容をdeepcopyしています(Session.py 172行目付近)。

Python2はopenの結果はfileなので問題はないみたいなのですが、 Python3では結果が_io.BufferedReaderのインスタンスに変わっています。 そのためか、deepcopyの部分でエラーが発生してしまいます。

Requestsのコードを少し読んでみると、別にopenの戻り値をそのまま渡さなければいけない訳ではなく、読み込んだ結果のBytes型のインスタンスでもいいようなので、代わりに

files = {"file": open(filepath, "rb").read()}

とすることで問題を解決することができました。

しかし、まだ問題は続きます。

multipart/form-dataにならない

上記の対応で修正完了、かと思いきや、まだエラーが発生します。 次に出てきたエラーの発生箇所を見てみると、どうやらmultipart/form-dataとして送信するものだとみなされておらず、Signatureの計算をしようとして 存在しないキーにアクセスしようとするために発生しているようでした。

Requestsは名前付き引数filesにデータを設定してくれるじゃん! と思いながらも、ソースを読んでみると、自分でContent-Typeを設定するようにすればいいようなのでそれに従います。

res = req.post(
    "https://upload.twitter.com/1.1/media/upload.json",
    files=media,
    headers={"Content-Type": "multipart/form-data"})

これで問題が解決……、かと思いきや、まだエラーが消えません。

2015/08/12: 追記

のは、ライブラリが古いことが原因でした。 Content-Typeの指定は関係なく、ちゃんと最新版では引数filesにデータが指定されているかどうかで 判断してくれるようになっていました。

ここで、Content-Typeを指定してしまうと、あとでRequestsの方でリクエストデータを作成する際、 正しいmultipart/form-dataが作れないため、APIの呼び出しに失敗します。 そうです、boundaryがここに書いてないし、Requests側で更新もしてくれないので不正なmultipart/form-dataになってしまうんです。

あとでそのことに気付き、「なんで前はこれで動いたんだ?」と思ったら、

    headers={"Content-Type", "multipart/form-data"})

と「:」と「,」をミスタイプしてSet型になってました。 なので、当人はDict型として書いていたつもりで、たまたま正しく動いてしまっていたようです。

ライブラリが古い?

ソースを読んで記述を変えたのに、なんで!? もう最悪の手段だけど手元のライブラリの実体に手を加えて調査しないとダメか……、 と思ってソースを開いてビックリ。GitHubのコードと内容が違うじゃありませんか。

元々pipを使ってインストールしていたので、

$ pip install -U rauth

と入力してみましたが、すでに最新版のライブラリが入っているとのこと。なんで? GitHubの最終更新が4ヶ月くらい前だったけど、まだ反映されてないの?

ということで、仕方なくGitHubから最新版のライブラリをインストールするようにします。

$ pip install -U -e git+https://github.com/litl/rauth.git#egg=rauth

これを試したら無事画像がアップされました。

まとめ

rauthは確かに楽です。 ちょっとだけTwitterAPIを叩きたいときには便利です。 しかし、画像のアップは大変でした。主に自分の不注意で。

2015/08/12 に改めて間違った内容を修正させていただきました。 rauthでも簡単にできることがちゃんと判明しました(関係者のみなさま、すみませんでした)。 ただ、できればpipで導入できるバージョンを最新のものに、 そしてこのような場合の例をドキュメントに載せてほしいかなと思いました