urllib.urlencode не любит значения Unicode: как насчет этого обходного пути? - PullRequest
48 голосов
/ 26 июня 2011

Если у меня есть такой объект, как:

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 функция мне кажется просто шаткой. Во-первых, он не обрабатывает вложенные объекты.

Во-вторых, я нервничаю по поводу этого заявления if. Есть ли другие типы, которые я должен принимать во внимание?

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

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

Спасибо!

Ответы [ 8 ]

66 голосов
/ 26 июня 2011

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

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

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

Вот рекомендуемая функция преобразователя.Он не изменяет входные данные и не возвращает их (как у вас);это возвращает новый диктат.Это вызывает исключение, если значение является объектом 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
>>>

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

10 голосов
/ 21 апреля 2016

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

В 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'
7 голосов
/ 26 октября 2014

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

5 голосов
/ 26 июня 2011

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

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

2 голосов
/ 16 ноября 2011

Ничего нового, кроме как указать на то, что алгоритм 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):, если знаете, что вам не нужно обрабатывать списки из двух кортежей, а также диктов.

1 голос
/ 25 февраля 2016

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

Обратная связь приветствуется.

0 голосов
/ 11 июля 2017

эта строка работает нормально в моем случае ->

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

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

0 голосов
/ 27 мая 2012

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

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

...