Как получить строковые объекты вместо Unicode из JSON? - PullRequest
264 голосов
/ 05 июня 2009

Я использую 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 и более поздних версий.

Ответы [ 21 ]

175 голосов
/ 04 мая 2013

Хотя здесь есть несколько хороших ответов, я в итоге использовал 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, так как это может повысить производительность больших файлов. См. Чуть более сложный ответ от Мирека Мискуфа за это.

139 голосов
/ 28 октября 2012

Нет встроенной опции, чтобы функции модуля 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. Ответ Мирека Мискуфа пока единственный, кому удается правильно это осуществить, хотя, как следствие, это значительно сложнее, чем мой подход.
93 голосов
/ 06 ноября 2015

Решение с 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

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

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

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

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

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

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

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

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

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

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

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

74 голосов
/ 09 июля 2011

Вы можете использовать параметр 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)
37 голосов
/ 05 июня 2009

Это потому, что у 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']
14 голосов
/ 07 ноября 2013

Существует простой обходной путь.

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. Полный ответ быстро становится волосатым.

11 голосов
/ 14 января 2016

Ответ Майка Бреннана близок, но нет причин пересматривать всю структуру. Если вы используете параметр 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, что может стоить того, если вы анализируете огромный документ.

9 голосов
/ 05 июня 2009

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

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

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

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

4 голосов
/ 14 апреля 2015

Как правильно замечает Марк (Амери): Использование десериализатора 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]

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

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

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

Почему?

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

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

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

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

3 голосов
/ 19 октября 2010

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

...