Help us understand the problem. What is going on with this article?

pythonでGCSに置いた文字列をブラウザで見ると文字化けする

tl;dr

  • GCSはContent-Typeを指定できる
  • Chromeはtext/plainをShift-JISで表示しようとする
  • text/plain; charset=utf-8 が親切

現象

次のようなスクリプトでGoogle Cloud Storageにオブジェクトを作成します。
これはgoogleapis/python-storageのExample Usageのコードを、日本語を含む文字列を保存するよう変更したものです。
bucketの作成とか認証とかはここでは関係ないので飛ばします。

from google.cloud import storage
client = storage.Client()
bucket = client.get_bucket('bucket-id-here')
blob = bucket.get_blob('remote/path/to/file.json')
blob.upload_from_string('{"name": "日本語"}')

スクリプト実行後、オブジェクトが作成できたかブラウザで確認してみたいと思います。
スクリーンショット 2020-04-25 1.05.50.png

オブジェクトは無事作成されたようです。中身も見てみましょう。
スクリーンショット 2020-04-25 2.23.36.png

Google Cloud Storageは一時的なリンクを生成する機能があり、ブラウザからリンクを踏むとオブジェクトがダウンロードできます。

スクリーンショット 2020-04-25 1.07.30.png

オブジェクトの中身がこのように文字化けしてしまいました。
これが今回の問題です。

file.json
{"name": "譌・譛ャ隱�"}

調査

encode忘れ仮説

よくやるのが文字列のencode, decode忘れです。
upload_from_string にはstr型を渡していますが、UTF-8などにencodeしなくて良かったのかを調べます。

コードを見るとupload_from_stringがやっていることは単純で

抜粋
def upload_from_string(省略):
    data = _to_bytes(data, encoding="utf-8")
    string_buffer = BytesIO(data)
    self.upload_from_file(省略)
  1. データをUTF-8のバイト列にする
  2. 先のバイト列をファイルのように扱えるようにする
  3. upload_from_fileを呼ぶ

上記から、文字列のencodeは問題ないように見えます。

Content-Type仮説

ところで、ブラウザでオブジェクトの情報を見ていると気になる部分を見つけました。

スクリーンショット 2020-04-25 2.23.36.png

type="text/plain"

GCSではオブジェクトにメタデータ与えられます。
オブジェクトが呼ばれたときのResponse HeaderをContent-Typeメタデータで指定することができるようです。
https://cloud.google.com/storage/docs/metadata#content-type

デフォルトではapplication/octet-streamapplication/x-www-form-urlencoded になるはずですが、どうもこれが text/plainになっているようです。これが原因でしょうか?

実験

文字化けの原因は Content-Type: text/plain という仮説を立てたので、GCSと切り分けるために手元にサーバを立てて表示を確認してみます。
bottleでシンプルに文字列をContent-Type: text/plainで返すサーバを立てます。

server.py
from bottle import Bottle, HTTPResponse
import os

app = Bottle()

@app.route('/')
def serve():
    r = HTTPResponse(status=200, body='ほげ')
    r.set_header('Content-Type', 'text/plain')
    return r

if __name__ == '__main__':
    port = os.environ['PORT'] if 'PORT' in os.environ else '3000'
    app.run(host='0.0.0.0', port=port)

ブラウザで開きます

スクリーンショット 2020-04-25 2.28.16.png

再現しました。

ついでに、 application/json などにしてブラウザで開きます。

スクリーンショット 2020-04-25 2.34.56.png

application/json だと正しく表示されます。

上記よりGCSとは関係なく Content-Type: text/plain が文字化けの原因と考えて良さそうです。

どうしてContent-Type: text/plainになっているのか

GCSのデフォルトではapplication/octet-streamapplication/x-www-form-urlencoded になるはずの Content-Typeがどうして text/plainになっていたのかという疑問が残ります。
これは調べるとuploadに利用したモジュール google.cloud.storageBlob.upload_from_stringが悪さをしていて、

抜粋
    def upload_from_string(
        self,
        data,
        content_type="text/plain",
        client=None,
        predefined_acl=None,
        if_generation_match=None,
        if_generation_not_match=None,
        if_metageneration_match=None,
        if_metageneration_not_match=None,
    ):

content_typeのデフォルト引数が text/plain になっているので、特に指定しなければContent-Type: text/plainとなるように実装されています。

ブラウザの挙動

昔は見る側が文字エンコーディングを変更できたようなのですが今はブラウザの自動推論onlyのようです。

> document.characterSet
"Shift_JIS"

Shift_JISで表示しようとしてる

問題の大きさ

ここまでで、次の2点が文字化けの原因であることがわかりました。
- GCSがオブジェクトを返すときのResponse HeaderがContent-Type: text/plainとなっている
- Chromeブラウザでは Content-Type: text/plain をShift_JISで表示しようとして化ける

ブラウザで見る場合には文字化けするのですが、次のようにプログラムで処理する場合は問題になりません。

from google.cloud import storage
client = storage.Client()
bucket = client.get_bucket('bucket-id-here')
blob = bucket.get_blob('remote/path/to/file.txt')
print(blob.download_as_string())

私の場合保存したオブジェクトはどうせプログラムで読み出すので実は中身を手軽に見たいときにちょっと困る程度の問題でした。
静的ファイルホスティングとして利用している場合は問題になるかもしれない。

解決策

upload_from_stringを利用する場合、Content-typeを指定すると良いです。
jsonならapplication/jsonにしても良いですし、textならtext/plain; charset=utf-8のようにcharsetを指定してもChromeはutf-8で読んでくれます。

まとめ

最近めったに化けないので油断していました。
今回Lesson learnedはあまりなくて、Blob.upload_from_string使うときは気をつけましょう。

amuyikam
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした