Python: сделать генератор списка JSON сериализуемым

как я могу объединить список файлов 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

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

4 ответов


вы должны вывести из 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]].


начиная с 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]


полное простое читаемое решение, которое может сериализовать генератор из обычного или пустого iterable, может работать .encode () or .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.отвал. (против решения user1158559)
  • нет необходимости переписывать многие методы NotImplementedError, потому что это не все методы, такие как __repr__. Лучше хранить генератор также в списке, чтобы обеспечить значимые результаты, такие как [<generator object ...>]. (против Клода). Методы по умолчанию __len__ и __bool__ теперь работает правильно, чтобы распознать пустой и не пустой объект.

преимуществом этого решения является то, что стандартный сериализатор JSON может использоваться без параметров. Если вложенные Генераторы должны поддерживаться или если инкапсуляция SerializableGenerator(iterator) нежелательно, то я рекомендую IterEncoder ответ.


на основе принятого ответа, вот StreamArray в конце концов я пошел. Она содержит две лжи:--4-->

  1. предложение self.__tail__ может быть неизменным
  2. 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]

только для одного использования!