webentwicklung-frage-antwort-db.com.de

Wie kann ich riesige JSON-Dateien als Stream in Json.NET analysieren?

Ich habe eine sehr, sehr große JSON-Datei (1000+ MB) identischer JSON-Objekte. Zum Beispiel:

[
    {
        "id": 1,
        "value": "hello",
        "another_value": "world",
        "value_obj": {
            "name": "obj1"
        },
        "value_list": [
            1,
            2,
            3
        ]
    },
    {
        "id": 2,
        "value": "foo",
        "another_value": "bar",
        "value_obj": {
            "name": "obj2"
        },
        "value_list": [
            4,
            5,
            6
        ]
    },
    {
        "id": 3,
        "value": "a",
        "another_value": "b",
        "value_obj": {
            "name": "obj3"
        },
        "value_list": [
            7,
            8,
            9
        ]

    },
    ...
]

Jedes einzelne Element in der Root-JSON-Liste hat dieselbe Struktur und wäre daher einzeln deserialisierbar. Ich habe bereits die C # -Klassen geschrieben, um diese Daten zu erhalten, und die Deserialisierung einer JSON-Datei, die ein einzelnes Objekt ohne die Liste enthält, funktioniert erwartungsgemäß.

Zuerst habe ich versucht, meine Objekte direkt in einer Schleife zu deserialisieren:

JsonSerializer serializer = new JsonSerializer();
MyObject o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (!sr.EndOfStream)
    {
        o = serializer.Deserialize<MyObject>(reader);
    }
}

Das hat nicht funktioniert, eine Ausnahme warf klar, dass ein Objekt erwartet wurde, keine Liste. Ich verstehe, dass dieser Befehl nur ein einzelnes Objekt lesen würde, das sich auf der Stammebene der JSON-Datei befindet. Da wir jedoch eine list von Objekten haben, ist dies eine ungültige Anforderung.

Meine nächste Idee war, als C # -Liste der Objekte zu deserialisieren:

JsonSerializer serializer = new JsonSerializer();
List<MyObject> o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (!sr.EndOfStream)
    {
        o = serializer.Deserialize<List<MyObject>>(reader);
    }
}

Das gelingt. Das Problem der hohen RAM - Verwendung wird jedoch nur geringfügig reduziert. In diesem Fall sieht es so aus, als ob die Anwendung Elemente nacheinander deserialisiert und nicht die gesamte JSON-Datei in den RAM-Speicher liest. Am Ende wird jedoch immer noch viel RAM verwendet, da das C # -Listenobjekt jetzt vorhanden ist enthält alle Daten aus der JSON-Datei im RAM. Dies hat das Problem nur verdrängt.

Ich entschied mich dann einfach zu versuchen, ein einzelnes Zeichen vom Anfang des Streams zu entfernen (um den [ zu entfernen), indem ich sr.Read() mache, bevor ich in die Schleife gehe. Das erste Objekt liest dann erfolgreich, die nachfolgenden jedoch nicht, mit Ausnahme von "unerwartetem Token". Ich vermute, dies ist das Komma und der Abstand zwischen den Objekten, die den Leser abwerfen.

Das Entfernen von eckigen Klammern funktioniert nicht, da die Objekte eine eigene primitive Liste enthalten, wie Sie im Beispiel sehen können. Selbst der Versuch, }, als Trennzeichen zu verwenden, funktioniert nicht, da sich, wie Sie sehen, Unterobjekte in den Objekten befinden.

Mein Ziel ist es, die Objekte einzeln aus dem Stream lesen zu können. Lesen Sie ein Objekt, tun Sie etwas damit, löschen Sie es aus dem RAM, lesen Sie das nächste Objekt und so weiter. Dies würde die Notwendigkeit beseitigen, entweder den gesamten JSON-String oder den gesamten Inhalt der Daten als C # -Objekte in RAM zu laden. 

Was vermisse ich?

10
fdmillion

Dies sollte Ihr Problem lösen. Im Grunde funktioniert es genauso wie Ihr ursprünglicher Code, es sei denn, es ist nur ein deserialisierendes Objekt, wenn der Leser das {-Zeichen im Stream trifft, und ansonsten springt er zum nächsten, bis er ein anderes Startobjekt-Token findet.

JsonSerializer serializer = new JsonSerializer();
MyObject o;
using (FileStream s = File.Open("bigfile.json", FileMode.Open))
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
    while (reader.Read())
    {
        // deserialize only when there's "{" character in the stream
        if (reader.TokenType == JsonToken.StartObject)
        {
            o = serializer.Deserialize<MyObject>(reader);
        }
    }
}
16
nocodename

Ich denke, wir können es besser machen als die akzeptierte Antwort, indem wir mehr Funktionen von JsonReader verwenden, um eine allgemeinere Lösung zu finden.

Da ein JsonReader Token aus einem JSON verbraucht, wird der Pfad in der JsonReader.Path Eigentum.

Auf diese Weise können wir tief verschachtelte Daten aus einer JSON-Datei präzise auswählen und mithilfe von Regex sicherstellen, dass wir auf dem richtigen Weg sind.

Verwenden Sie also die folgende Erweiterungsmethode:

public static class JsonReaderExtensions
{
    public static IEnumerable<T> SelectTokensWithRegex<T>(
        this JsonReader jsonReader, Regex regex)
    {
        JsonSerializer serializer = new JsonSerializer();
        while (jsonReader.Read())
        {
            if (regex.IsMatch(jsonReader.Path) 
                && jsonReader.TokenType != JsonToken.PropertyName)
            {
                yield return serializer.Deserialize<T>(jsonReader);
            }
        }
    }
}

Die Daten, mit denen Sie sich befassen, liegen auf Pfaden:

[0]
[1]
[2]
... etc

Wir können den folgenden regulären Ausdruck so konstruieren, dass er genau diesem Pfad entspricht:

var regex = new Regex(@"^\[\d+\]$");

es ist jetzt möglich, Objekte aus Ihren Daten zu streamen (ohne die gesamte JSON vollständig zu laden oder zu analysieren)

IEnumerable<MyObject> objects = jsonReader.SelectTokensWithRegex<MyObject>(regex);

Oder wenn wir noch tiefer in die Struktur eintauchen wollen, können wir mit unserem Regex noch präziser vorgehen

var regex = new Regex(@"^\[\d+\]\.value$");
IEnumerable<string> objects = jsonReader.SelectTokensWithRegex<string>(regex);

um nur Eigenschaften von value aus den Elementen im Array zu extrahieren.

Ich fand diese Technik äußerst nützlich, um bestimmte Daten aus riesigen (100 GiB) JSON-Dumps direkt aus HTTP mithilfe eines Netzwerk-Streams zu extrahieren (mit geringem Speicherbedarf und ohne erforderlichen Zwischenspeicher).

2
spender

sie können ein einfaches Nuget-Paket verwenden, das über die oben beschriebenen einfachen Erweiterungsmethoden verfügt JStreamAsyncNet

0