web-dev-qa-db-ja.com

urllib.urlencodeはUnicode値を好みません:この回避策はどうですか?

次のようなオブジェクトがある場合:

_d = {'a':1, 'en': 'hello'}
_

...それから_urllib.urlencode_に渡すことができます、問題ありません:

_percent_escaped = urlencode(d)
print percent_escaped
_

しかし、unicode型の値を持つオブジェクトを渡そうとすると、ゲームオーバー:

_d2 = {'a':1, 'en': 'hello', 'pt': u'olá'}
percent_escaped = urlencode(d2)
print percent_escaped # This fails with a UnicodeEncodingError
_

したがって、私の質問は、urlencodeに渡されるオブジェクトを準備する信頼できる方法についてです。

オブジェクトを繰り返し処理し、文字列型またはユニコード型の値をエンコードするこの関数を思い付きました。

_def encode_object(object):
  for k,v in object.items():
    if type(v) in (str, unicode):
      object[k] = v.encode('utf-8')
  return object
_

これはうまくいくようです:

_d2 = {'a':1, 'en': 'hello', 'pt': u'olá'}
percent_escaped = urlencode(encode_object(d2))
print percent_escaped
_

そして、それは_a=1&en=hello&pt=%C3%B3la_を出力し、POST呼び出しまたは何でもに渡す準備ができています。

しかし、私の_encode_object_関数は、私には本当に不安定に見えます。一つには、ネストされたオブジェクトを処理しません。

もう1つは、ifステートメントについて緊張しています。他に考慮すべきタイプはありますか?

そして、このグッドプラクティスのようなネイティブオブジェクトと何かのtype()を比較していますか?

_type(v) in (str, unicode) # not so sure about this...
_

ありがとう!

48
user18015

本当に緊張するはずです。データ構造にバイトとテキストが混在している可能性があるという考え全体は恐ろしいものです。文字列データの操作の基本原則に違反します。入力時にデコードし、ユニコードでのみ動作し、出力時にエンコードします。

コメントへの応答で更新:

何らかのHTTPリクエストを出力しようとしています。これは、バイト文字列として準備する必要があります。 dictに序数が128以上のユニコード文字がある場合、urllib.urlencodeがそのバイト文字列を適切に準備できないという事実は、本当に残念です。 dictにバイト文字列とUnicode文字列が混在している場合は、注意する必要があります。 urlencode()の機能を調べてみましょう。

>>> import urllib
>>> tests = ['\x80', '\xe2\x82\xac', 1, '1', u'1', u'\x80', u'\u20ac']
>>> for test in tests:
...     print repr(test), repr(urllib.urlencode({'a':test}))
...
'\x80' 'a=%80'
'\xe2\x82\xac' 'a=%E2%82%AC'
1 'a=1'
'1' 'a=1'
u'1' 'a=1'
u'\x80'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "C:\python27\lib\urllib.py", line 1282, in urlencode
    v = quote_plus(str(v))
UnicodeEncodeError: 'ascii' codec can't encode character u'\x80' in position 0: ordinal not in range(128)

最後の2つのテストは、urlencode()の問題を示しています。では、strテストを見てみましょう。

混合物を使用する場合は、少なくともstrオブジェクトがUTF-8でエンコードされていることを確認する必要があります。

「\ x80」は疑わしい-any_valid_unicode_string.encode( 'utf8')の結果ではない。
'\ xe2\x82\xac'は問題ありません。 u '\ u20ac'.encode(' utf8 ')の結果です。
'1'は問題ありません-すべてのASCII文字はurlencode()への入力で問題ありません。必要に応じて '%'などのパーセントエンコードを行います。

推奨されるコンバーター関数は次のとおりです。入力辞書を変更するだけでなく、返すこともしません(あなたのように)。新しい辞書を返します。値がstrオブジェクトであるが、有効なUTF-8ストリングではない場合、例外を強制します。ちなみに、ネストされたオブジェクトを処理しないことに対する懸念は少し間違っています。コードは辞書でのみ動作し、ネストされた辞書の概念は実際には動作しません。

def encoded_dict(in_dict):
    out_dict = {}
    for k, v in in_dict.iteritems():
        if isinstance(v, unicode):
            v = v.encode('utf8')
        Elif isinstance(v, str):
            # Must be encoded in UTF-8
            v.decode('utf8')
        out_dict[k] = v
    return out_dict

そして、同じテストを逆の順序で使用した出力があります(今回は厄介なテストが先頭にあるため)。

>>> for test in tests[::-1]:
...     print repr(test), repr(urllib.urlencode(encoded_dict({'a':test})))
...
u'\u20ac' 'a=%E2%82%AC'
u'\x80' 'a=%C2%80'
u'1' 'a=1'
'1' 'a=1'
1 'a=1'
'\xe2\x82\xac' 'a=%E2%82%AC'
'\x80'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 8, in encoded_dict
  File "C:\python27\lib\encodings\utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode byte 0x80 in position 0: invalid start byte
>>>

それは役立ちますか?

66
John Machin

ドイツ語の「ウムラウト」でも同じ問題がありました。解決策は非常に簡単です。

Python 3+では、urlencodeでエンコードを指定できます。

from urllib import urlencode
args = {}
args = {'a':1, 'en': 'hello', 'pt': u'olá'}
urlencode(args, 'utf-8')

>>> 'a=1&en=hello&pt=ol%3F'
10
Saskia Vola

特に複雑な辞書の値を処理する必要がある場合、見た目よりも幅広いトピックのようです。問題を解決する3つの方法を見つけました。

  1. Urllib.pyにパッチを適用して、エンコードパラメーターを含めます。

    _def urlencode(query, doseq=0, encoding='ascii'):
    _

    すべてのstr(v)変換をv.encode(encoding)のようなものに置き換えます

    再配布が困難であり、保守がさらに難しいため、明らかに良くありません。

  2. デフォルトを変更Python説明通りのエンコード ここ 。 。それで私にも似合わない。

  3. だから、個人的には、すべてのUnicode文字列を(合理的に)複雑な構造のUTF-8バイト文字列にエンコードするこの忌まわしい結果になりました。

    _def encode_obj(in_obj):
    
        def encode_list(in_list):
            out_list = []
            for el in in_list:
                out_list.append(encode_obj(el))
            return out_list
    
        def encode_dict(in_dict):
            out_dict = {}
            for k, v in in_dict.iteritems():
                out_dict[k] = encode_obj(v)
            return out_dict
    
        if isinstance(in_obj, unicode):
            return in_obj.encode('utf-8')
        Elif isinstance(in_obj, list):
            return encode_list(in_obj)
        Elif isinstance(in_obj, Tuple):
            return Tuple(encode_list(in_obj))
        Elif isinstance(in_obj, dict):
            return encode_dict(in_obj)
    
        return in_obj
    _

    次のように使用できます:urllib.urlencode(encode_obj(complex_dictionary))

    キーもエンコードするには、_out_dict[k]_をout_dict[k.encode('utf-8')]に置き換えることができますが、私にとっては少なすぎました。

7
ogurets

Unicodeオブジェクトをurlencodeに渡すことはできないようですので、呼び出す前に、すべてのUnicodeオブジェクトパラメータをエンコードする必要があります。適切な方法でこれを行う方法は、コンテキストに非常に依存しているようですが、コードでは、Unicode python object(unicode表現))をいつ使用するかを常に意識する必要がありますエンコードされたオブジェクト(バイト文字列)を使用します。

また、str値のエンコードは「不要」です。 エンコード/デコードの違いは何ですか?

5
Javier

Urlencodeアルゴリズムはトリッキーではないことを指摘する以外に、追加する新しいものはありません。データを一度処理してからurlencodeを呼び出すのではなく、次のようなことをすればまったく問題ありません。

_from urllib import quote_plus

def urlencode_utf8(params):
    if hasattr(params, 'items'):
        params = params.items()
    return '&'.join(
        (quote_plus(k.encode('utf8'), safe='/') + '=' + quote_plus(v.encode('utf8'), safe='/')
            for k, v in params))
_

Urllibモジュール(Python 2.6)のソースコードを見ると、それらの実装はそれ以上のことはしていません。パラメーターの値自体が2タプルであるパラメーターの値が個別のキーと値のペアに変換されるオプション機能がありますが、これは便利な場合がありますが、必要ないことがわかっている場合は上記を実行します。

2タプルと辞書のリストを処理する必要がないことがわかっている場合は、if hasattr('items', params):を削除することもできます。

2
ejm

このadd_get_to_url()メソッドで解決しました:

import urllib

def add_get_to_url(url, get):
   return '%s?%s' % (url, urllib.urlencode(list(encode_dict_to_bytes(get))))

def encode_dict_to_bytes(query):
    if hasattr(query, 'items'):
        query=query.items()
    for key, value in query:
        yield (encode_value_to_bytes(key), encode_value_to_bytes(value))

def encode_value_to_bytes(value):
    if not isinstance(value, unicode):
        return str(value)
    return value.encode('utf8')

特徴:

  • 「get」は、dictまたは(key、value)ペアのリストです
  • 注文は失われません
  • 値は整数または他の単純なデータ型にすることができます。

フィードバックを歓迎します。

1
guettli