Подтвердить что ты не робот

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.

Я придумал эту функцию, где я просто перебираю объект и кодирую значения типа string или unicode:

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 выглядит очень шаткой для меня. Во-первых, он не обрабатывает вложенные объекты.

С другой стороны, я нервничаю из-за этого заявления. Существуют ли какие-либо другие типы, которые я должен учитывать?

И сравнивает type() что-то с нативным объектом, как эта хорошая практика?

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

Спасибо!

4b9b3361

Ответ 1

Вы действительно должны нервничать. Вся идея, что у вас может быть смесь байтов и текста в некоторой структуре данных, ужасающая. Это нарушает основополагающий принцип работы со строковыми данными: декодирование во время ввода, работа исключительно в Юникоде, кодирование на выходе.

Обновление в ответ на комментарий:

Вы собираетесь вывести какой-то HTTP-запрос. Это нужно подготовить как строку байтов. Тот факт, что urllib.urlencode не способен правильно подготовить эту байтовую строку, если в вашем dict есть символы Unicode с порядковым номером = 128, действительно является неудачным. Если у вас есть смесь строк байтов и строк юникода в вашем dict, вы должны быть осторожны. Рассмотрим только то, что делает 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)

Последние два теста демонстрируют проблему с urlencode(). Теперь посмотрим на тесты str.

Если вы настаиваете на том, чтобы иметь смесь, вы должны, по крайней мере, обеспечить, чтобы объекты str были закодированы в UTF-8.

'\ x80' является подозрительным - это не результат any_valid_unicode_string.encode('utf8').
'\ xe2\x82\xac' в порядке; это результат u '\ u20ac'.encode(' utf8 ').
"1" в порядке - все символы ASCII в порядке, на входе в urlencode(), который будет, если необходимо, процентным кодированием, таким как "%".

Здесь предлагается предлагаемая функция преобразователя. Он не мутирует входной сигнал и не возвращает его (как это делает ваш); он возвращает новый dict. Он выдает исключение, если значение является объектом str, но не является допустимой строкой UTF-8. Кстати, ваша забота об этом, не обращаясь к вложенным объектам, немного неверно указана - ваш код работает только с dicts, а концепция вложенных dicts действительно не летает.

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
>>>

Помогает ли это?

Ответ 2

Похоже, что это более широкая тема, чем кажется, особенно когда вам приходится иметь дело с более сложными значениями словаря. Я нашел 3 способа решения проблемы:

  • Запустите urllib.py, чтобы включить параметр кодирования:

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

    и замените все преобразования str(v) на что-то вроде v.encode(encoding)

    Очевидно, что это не хорошо, поскольку он вряд ли распространяется и даже сложнее поддерживать.

  • Изменить кодировку Python по умолчанию, как описано здесь. Автор блога довольно четко описывает некоторые проблемы с этим решением, и кто знает, как больше из них может скрываться в тени. Так что это не выглядит хорошо для меня.

  • Итак, я лично попал в эту мерзость, которая кодирует все строки 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')], но для меня это было слишком много.

Ответ 3

У меня была такая же проблема с немецким "Умлаут". Решение довольно просто:

В 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'

Ответ 4

Кажется, что вы не можете передать объект Unicode в urlencode, поэтому перед его вызовом вы должны закодировать каждый параметр объекта юникода. Как вы делаете это правильно, мне кажется, что я очень зависим от контекста, но в вашем коде вы всегда должны знать, когда использовать юникодный объект python (представление юникода) и когда использовать закодированный объект (bytestring).

Кроме того, кодирование значений str является "лишним": В чем разница между кодированием/декодированием?

Ответ 5

Ничего нового для добавления, кроме как указать на то, что алгоритм 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-кортежами, превращаются в отдельные пары ключ-значение, что иногда полезно, но если вы знаете, что вам это не понадобится, это будет сделано выше.

Вы даже можете избавиться от if hasattr('items', params):, если знаете, что вам не нужно обрабатывать списки из 2-х кортежей, а также dicts.

Ответ 6

Я решил это с помощью этого метода 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 или список (ключ, значение) пары
  • Заказ не потерян
  • значения могут быть целыми или другими простыми типами данных.

Обратная связь.

Ответ 7

эта одна строка работает отлично в моем случае →

urllib.quote(unicode_string.encode('utf-8'))

спасибо @IanCleland и @PavelVlasov

Ответ 8

Почему так долго ответы?

urlencode(unicode_string.encode('utf-8'))