Достаточно напечатать json, но сохранить внутренние массивы на одной строке python

Я довольно печатаю json в Python, используя этот код:

json.dumps(json_output, indent=2, separators=(',', ': ')

Это печатает мой json как:

{    
    "rows_parsed": [
        [
          "a",
          "b",
          "c",
          "d"
        ],
        [
          "e",
          "f",
          "g",
          "i"
        ],
    ]
}

Однако, я хочу, чтобы он печатался так:

{    
    "rows_parsed": [
        ["a","b","c","d"],
        ["e","f","g","i"],
    ]
}

Как я могу хранить массивы, которые находятся в массивах, все на одной строке, как указано выше?

Ответ 1

Вот способ сделать это с минимальным количеством модификаций:

import json
from json import JSONEncoder
import re

class MarkedList:
    _list = None
    def __init__(self, l):
        self._list = l

z = {    
    "rows_parsed": [
        MarkedList([
          "a",
          "b",
          "c",
          "d"
        ]),
        MarkedList([
          "e",
          "f",
          "g",
          "i"
        ]),
    ]
}

class CustomJSONEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, MarkedList):
            return "##<{}>##".format(o._list)

b = json.dumps(z, indent=2, separators=(',', ':'), cls=CustomJSONEncoder)
b = b.replace('"##<', "").replace('>##"', "")

print(b)

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

Другой, гораздо более эффективный способ сделать это, но гораздо более уродливый, - это обезьяна исправить патч json.encoder._make_iterencode._iterencode примерно так:

def _iterencode(o, _current_indent_level):
    if isinstance(o, str):
        yield _encoder(o)
    elif o is None:
        yield 'null'
    elif o is True:
        yield 'true'
    elif o is False:
        yield 'false'
    elif isinstance(o, int):
        # see comment for int/float in _make_iterencode
        yield _intstr(o)
    elif isinstance(o, float):
        # see comment for int/float in _make_iterencode
        yield _floatstr(o)
    elif isinstance(o, MarkedList):
        yield _my_custom_parsing(o)
    elif isinstance(o, (list, tuple)):
        yield from _iterencode_list(o, _current_indent_level)
    elif isinstance(o, dict):
        yield from _iterencode_dict(o, _current_indent_level)
    else:
        if markers is not None:
            markerid = id(o)
            if markerid in markers:
                raise ValueError("Circular reference detected")
            markers[markerid] = o
        o = _default(o)
        yield from _iterencode(o, _current_indent_level)
        if markers is not None:
            del markers[markerid]

Ответ 2

Начиная с решения @martin-gergov, это должно быть немного более надежно:

import json
from json import JSONEncoder


class SingleLine:
    def __init__(self, v):
        self.wrapped = v


class SingleLineEncoder(JSONEncoder):

    def iterencode(self, o, _one_shot=False):
        try:
            self.wrap(o)
            yield from super().iterencode(o, _one_shot=_one_shot)
        finally:
            # Important: we altered the data structure. Now we must restore it.
            self.unwrap(o)

    def wrap(self, o):
        # Go recursively through all containers (dict, list) in o
        if isinstance(o, list):
            for i, v in enumerate(o):
                if self.must_be_formatted_as_single_line(v):
                    o[i] = SingleLine(v)
                else:
                    self.wrap(v)
        elif isinstance(o, dict):
            for k, v in o.items():
                if self.must_be_formatted_as_single_line(v):
                    o[k] = SingleLine(v)
                else:
                    self.wrap(v)

    def unwrap(self, o):
        # Go recursively through all containers (dict, list) in o
        if isinstance(o, list):
            for i, v in enumerate(o):
                if isinstance(v, SingleLine):
                    o[i] = v.wrapped
                else:
                    self.unwrap(v)
        elif isinstance(o, dict):
            for k, v in o.items():
                if isinstance(v, SingleLine):
                    o[k] = v.wrapped
                else:
                    self.unwrap(v)

    def must_be_formatted_as_single_line(self, o):
        # This is your rule for deciding what to format as a single line
        return isinstance(o, list) and all(isinstance(v, str) for v in o)

    def default(self, o):
        if isinstance(o, SingleLine):
            return str(o.wrapped)
        return json.JSONEncoder.default(self, o)


z = {
    "rows_parsed": [
        [
          "a",
          "b",
          "c",
          "d"
        ],
        [
          "e",
          "f",
          "g",
          "i"
        ],
    ]
}
b = json.dumps(z, indent=2, separators=(',', ':'), cls=SingleLineEncoder)
print(b)

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

Вывод вышеуказанного кода:

{
  "rows_parsed":[
    "['a', 'b', 'c', 'd']",
    "['e', 'f', 'g', 'i']"
  ]
}

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

Ответ 3

Я не вижу, как вы могли бы сделать это в json.dumps. После небольшого поиска я наткнулся на несколько вариантов: Один из вариантов - выполнить некоторую постобработку с пользовательской функцией:

def fix_json_indent(text, indent=3):
            space_indent = indent * 4
    initial = " " * space_indent
    json_output = []
    current_level_elems = []
    all_entries_at_level = None  # holder for consecutive entries at exact space_indent level
    for line in text.splitlines():
        if line.startswith(initial):
            if line[space_indent] == " ":
                # line indented further than the level
                if all_entries_at_level:
                    current_level_elems.append(all_entries_at_level)
                    all_entries_at_level = None
                item = line.strip()
                current_level_elems.append(item)
                if item.endswith(","):
                    current_level_elems.append(" ")
            elif current_level_elems:
                # line on the same space_indent level
                # no more sublevel_entries 
                current_level_elems.append(line.strip())
                json_output.append("".join(current_level_elems))
                current_level_elems = []
            else:
                # line at the exact space_indent level but no items indented further
                if all_entries_at_level:
                    # last pending item was not the start of a new sublevel_entries.
                    json_output.append(all_entries_at_level)
                all_entries_at_level = line.rstrip()
        else:
            if all_entries_at_level:
                json_output.append(all_entries_at_level)
                all_entries_at_level = None
            if current_level_elems:
                json_output.append("".join(current_level_elems))
            json_output.append(line)
    return "\n".join(json_output)

Другая возможность - регулярное выражение, но оно довольно уродливо и зависит от структуры кода, который вы разместили:

def fix_json_indent(text):
    import re
    return  re.sub('{"', '{\n"', re.sub('\[\[', '[\n[', re.sub('\]\]', ']\n]', re.sub('}', '\n}', text))))