Python: создать генератор списка JSON serializable

Как я могу объединить список файлов JSON в огромный массив JSON? У меня 5000 файлов и 550 000 элементов списка.

Моя первая попытка заключалась в использовании jq, но похоже, что jq -s не оптимизирован для большого ввода.

jq -s -r '[.[][]]' *.js 

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

Вот мой текущий код:

def concatFiles(outName, inFileNames):
    def listGenerator():
        for inName in inFileNames:
            with open(inName, 'r') as f:
                for item in json.load(f):
                    yield item

    with open(outName, 'w') as f:
        json.dump(listGenerator(), f)

Я получаю:

TypeError: <generator object listGenerator at 0x7f94dc2eb3c0> is not JSON serializable

Любая попытка загрузить все файлы в RAM вызовет OOM-killer Linux. У вас есть идеи?

Ответ 1

Вы должны получить от list и переопределить метод __iter__.

import json

def gen():
    yield 20
    yield 30
    yield 40

class StreamArray(list):
    def __iter__(self):
        return gen()

    # according to the comment below
    def __len__(self):
        return 1

a = [1,2,3]
b = StreamArray()

print(json.dumps([1,a,b]))

Результат [1, [1, 2, 3], [20, 30, 40]].

Ответ 2

Как и в случае с simplejson 3.8.0, вы можете использовать опцию iterable_as_array, чтобы сделать любую итерабельную сериализацию в массив

# Since simplejson is backwards compatible, you should feel free to import
# it as `json`
import simplejson as json
json.dumps((i*i for i in range(10)), iterable_as_array=True)

результат [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Ответ 3

Полное простое читаемое решение, которое может сериализовать генератор из обычного или пустого итеративного, может работать с .encode() или .iterencode(). Письменные тесты. Протестировано с помощью Python 2.7, 3.0, 3.3, 3.6

import itertools

class SerializableGenerator(list):
    """Generator that is serializable by JSON

    It is useful for serializing huge data by JSON
    >>> json.dumps(SerializableGenerator(iter([1, 2])))
    "[1, 2]"
    >>> json.dumps(SerializableGenerator(iter([])))
    "[]"

    It can be used in a generator of json chunks used e.g. for a stream
    >>> iter_json = ison.JSONEncoder().iterencode(SerializableGenerator(iter([])))
    >>> tuple(iter_json)
    ('[1', ']')
    # >>> for chunk in iter_json:
    # ...     stream.write(chunk)
    # >>> SerializableGenerator((x for x in range(3)))
    # [<generator object <genexpr> at 0x7f858b5180f8>]
    """

    def __init__(self, iterable):
        tmp_body = iter(iterable)
        try:
            self._head = iter([next(tmp_body)])
            self.append(tmp_body)
        except StopIteration:
            self._head = []

    def __iter__(self):
        return itertools.chain(self._head, *self[:1])


# -- test --

import unittest
import json


class Test(unittest.TestCase):

    def combined_dump_assert(self, iterable, expect):
        self.assertEqual(json.dumps(SerializableGenerator(iter(iterable))), expect)

    def combined_iterencode_assert(self, iterable, expect):
        encoder = json.JSONEncoder().iterencode
        self.assertEqual(tuple(encoder(SerializableGenerator(iter(iterable)))), expect)

    def test_dump_data(self):
        self.combined_dump_assert(iter([1, "a"]), '[1, "a"]')

    def test_dump_empty(self):
        self.combined_dump_assert(iter([]), '[]')

    def test_iterencode_data(self):
        self.combined_iterencode_assert(iter([1, "a"]), ('[1', ', "a"', ']'))

    def test_terencode_empty(self):
        self.combined_iterencode_assert(iter([]), ('[]',))

    def test_that_all_data_are_consumed(self):
        gen = SerializableGenerator(iter([1, 2]))
        list(gen)
        self.assertEqual(list(gen), [])

Используемые решения: Вадим Пуштаев (неполный), user1158559 (излишне сложный) и Клод (в другом вопросе тоже сложно).

Полезное упрощение:

  • Нет необходимости оценивать первый элемент лениво и это можно сделать в __init__, потому что мы можем ожидать, что SerializableGenerator можно вызвать непосредственно перед json.dumps. (против решения user1158559)
  • Нет необходимости переписывать многие методы NotImplementedError, потому что это не все методы вроде __repr__. Лучше хранить генератор и в списке, чтобы обеспечить значимые результаты, такие как [<generator object ...>]. (против Клода). Методы по умолчанию __len__ и __bool__ теперь корректно распознают пустой и не пустой объект.

Преимущество этого решения заключается в том, что стандартный сериализатор JSON можно использовать без параметров. Если вложенные генераторы должны поддерживаться или если инкапсуляция с помощью SerializableGenerator(iterator) нежелательна, я рекомендую answer.a href= "/questions/533788/json-encoding-very-long-iterators/2225890#2225890" > IterEncoder.

Ответ 4

Основываясь на принятом ответе, вот StreamArray, в который я в конце концов пошел. Он содержит две ложности:

  • Предложение self.__tail__ может быть неизменным
  • len(StreamArray(some_gen)) является либо 0, либо 1

.

class StreamArray(list):

    def __init__(self, gen):
        self.gen = gen

    def destructure(self):
        try:
            return self.__head__, self.__tail__, self.__len__
        except AttributeError:
            try:
                self.__head__ = self.gen.__next__()
                self.__tail__ = self.gen
                self.__len__ = 1 # A lie
            except StopIteration:
                self.__head__ = None
                self.__tail__ = []
                self.__len__ = 0
            return self.__head__, self.__tail__, self.__len__

    def rebuilt_gen(self):
        def rebuilt_gen_inner():
            head, tail, len_ = self.destructure()
            if len_ > 0:
                yield head
            for elem in tail:
                yield elem
        try:
            return self.__rebuilt_gen__
        except AttributeError:
            self.__rebuilt_gen__ = rebuilt_gen_inner()
            return self.__rebuilt_gen__

    def __iter__(self):
        return self.rebuilt_gen()

    def __next__(self):
        return self.rebuilt_gen()

    def __len__(self):
        return self.destructure()[2]

Только одноразовое использование!