it-roy-ru.com

Как получить строковые объекты вместо Unicode из JSON?

Я использую Python 2 для анализа JSON из ASCII кодированных текстовых файлов. 

При загрузке этих файлов с помощью json или simplejson все мои строковые значения преобразуются в объекты Unicode вместо строковых объектов. Проблема в том, что мне приходится использовать данные с некоторыми библиотеками, которые принимают только строковые объекты. Я не могу изменить библиотеки ни обновлять их.

Можно ли получить строковые объекты вместо Unicode?

Пример

>>> import json
>>> original_list = ['a', 'b']
>>> json_list = json.dumps(original_list)
>>> json_list
'["a", "b"]'
>>> new_list = json.loads(json_list)
>>> new_list
[u'a', u'b']  # I want these to be of type `str`, not `unicode`

Обновление

Этот вопрос был задан давно, когда я застрял с Python 2. На сегодняшний день одним простым и понятным решением является использование последней версии Python - т.е. Python 3 и более поздних версий.

255
Brutus

Решение с object_hook

import json

def json_load_byteified(file_handle):
    return _byteify(
        json.load(file_handle, object_hook=_byteify),
        ignore_dicts=True
    )

def json_loads_byteified(json_text):
    return _byteify(
        json.loads(json_text, object_hook=_byteify),
        ignore_dicts=True
    )

def _byteify(data, ignore_dicts = False):
    # if this is a unicode string, return its string representation
    if isinstance(data, unicode):
        return data.encode('utf-8')
    # if this is a list of values, return list of byteified values
    if isinstance(data, list):
        return [ _byteify(item, ignore_dicts=True) for item in data ]
    # if this is a dictionary, return dictionary of byteified keys and values
    # but only if we haven't already byteified it
    if isinstance(data, dict) and not ignore_dicts:
        return {
            _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True)
            for key, value in data.iteritems()
        }
    # if it's anything else, return it in its original form
    return data

Пример использования:

>>>json_loads_byteified('{"Hello": "World"}')
{'Hello': 'World'}
>>>json_loads_byteified('"I am a top-level string"')
'I am a top-level string'
>>>json_loads_byteified('7')
7
>>>json_loads_byteified('["I am inside a list"]')
['I am inside a list']
>>>json_loads_byteified('[[[[[[[["I am inside a big nest of lists"]]]]]]]]')
[[[[[[[['I am inside a big nest of lists']]]]]]]]
>>>json_loads_byteified('{"foo": "bar", "things": [7, {"qux": "baz", "moo": {"cow": ["milk"]}}]}')
{'things': [7, {'qux': 'baz', 'moo': {'cow': ['milk']}}], 'foo': 'bar'}
>>>json_load_byteified(open('somefile.json'))
{'more json': 'from a file'}

Как это работает и зачем мне его использовать?

Функция Марка Эмери короче и понятнее, чем эти, так какой в ​​них смысл? Почему вы хотите их использовать?

Чисто для performance. Ответ Марка полностью декодирует текст JSON сначала со строками Unicode, а затем повторяется по всему декодированному значению, чтобы преобразовать все строки в строки байтов. Это имеет пару нежелательных эффектов:

  • Копия всей декодированной структуры создается в памяти
  • Если ваш JSON-объект является действительно глубоко вложенным (500 уровней или более), то вы достигнете максимальной глубины рекурсии Python

Этот ответ устраняет обе эти проблемы с производительностью, используя параметр object_hookjson.load и json.loads. От документы :

object_hook - это необязательная функция, которая будет вызываться с результатом декодирования любого литерала объекта (a dict). Возвращаемое значение object_hook будет использоваться вместо dict. Эта функция может быть использована для реализации пользовательских декодеров

Поскольку словари, вложенные во многие уровни в других словарях, передаются в object_hookпо мере их декодирования, мы можем байтировать любые строки или списки внутри них на этом этапе и избежать необходимости глубокой рекурсии позже.

Ответ Марка не подходит для использования в качестве object_hook в том виде, как он есть, потому что он повторяется во вложенных словарях. Мы предотвращаем эту рекурсию в этом ответе с помощью параметра ignore_dicts для _byteify, который всегда передается ему кроме, когда object_hook передает ему новую dict для байтования. Флаг ignore_dicts указывает _byteify игнорировать dicts, поскольку они уже были байтизированы.

Наконец, наши реализации json_load_byteified и json_loads_byteified вызывают _byteifyignore_dicts=True) для результата, возвращаемого из json.load или json.loads, для обработки случая, когда декодируемый текст JSON не имеет dict на верхнем уровне.

89
Mirec Miskuf

Хотя здесь есть несколько хороших ответов, в итоге я использовал PyYAML для анализа моих файлов JSON, поскольку он дает ключи и значения в виде строк типа str вместо типа unicode. Поскольку JSON является подмножеством YAML, он прекрасно работает:

>>> import json
>>> import yaml
>>> list_org = ['a', 'b']
>>> list_dump = json.dumps(list_org)
>>> list_dump
'["a", "b"]'
>>> json.loads(list_dump)
[u'a', u'b']
>>> yaml.safe_load(list_dump)
['a', 'b']

Заметки

Некоторые вещи, чтобы отметить, хотя:

  • Я получаю строковые объекты, потому что все мои записи имеют кодировку ASCII. Если бы я использовал записи в кодировке Unicode, я бы возвратил их как объекты Unicode - преобразования не происходит!

  • Вы должны (вероятно, всегда) использовать функцию safe_load в PyYAML; если вы используете его для загрузки файлов JSON, вам все равно не понадобится «дополнительная мощность» функции load.

  • Если вам нужен анализатор YAML, который имеет большую поддержку спецификации версии 1.2 (и правильно анализирует очень низкие числа ), попробуйте Ruamel YAML : pip install ruamel.yaml и import ruamel.yaml as yaml - все, что мне было нужно в моих тестах.

Преобразование

Как сказано, конверсии нет! Если вы не можете быть уверены, что имеете дело только со значениями ASCII (и не можете быть уверены в этом большую часть времени), лучше использовать функцию преобразование:

Я использовал один из Mark Amery пару раз сейчас, он прекрасно работает и очень прост в использовании. Вместо этого вы также можете использовать функцию, аналогичную object_hook, поскольку это может повысить производительность больших файлов. Смотрите чуть более сложный ответ от Mirec Miskuf для этого.

169
Brutus

Нет встроенной опции, чтобы функции модуля json возвращали строки байтов вместо строк Юникода. Однако эта короткая и простая рекурсивная функция преобразует любой декодированный объект JSON из строк Unicode в строки байтов в кодировке UTF-8:

def byteify(input):
    if isinstance(input, dict):
        return {byteify(key): byteify(value)
                for key, value in input.iteritems()}
    Elif isinstance(input, list):
        return [byteify(element) for element in input]
    Elif isinstance(input, unicode):
        return input.encode('utf-8')
    else:
        return input

Просто вызовите это на выходе, который вы получаете от вызова json.load или json.loads.

Пара заметок:

  • Для поддержки Python 2.6 или более ранней версии замените return {byteify(key): byteify(value) for key, value in input.iteritems()} на return dict([(byteify(key), byteify(value)) for key, value in input.iteritems()]), так как понимание словаря не поддерживалось до Python 2.7.
  • Поскольку этот ответ повторяется по всему декодированному объекту, у него есть пара нежелательных характеристик производительности, которых можно избежать при очень осторожном использовании параметров object_hook или object_pairs_hook. ответ Мирека Мискуфа пока единственный, кому удается правильно это осуществить, хотя, как следствие, это значительно сложнее, чем мой подход.
139
Mark Amery

Вы можете использовать параметр object_hook для json.loads , чтобы передать конвертер. Вам не нужно делать преобразование после факта. Модуль json всегда будет передавать только дикты object_hook, и он будет рекурсивно передавать вложенные дикты, поэтому вам не нужно повторяться во вложенных диктах самостоятельно. Я не думаю, что я бы конвертировал строки Юникода в числа, как показывает Уэллс. Если это строка в кодировке Unicode, она указана в виде строки в файле JSON, поэтому предполагается, что это строка (или файл плохой).

Кроме того, я бы старался не делать что-то вроде str(val) на объекте unicode. Вы должны использовать value.encode(encoding) с правильной кодировкой, в зависимости от того, что ожидает ваша внешняя библиотека.

Так, например:

def _decode_list(data):
    rv = []
    for item in data:
        if isinstance(item, unicode):
            item = item.encode('utf-8')
        Elif isinstance(item, list):
            item = _decode_list(item)
        Elif isinstance(item, dict):
            item = _decode_dict(item)
        rv.append(item)
    return rv

def _decode_dict(data):
    rv = {}
    for key, value in data.iteritems():
        if isinstance(key, unicode):
            key = key.encode('utf-8')
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        Elif isinstance(value, list):
            value = _decode_list(value)
        Elif isinstance(value, dict):
            value = _decode_dict(value)
        rv[key] = value
    return rv

obj = json.loads(s, object_hook=_decode_dict)
73
Mike Brennan

Это потому, что у json нет никакой разницы между строковыми и юникодными объектами. Они все строки в JavaScript.

Я думаю JSON прав, чтобы возвращать объекты Unicode . На самом деле, я бы не стал соглашаться на меньшее, поскольку строки javascript фактически являются unicode объектами (то есть строки JSON (javascript) могут хранить любой тип символа Юникода), поэтому имеет смысл создавать объекты unicode при переводе строк из JSON. Простые строки просто не подходят, поскольку библиотека должна будет угадать, какую кодировку вы хотите.

Лучше использовать строковые объекты unicode везде. Поэтому лучше всего обновить библиотеки, чтобы они могли работать с объектами Unicode.

Но если вам действительно нужны строки байтов, просто закодируйте результаты в кодировку по вашему выбору:

>>> nl = json.loads(js)
>>> nl
[u'a', u'b']
>>> nl = [s.encode('utf-8') for s in nl]
>>> nl
['a', 'b']
37
nosklo

Существует легкий обходной путь.

TL; DR - использовать ast.literal_eval() вместо json.loads(). И ast, и json находятся в стандартной библиотеке.

Хотя это и не идеальный ответ, он довольно далеко уходит, если вы планируете полностью игнорировать Юникод. В Python 2.7

import json, ast
d = { 'field' : 'value' }
print "JSON Fail: ", json.loads(json.dumps(d))
print "AST Win:", ast.literal_eval(json.dumps(d))

дает:

JSON Fail:  {u'field': u'value'}
AST Win: {'field': 'value'}

Это становится более волосатым, когда некоторые объекты действительно являются строками Unicode. Полный ответ быстро становится волосатым.

15
Charles Merriam

ответ Майка Бреннана близок, но нет причин пересматривать всю структуру. Если вы используете параметр object_hook_pairs (Python 2.7+):

object_pairs_hook - это дополнительная функция, которая будет вызываться с результатом любого литерала объекта, декодированного упорядоченным списком пар. Возвращаемое значение object_pairs_hook будет использоваться вместо dict. Эта функция может использоваться для реализации пользовательских декодеров, которые полагаются на порядок декодирования пар ключ и значение (например, collections.OrderedDict запоминает порядок вставки). Если также задан object_hook, приоритет имеет object_pairs_hook.

С его помощью вы получаете каждый объект JSON, поэтому вы можете выполнять декодирование без необходимости рекурсии:

def deunicodify_hook(pairs):
    new_pairs = []
    for key, value in pairs:
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        if isinstance(key, unicode):
            key = key.encode('utf-8')
        new_pairs.append((key, value))
    return dict(new_pairs)

In [52]: open('test.json').read()
Out[52]: '{"1": "hello", "abc": [1, 2, 3], "def": {"hi": "mom"}, "boo": [1, "hi", "moo", {"5": "some"}]}'                                        

In [53]: json.load(open('test.json'))
Out[53]: 
{u'1': u'hello',
 u'abc': [1, 2, 3],
 u'boo': [1, u'hi', u'moo', {u'5': u'some'}],
 u'def': {u'hi': u'mom'}}

In [54]: json.load(open('test.json'), object_pairs_hook=deunicodify_hook)
Out[54]: 
{'1': 'hello',
 'abc': [1, 2, 3],
 'boo': [1, 'hi', 'moo', {'5': 'some'}],
 'def': {'hi': 'mom'}}

Обратите внимание, что мне никогда не придется вызывать ловушку рекурсивно, так как каждый объект будет передан ловушке, когда вы используете object_pairs_hook. Вы должны заботиться о списках, но, как видите, объект в списке будет правильно преобразован, и вам не нужно повторяться, чтобы это произошло.

Правка: сотрудник отметил, что Python2.6 не имеет object_hook_pairs. Вы все еще можете использовать это Python2.6, сделав очень небольшое изменение. В крюке выше, измените:

for key, value in pairs:

в

for key, value in pairs.iteritems():

Затем используйте object_hook вместо object_pairs_hook:

In [66]: json.load(open('test.json'), object_hook=deunicodify_hook)
Out[66]: 
{'1': 'hello',
 'abc': [1, 2, 3],
 'boo': [1, 'hi', 'moo', {'5': 'some'}],
 'def': {'hi': 'mom'}}

Использование object_pairs_hook приводит к тому, что для каждого объекта в объекте JSON создается один словарь, что может стоить того, если вы анализируете огромный документ.

10
Travis Jensen

Боюсь, что нет способа достичь этого автоматически в библиотеке simplejson.

Сканер и декодер в simplejson предназначены для вывода текста в Юникоде. Для этого в библиотеке используется функция с именем c_scanstring (если она доступна, для скорости) или py_scanstring, если версия C недоступна. Функция scanstring вызывается несколько раз почти каждой подпрограммой, используемой simplejson для декодирования структуры, которая может содержать текст. Вы должны либо monkeypatch значение scanstring в simplejson.decoder, либо подкласс JSONDecoder и предоставить практически всю свою реализацию всего, что может содержать текст.

Однако причина, по которой simplejson выводит Unicode, заключается в том, что в спецификации json конкретно упоминается, что «строка представляет собой набор из нуля или более символов Unicode» ... поддержка unicode предполагается как часть самого формата. Реализация Simplejson scanstring заходит так далеко, что сканирует и интерпретирует экранированные символы Юникода (даже проверку ошибок для искаженных многобайтовых представлений кодировки), поэтому единственный способ надежно вернуть вам значение - это использовать юникод.

Если у вас есть устаревшая библиотека, для которой требуется str, я рекомендую вам либо кропотливо искать во вложенной структуре данных после синтаксического анализа (что я признаю, это то, что вы явно сказали, что вы хотели избежать ... извините), или, возможно, обернуть ваши библиотеки каким-то образом фасада, где вы можете массировать входные параметры на более детальном уровне. Второй подход может быть более управляемым, чем первый, если ваши структуры данных действительно глубоко вложены.

9
Jarret Hardie

Как правильно замечает Марк (Амери): Использование десериализатора PyYaml в дампе json работает, только если у вас есть только ASCII. По крайней мере, из коробки. 

Два быстрых комментария к подходу PyYaml:

  1. НИКОГДА использовать yaml.load для данных из поля. Свойство (!) В yaml - выполнять произвольный код, скрытый внутри структуры. 

  2. Вы можете заставить это работать также для не ASCII через это:

    def to_utf8(loader, node):
        return loader.construct_scalar(node).encode('utf-8')
    yaml.add_constructor(u'tag:yaml.org,2002:str', to_utf8)
    

Но производительность не сравнится с ответом Марка Эмери:

Бросая некоторые глубоко вложенные образцы диктов на два метода, я получаю это (с dt [j] = временная дельта json.loads (json.dumps (m))):

     dt[yaml.safe_load(json.dumps(m))] =~ 100 * dt[j]
     dt[byteify recursion(Mark Amery)] =~   5 * dt[j]

Таким образом, десериализация, включая полное прохождение кодирования по дереву и, находится в пределах порядка реализации на основе языка j в Си. Я нахожу это удивительно быстрым и более надежным, чем нагрузка yaml в глубоко вложенных структурах. И менее подвержены ошибкам безопасности, глядя на yaml.load.

=> Хотя я был бы признателен за указатель на конвертер, основанный только на C, функция byteify должна быть ответом по умолчанию. 

Это особенно верно, если ваша структура json из поля, содержащего пользовательский ввод. Потому что тогда вам, вероятно, нужно пройти в любом случае по вашей структуре - независимо от ваших желаемых внутренних структур данных («сэндвич Юникод» или только байтовые строки).

Зачем?

Юникод нормализация. Для незнающих: возьмите обезболивающее и прочитайте это .

Таким образом, используя рекурсию byteify, вы убиваете двух зайцев одним выстрелом: 

  1. получить свои байтовые строки из вложенных JSON-дампов
  2. нормализовать вводимые пользователем значения, чтобы вы могли найти вещи в своем хранилище.

В моих тестах оказалось, что замена input.encode ('utf-8') на unicodedata.normalize ('NFC', input) .encode ('utf-8') была даже быстрее, чем без NFC - но это сильно зависит от данных выборки, я думаю.

4
Red Pill

Суть в том, что simplejson и json - это два разных модуля, по крайней мере в том, как они работают с юникодом. У вас есть json в py 2.6+, и это дает вам значения Юникода, тогда как simplejson возвращает строковые объекты. Просто попробуйте easy_install-ing simplejson в вашей среде и посмотрите, работает ли это. Это для меня.

3
ducu

Просто используйте pickle вместо json для dump и load, вот так:

    import json
    import pickle

    d = { 'field1': 'value1', 'field2': 2, }

    json.dump(d,open("testjson.txt","w"))

    print json.load(open("testjson.txt","r"))

    pickle.dump(d,open("testpickle.txt","w"))

    print pickle.load(open("testpickle.txt","r"))

Результатные данные: (строки и целые числа обрабатываются правильно):

    {u'field2': 2, u'field1': u'value1'}
    {'field2': 2, 'field1': 'value1'}
2
Stefan Gruenwald

Поддержка Python2 & 3 с помощью хука (от https://stackoverflow.com/a/33571117/558397 )

import requests
import six
from six import iteritems

requests.packages.urllib3.disable_warnings()  # @UndefinedVariable
r = requests.get("http://echo.jsontest.com/key/value/one/two/three", verify=False)

def _byteify(data):
    # if this is a unicode string, return its string representation
    if isinstance(data, six.string_types):
        return str(data.encode('utf-8').decode())

    # if this is a list of values, return list of byteified values
    if isinstance(data, list):
        return [ _byteify(item) for item in data ]

    # if this is a dictionary, return dictionary of byteified keys and values
    # but only if we haven't already byteified it
    if isinstance(data, dict):
        return {
            _byteify(key): _byteify(value) for key, value in iteritems(data)
        }
    # if it's anything else, return it in its original form
    return data

w = r.json(object_hook=_byteify)
print(w)

Возвращает:

 {'three': '', 'key': 'value', 'one': 'two'}
1
abarik

Итак, я столкнулся с той же проблемой. Угадайте, какой был первый результат Google.

Поскольку мне нужно передать все данные в PyGTK, строки Unicode для меня тоже не очень полезны. Так что у меня есть другой метод рекурсивного преобразования. На самом деле это также необходимо для безопасных типов JSON-преобразований - json.dump () будет вызывать любые не-литералы, такие как объекты Python. Не конвертирует индексы dict.

# removes any objects, turns unicode back into str
def filter_data(obj):
        if type(obj) in (int, float, str, bool):
                return obj
        Elif type(obj) == unicode:
                return str(obj)
        Elif type(obj) in (list, Tuple, set):
                obj = list(obj)
                for i,v in enumerate(obj):
                        obj[i] = filter_data(v)
        Elif type(obj) == dict:
                for i,v in obj.iteritems():
                        obj[i] = filter_data(v)
        else:
                print "invalid object in data, converting to string"
                obj = str(obj) 
        return obj
1
mario

Я переписал _parse_json () Уэллса для обработки случаев, когда сам объект json является массивом (мой вариант использования).

def _parseJSON(self, obj):
    if isinstance(obj, dict):
        newobj = {}
        for key, value in obj.iteritems():
            key = str(key)
            newobj[key] = self._parseJSON(value)
    Elif isinstance(obj, list):
        newobj = []
        for value in obj:
            newobj.append(self._parseJSON(value))
    Elif isinstance(obj, unicode):
        newobj = str(obj)
    else:
        newobj = obj
    return newobj
0
darnmarshall

Это поздно для игры, но я построил этот рекурсивный заклинатель. Это работает для моих нужд, и я думаю, что это относительно полно. Это может помочь вам.

def _parseJSON(self, obj):
    newobj = {}

    for key, value in obj.iteritems():
        key = str(key)

        if isinstance(value, dict):
            newobj[key] = self._parseJSON(value)
        Elif isinstance(value, list):
            if key not in newobj:
                newobj[key] = []
                for i in value:
                    newobj[key].append(self._parseJSON(i))
        Elif isinstance(value, unicode):
            val = str(value)
            if val.isdigit():
                val = int(val)
            else:
                try:
                    val = float(val)
                except ValueError:
                    val = str(val)
            newobj[key] = val

    return newobj

Просто передайте ему объект JSON следующим образом:

obj = json.loads(content, parse_float=float, parse_int=int)
obj = _parseJSON(obj)

Он у меня есть как закрытый член класса, но вы можете использовать метод по своему усмотрению.

0
Wells

У меня был JSON dict как строка. Ключи и значения были объектами Unicode, как в следующем примере:

myStringDict = "{u'key':u'value'}"

Я мог бы использовать предложенную выше функцию byteify, преобразовав строку в объект dict, используя функцию ast.literal_eval(myStringDict).

0
narko

Проверьте это ответ на подобный вопрос, который утверждает, что

Префикс u означает, что у вас есть строка Unicode. Когда вы действительно используете строку, она не появится в ваших данных. Не поддавайтесь распечатке.

Например, попробуйте это:

print mail_accounts[0]["i"]

Вы не увидите вас.

0
kunal

С Python 3.6 иногда я все еще сталкиваюсь с этой проблемой. Например, при получении ответа от API REST и загрузке текста ответа в JSON я по-прежнему получаю строки в кодировке Unicode . Нашел простое решение с помощью json.dumps ().

response_message = json.loads(json.dumps(response.text))
print(response_message)
0
Yuelin

вот рекурсивный кодировщик, написанный на C: https://github.com/axiros/nested_encode

Снижение производительности для «средних» структур около 10% по сравнению с json.loads.

python speed.py                                                                                            
  json loads            [0.16sec]: {u'a': [{u'b': [[1, 2, [u'\xd6ster..
  json loads + encoding [0.18sec]: {'a': [{'b': [[1, 2, ['\xc3\x96ster.
  time overhead in percent: 9%

используя эту тестовую структуру:

import json, nested_encode, time

s = """
{
  "firstName": "Jos\\u0301",
  "lastName": "Smith",
  "isAlive": true,
  "age": 25,
  "address": {
    "streetAddress": "21 2nd Street",
    "city": "\\u00d6sterreich",
    "state": "NY",
    "postalCode": "10021-3100"
  },
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "office",
      "number": "646 555-4567"
    }
  ],
  "children": [],
  "spouse": null,
  "a": [{"b": [[1, 2, ["\\u00d6sterreich"]]]}]
}
"""


t1 = time.time()
for i in xrange(10000):
    u = json.loads(s)
dt_json = time.time() - t1

t1 = time.time()
for i in xrange(10000):
    b = nested_encode.encode_nested(json.loads(s))
dt_json_enc = time.time() - t1

print "json loads            [%.2fsec]: %s..." % (dt_json, str(u)[:20])
print "json loads + encoding [%.2fsec]: %s..." % (dt_json_enc, str(b)[:20])

print "time overhead in percent: %i%%"  % (100 * (dt_json_enc - dt_json)/dt_json)
0
Red Pill