文字コードの扱い方

現在App Engineの書籍をサンプルを作りながら学習しているのですが、文字コードの扱い方がいまいち分からないので、ちょっと調べてみた。

たとえばこんな感じで日本語の文字列をポストするフォームを作ったとします。


このサブミットを受け取るpostメソッドで、たとえばテキストボックスの値をGETメソッドのパラメータにしたい場合、たとえばこんな感じで実装したとします。

def post(self):
    url = '/sort2'
    charClass = self.request.get('charClass')
    level = self.request.get('level')
    if charClass != None and level != None:
        if level.isdigit():
            url += '?charClass=%s&level=%s' % (charClass, level)
    self.redirect(url)


これだと、デバッグサーバーではうまく行きますが、本番環境では最後のリダイレクトするところで例外が発生してしまいます。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 53-56: ordinal not in range(128)


なるほどURLエンコードしていなかったので、ASCII範囲外の値を渡してしまっていました。なので、次のようにURLエンコードして渡すようにします。

def post(self):
    url = '/sort2'
    charClass = self.request.get('charClass')
    level = self.request.get('level')
    if charClass != None and level != None:
        if level.isdigit():
            # 一度UTF-8にエンコードしてからURLエンコードする
            str = charClass.encode('utf-8')
            buf = urllib2.quote(str)
            url += '?charClass=%s&level=%s' % (buf, level)

    self.redirect(url)


わざわざcharClass (UNICODE) をUTF-8エンコードし、その上でURLエンコードしています。まぁ、ここまではいいとしましょう。

そして、このURLを受け取るGETメソッドで以下のように実装したとします。

def get(self):
    # URLデコードしてからUTF-8でデコードする (最終的にはUnicodeになる)
    charClass = self.request.GET.get('charClass')
    if charClass != None:
        charClass = urllib2.unquote(charClass).decode('utf-8')

    level = self.request.GET.get('level')
    if level != None:
        level = urllib2.unquote(level).decode('utf-8')

    # ... 省略 ...


POSTでUnicodeUTF-8エンコード→URLエンコードしたので、リダイレクト先であるGETではこの逆変換、すなわちURLデコード→UTF-8デコード→UNICODEしてやるのかと思いきや、このコードだと例外が発生します。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)


エラーメッセージから、どうも

.decode('utf-8')


の内部で例外が発生しているようでした。いろいろ試してみると次の実装だと例外が発生しません。

def get(self):
    charClass = self.request.GET.get('charClass')
    level = self.request.GET.get('level')

    # ... 省略 ...


つまり、GETではURLデコードも、UTF-8デコードもしない実装です。

???なんでこれでうまく行くの?

結果からのみ判断すると、App EngineのRequestHandlerはURLエンコードされたUTF-8文字列である引数を勝手に (?) UNICODE 文字列の引数に変換している、ということになります (間違ってる?) 。


じゃぁ、リダイレクトする時にUTF-8ではなく、EUC-JPにエンコードして渡してみるとどうなるでしょうか?

def post(self):
    url = '/sort2'
    charClass = self.request.get('charClass')
    level = self.request.get('level')
    if charClass != None and level != None:
        if level.isdigit():
            # 一度EUC-JPにエンコードしてからURLエンコードする
            str = charClass.encode('euc-jp')
            buf = urllib2.quote(str)
            url += '?charClass=%s&level=%s' % (buf, level)

    self.redirect(url)


GETメソッドでこのパラメータを取得すると見事に化け化けになります。UTF-8がデフォルトのようですね。でもここで以下のように実装すると

def get(self):
    charClass = self.request.GET.get('charClass')
    if charClass != None:
        charClass = urllib2.unquote(charClass).decode('euc-jp')

    level = self.request.GET.get('level')
    if level != None:
        level = urllib2.unquote(level).decode('euc-jp')


残念ながらdecodeメソッドで例外が発生します (デバッグサーバーでも) 。

UnicodeDecodeError: 'euc_jp' codec can't decode byte 0xbc in position 4: incomplete multibyte sequence


ということはもうUTF-8エンコードしか許されていないように感じられます。何だかすっきりしない結果になりました。