Форматирование определенных объектов JSON в одной строке

#python #json #python-3.x #formatting

#python #json #python-3.x #форматирование

Вопрос:

Рассмотрим следующий код:

 >>> import json
>>> data = {
...     'x': [1, {'$special': 'a'}, 2],
...     'y': {'$special': 'b'},
...     'z': {'p': True, 'q': False}
... }
>>> print(json.dumps(data, indent=2))
{
  "y": {
    "$special": "b"
  },
  "z": {
    "q": false,
    "p": true
  },
  "x": [
    1,
    {
      "$special": "a"
    },
    2
  ]
}
 

Я хочу отформатировать JSON так, чтобы объекты JSON, имеющие только одно свойство '$special' , отображались в одной строке следующим образом.

 {
  "y": {"$special": "b"},
  "z": {
    "q": false,
    "p": true
  },
  "x": [
    1,
    {"$special": "a"},
    2
  ]
}
 

Я поиграл с реализацией пользовательского JSONEncoder интерфейса и передал его в json.dumps качестве cls аргумента, но у двух методов для JSONEncoder каждого есть проблема:

  • JSONEncoder default Метод вызывается для каждой части data , но возвращаемое значение не является необработанной строкой JSON, поэтому, похоже, нет никакого способа настроить его форматирование.
  • JSONEncoder encode Метод возвращает необработанную строку JSON, но вызывается только один раз для data всего объекта в целом.

Есть ли какой-либо способ заставить меня JSONEncoder делать то, что я хочу?

Комментарии:

1. Зачем вам это нужно в первую очередь? На самом json деле модуль не настроен так, чтобы вы могли контролировать формат вывода в такой степени.

2. Кроме того, когда "$special" присутствует, гарантируется ли, что это единственный ключ?

3. @MartijnPieters Я хочу отображать данные JSON в пользовательском интерфейсе, ориентированном на разработчика. Объекты формы JSON {'$special': 'some key'} в изобилии появляются в этих данных JSON, поэтому я просто изучал возможность их визуального уплотнения. немного. Можно предположить, что '$special' это единственный ключ, если он присутствует, хотя я полагаю, что это ортогонально тому, что я действительно спрашиваю: как локально изменить форматирование JSON. Возможно, ответ просто «вы не можете с json модулем».

4. Я сам пытался сделать что-то очень похожее на это и не нашел в нем никаких кубиков JSONEncoder . В итоге я просто отказался от борьбы и пошел со стандартным prettify.

5. Я действительно надеялся найти что-то вроде yapf , но для форматирования json, в идеале как библиотека Python. Однако я еще не нашел ни одного.

Ответ №1:

json Модуль на самом деле не предназначен для предоставления вам такого большого контроля над выводом; отступы в основном предназначены для удобства чтения при отладке.

Вместо того json , чтобы создавать выходные данные, вы можете преобразовать выходные данные с помощью стандартного библиотечного tokenize модуля:

 import tokenize
from io import BytesIO


def inline_special(json_data):
    def adjust(t, ld,):
        """Adjust token line number by offset"""
        (sl, sc), (el, ec) = t.start, t.end
        return t._replace(start=(sl   ld, sc), end=(el   ld, ec))

    def transform():
        with BytesIO(json_data.encode('utf8')) as b:
            held = []  # to defer newline tokens
            lastend = None  # to track the end pos of the prev token
            loffset = 0     # line offset to adjust tokens by
            tokens = tokenize.tokenize(b.readline)
            for tok in tokens:
                if tok.type == tokenize.NL:
                    # hold newlines until we know there's no special key coming
                    held.append(adjust(tok, loffset))
                elif (tok.type == tokenize.STRING and
                        tok.string == '"$special"'):
                    # special string, collate tokens until the next rbrace
                    # held newlines are discarded, adjust the line offset
                    loffset -= len(held)
                    held = []
                    text = [tok.string]
                    while tok.exact_type != tokenize.RBRACE:
                        tok = next(tokens)
                        if tok.type != tokenize.NL:
                            text.append(tok.string)
                            if tok.string in ':,':
                                text.append(' ')
                        else:
                            loffset -= 1  # following lines all shift
                    line, col = lastend
                    text = ''.join(text)
                    endcol = col   len(text)
                    yield tokenize.TokenInfo(
                        tokenize.STRING, text, (line, col), (line, endcol),
                        '')
                    # adjust any remaining tokens on this line
                    while tok.type != tokenize.NL:
                        tok = next(tokens)
                        yield tok._replace(
                            start=(line, endcol),
                            end=(line, endcol   len(tok.string)))
                        endcol  = len(tok.string)
                else:
                    # uninteresting token, yield any held newlines
                    if held:
                        yield from held
                        held = []
                    # adjust and remember last position
                    tok = adjust(tok, loffset)
                    lastend = tok.end
                    yield tok

    return tokenize.untokenize(transform()).decode('utf8')
 

Это успешно переформатирует ваш образец:

 import json

data = {
    'x': [1, {'$special': 'a'}, 2],
    'y': {'$special': 'b'},
    'z': {'p': True, 'q': False}
}

>>> print(inline_special(json.dumps(data, indent=2)))
{
  "x": [
    1,
    {"$special": "a"},
    2
  ],
  "y": {"$special": "b"},
  "z": {
    "p": true,
    "q": false
  }
}
 

Ответ №2:

Я обнаружил, что следующее решение на основе регулярных выражений является самым простым, хотя и … на основе регулярных выражений.

 import json
import re
data = {
    'x': [1, {'$special': 'a'}, 2],
    'y': {'$special': 'b'},
    'z': {'p': True, 'q': False}
}
text = json.dumps(data, indent=2)
pattern = re.compile(r"""
{
s*
"$special"
s*
:
s*
"
((?:[^"]|\"))*  # Captures zero or more NotQuote or EscapedQuote
"
s*
}
""", re.VERBOSE)
print(pattern.sub(r'{"$special": "1"}', text))
 

Вывод следующий.

 {
  "x": [
    1,
    {"$special": "a"},
    2
  ],
  "y": {"$special": "b"},
  "z": {
    "q": false,
    "p": true
  }
}
 

Ответ №3:

Вы можете это сделать, но вам в основном придется копировать / изменять большую часть кода, json.encoder потому что функции кодирования на самом деле не предназначены для частичного переопределения.

По сути, скопируйте весь _make_iterencode файл from json.encoder и внесите изменения, чтобы ваш специальный словарь печатался без отступов новой строки. Затем monkeypatch пакет json для использования вашей измененной версии, запустите дамп json, затем отмените monkeypatch (если хотите).

_make_iterencode Функция довольно длинная, поэтому я разместил только те части, которые необходимо изменить.

 import json
import json.encoder

def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
    ...
    def _iterencode_dict(dct, _current_indent_level):
        ...
        if _indent is not None:
            _current_indent_level  = 1
            if '$special' in dct:
                newline_indent = ''
                item_separator = _item_separator
            else:
                newline_indent = 'n'   (' ' * (_indent * _current_indent_level))
                item_separator = _item_separator   newline_indent
            yield newline_indent
        ...
        if newline_indent is not None:
            _current_indent_level -= 1
            if '$special' not in dct:
                yield 'n'   (' ' * (_indent * _current_indent_level))

def main():
    data = {
        'x': [1, {'$special': 'a'}, 2],
        'y': {'$special': 'b'},
        'z': {'p': True, 'q': False},
    }

    orig_make_iterencoder = json.encoder._make_iterencode
    json.encoder._make_iterencode = _make_iterencode
    print(json.dumps(data, indent=2))
    json.encoder._make_iterencode = orig_make_iterencoder