2014-02-09 5 views
12

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

Моя первая попытка заключалась в использовании 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 

Любой попытку загрузка всех файлов в оперативную память вызовут ОЫЙ-убийца Linux. У тебя есть идеи?

+1

Как насчет текстовой конкатенации документов, вставляющих запятые между? – bereal

+0

Вам нужно удалить внешний массив каждого файла. Удаление кулака и последнего символа каждого файла должно работать, но я хотел бы управлять (и удалять) отступ json. –

+0

Насколько велики файлы на самом деле? может ли быть, что полная сериализованная информация больше вашей памяти? – Alex

ответ

14

Вы должны получить от 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]].

+3

С Python 2.7.8 класс 'StreamArray' также должен переопределять метод' __len__' и возвращает значение больше 0 (например, 1). В противном случае json-кодер даже не вызывает метод '__iter__' – Tristan

+0

Обратите внимание, что это решение создает недопустимый JSON при использовании с параметром * indent *, и итерабельность является« пустой ». 'json.dumps ({" products ": StreamArray()}, indent = 2) # {" products ":]}' –

+0

Я считаю, что мы не должны «возвращать 1» для длины, если итерабельность «пуста». –

13

По 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]

2

На основании принятого ответа, вот StreamArray я в конечном итоге пошел. Он содержит две лжи:

  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] 

Одиночное использование только!

+0

+1: Ваше решение работает, но оно слишком сложное. Я думаю, что я реализовал то же самое проще. Посмотрите на мою, если вы найдете какой-либо недостаток против моего. – hynekcer

+0

С уважением! Для моего случая использования, лениво оценивая первый элемент, есть функция. Оглядываясь назад, может быть получено некоторое упрощение от «itertools». Очень приятно знать, что это работает так, как есть. – user1158559

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), []) 

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

Полезное упрощение является:

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

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

+0

Красиво сделано, и +1 для тестов! – user1158559

Смежные вопросы