#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