2014-02-09 7 views
12

Wie kann ich eine Liste von JSON-Dateien in einem riesigen JSON-Array erstellen? Ich habe 5000 Dateien und 550 000 Listenelemente.Python: Erstellen eines Listengenerators JSON serializable

Mein erster Versuch war zu verwenden jq, aber es sieht aus wie jq -s ist nicht für einen großen Eingang optimiert.

Dieser Befehl funktioniert, aber es dauert viel zu lange, und ich möchte das wirklich mit Python lösen.

Hier ist mein aktueller Code:

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) 

Ich erhalte:

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

Jeder Ladeversuch alle Dateien in den Arbeitsspeicher, den OOM-Killer von Linux auslösen. Hast du eine Idee?

+1

Wie wäre es, die Dokumente nur durch Einfügen von Kommas zu verketten? – bereal

+0

Sie müssen das äußere Array jeder Datei entfernen. Das Entfernen des ersten und letzten Zeichens jeder Datei sollte funktionieren, aber ich möchte den JSON-Einzug steuern (und entfernen). –

+0

Wie groß sind die Dateien eigentlich? könnte es sein, dass das Halten der vollständigen serialisierten Daten größer ist als Ihr Speicher? – Alex

Antwort

14

Sie sollten von list ableiten und __iter__ Methode überschreiben.

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

Ergebnis ist [1, [1, 2, 3], [20, 30, 40]].

+3

Mit Python 2.7.8 muss die 'StreamArray'-Klasse auch die Methode' __len__' überschreiben und gibt einen Wert zurück, der größer als 0 ist (z. B. 1). Andernfalls ruft der json-Encoder nicht einmal die '__iter__'-Methode auf. – Tristan

+0

Bitte beachten Sie, dass diese Lösung ungültiges JSON erzeugt, wenn sie mit dem Parameter * indent * verwendet wird und das iterable" leer "ist. 'json.dumps ({" products ": StreamArray()}, indent = 2) # {" products ":]}' –

+0

Ich glaube, wir sollten nicht '1 'für die Länge zurückgeben, wenn das iterable" leer "ist. –

13

Ab simplejson 3.8.0 können Sie die iterable_as_array Option jeden iterable serializable in ein Array auf der akzeptierte Antwort

# 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) 

Ergebnis [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

2

Basierend zu machen, hier ist die StreamArray I ging schließlich für. Es enthält zwei liegt:

  1. Der Vorschlag, dass self.__tail__ könnte
  2. len(StreamArray(some_gen)) entweder 0 oder 1

unveränderlich sein.

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] 

Nur zum einmaligen Gebrauch!

+0

+1: Ihre Lösung funktioniert, aber es ist zu kompliziert. Ich denke, dass ich das Gleiche einfacher umgesetzt habe. Schau auf meinen, wenn du einen Nachteil gegen meinen findest. – hynekcer

+0

Ihr sieht gut aus! Für meinen Anwendungsfall ist die faule Auswertung des ersten Gegenstandes eine Eigenschaft. Rückblickend könnte es eine gewisse Vereinfachung von "itertools" geben. Sehr erfreut zu wissen, dass das so funktioniert wie es ist. – user1158559

3

Eine vollständige, einfach lesbare Lösung, die einen Generator von einem normalen oder leeren iterierbaren Programm serialisieren kann, kann mit .encode() oder .itererencode() arbeiten. Schriftliche Prüfungen. Getestet mit 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), []) 

Eingesetzte Lösungen: Vadim Pushtaev (unvollständig), user1158559 (unnötig kompliziert) und Claude (in einer anderen Frage, auch kompliziert).

Nützliche Vereinfachung ist:

  • Es ist nicht notwendig, das erste Element gemächlich zu bewerten und es kann in __init__ erfolgen, weil wir, dass der SerializableGenerator unmittelbar vor json.dumps aufgerufen werden kann, erwarten können. (gegen user1158559 Lösung)
  • Es ist nicht notwendig, viele Methoden von NotImplementedError neu zu schreiben, da dies nicht alle Methoden wie __repr__ sind.Es ist besser, den Generator auch in der Liste zu speichern, um aussagekräftige Ergebnisse wie [<generator object ...>] zu erhalten. (gegen Claude). Die Standardmethoden __len__ und __bool__ funktionieren jetzt korrekt, um ein leeres und nicht leeres Objekt zu erkennen.

Ein Vorteil dieser Lösung ist, dass ein Standard-JSON Serializer ohne params verwendet werden kann. Wenn geschachtelte Generatoren unterstützt werden sollen oder wenn die Kapselung durch SerializableGenerator(iterator) nicht wünschenswert ist, empfehle ich IterEncoder Antwort.

+0

Schön gemacht, und +1 für Tests! – user1158559

Verwandte Themen