Wie kann ich in Python mehrere JSON-Werte aus einer Datei / einem Stream träge lesen?

100

Ich möchte mehrere JSON-Objekte nacheinander aus einer Datei / einem Stream in Python lesen. Leider json.load()nur .read()bis zum Ende der Datei; Es scheint keine Möglichkeit zu geben, damit ein einzelnes Objekt zu lesen oder träge über die Objekte zu iterieren.

Gibt es eine Möglichkeit, dies zu tun? Die Verwendung der Standardbibliothek wäre ideal, aber wenn es eine Bibliothek eines Drittanbieters gibt, würde ich diese stattdessen verwenden.

Im Moment setze ich jedes Objekt in eine separate Zeile und verwende es json.loads(f.readline()), aber ich würde es wirklich vorziehen, dies nicht tun zu müssen.

Beispiel Verwendung

example.py

import my_json as json
import sys

for o in json.iterload(sys.stdin):
    print("Working on a", type(o))

in.txt

{"foo": ["bar", "baz"]} 1 2 [] 4 5 6

Beispielsitzung

$ python3.2 example.py < in.txt
Working on a dict
Working on a int
Working on a int
Working on a list
Working on a int
Working on a int
Working on a int
Jeremy
quelle
Könnten Sie bitte ein Beispiel für das Verhalten hinzufügen, das Sie von verschachtelten Objekten erwarten?
Tim McNamara
@ TimMcNamara: Das Verhalten des verschachtelten Objekts sollte sich nicht ändern. Sobald wir jedoch das Ende des ersten Objekts der obersten Ebene erreicht haben ( {"foo": ["bar", "baz"]}in meinem Beispiel), sollte yieldes dies tun und dann mit dem nächsten fortfahren ( 1).
Jeremy
1
warum die "json lines" vermeiden? Es ist immer möglich , ein Objekt in Json serialisiert werden, so dass es keine hat '\n'(ein einzelnes Newline, nicht zwei Zeichen) in seiner json Darstellung , da '\n'muss entwertet wird in einem JSON - String und daher '\n'kann für die Formatierung nur zB verwendet werden, glaube ich json.dumps()doesn‘ t '\n'standardmäßig einführen . Beachten Sie, dass Unicode-Zeilenumbrüche wie U + 0085 möglicherweise nicht in JSON-Zeichenfolgen angezeigt werden.
JFS
2
Die ijson- Bibliothek könnte in diesem Fall hilfreich sein. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris
1
Sollte der Titel nicht "Wie kann ich mehrere JSON- Werte aus einer Datei / einem Stream in Python träge lesen ?" Sein. Da ein Objekt ebenso ein Wert ist wie ein json int, ein String usw., während das Gegenteil nicht unbedingt wahr ist?
Hetepeperfan

Antworten:

20

Hier ist eine viel, viel einfachere Lösung. Das Geheimnis besteht darin, zu versuchen, fehlzuschlagen und die Informationen in der Ausnahme zu verwenden, um sie korrekt zu analysieren. Die einzige Einschränkung ist, dass die Datei durchsuchbar sein muss.

def stream_read_json(fn):
    import json
    start_pos = 0
    with open(fn, 'r') as f:
        while True:
            try:
                obj = json.load(f)
                yield obj
                return
            except json.JSONDecodeError as e:
                f.seek(start_pos)
                json_str = f.read(e.pos)
                obj = json.loads(json_str)
                start_pos += e.pos
                yield obj

Bearbeiten: habe gerade bemerkt, dass dies nur für Python> = 3.5 funktioniert. Bei früheren Fehlern geben Fehler einen ValueError zurück, und Sie müssen die Position aus der Zeichenfolge analysieren, z

def stream_read_json(fn):
    import json
    import re
    start_pos = 0
    with open(fn, 'r') as f:
        while True:
            try:
                obj = json.load(f)
                yield obj
                return
            except ValueError as e:
                f.seek(start_pos)
                end_pos = int(re.match('Extra data: line \d+ column \d+ .*\(char (\d+).*\)',
                                    e.args[0]).groups()[0])
                json_str = f.read(end_pos)
                obj = json.loads(json_str)
                start_pos += end_pos
                yield obj
Nic Watson
quelle
Willkommen bei Stack Overflow und vielen Dank für die Antwort! Das ist viel näher an dem, was ich mir erhofft hatte. Ich sollte in der Lage sein, dies an die Arten von Fällen anzupassen, über die ich nachgedacht habe, auch wenn sie keine direkte Suche ermöglichen.
Jeremy
Das rewird nicht funktionieren - die Backslashes müssen entkommen. Betrachten Sie eine rohe Zeichenfolge r'...'.
Tom Swirly
2
Ich brauchte dies für meine eigene Arbeit, also erstellte ich eine kleine Python-Bibliothek, um dies zu tun, wobei ich mehr oder weniger Ihre Technik mit einigen Details verwendete, und es ist hier: pypi.python.org/pypi/Streamy
Tom Swirly
2
Wenn Sie ujsonanstelle von verwenden, werden jsonSie eine enorme Beschleunigung bekommen
OddNorg
39

JSON ist im Allgemeinen nicht sehr gut für diese Art der inkrementellen Verwendung. Es gibt keine Standardmethode zum Serialisieren mehrerer Objekte, sodass sie problemlos einzeln geladen werden können, ohne das gesamte Los zu analysieren.

Die von Ihnen verwendete Objekt-pro-Zeile-Lösung wird auch an anderer Stelle angezeigt. Scrapy nennt es "JSON-Zeilen":

Sie können es etwas mehr Pythonisch tun:

for jsonline in f:
    yield json.loads(jsonline)   # or do the processing in this loop

Ich denke, dies ist der beste Weg - es basiert nicht auf Bibliotheken von Drittanbietern und es ist leicht zu verstehen, was los ist. Ich habe es auch in meinem eigenen Code verwendet.

Thomas K.
quelle
4
re: "no standard way": Ich sehe das Problem nicht, die Syntax scheint mehrere aufeinanderfolgende Objekte eindeutig zu machen, solange Sie einen Ein-Zeichen-Puffer haben. Vielen Dank für den Hinweis, dass andere Leute "JSON-Leitungen" verwenden. Ich fühle mich im Moment weniger schlecht, wenn ich sie verwende.
Jeremy
30

Ein bisschen spät vielleicht, aber ich hatte genau dieses Problem (mehr oder weniger). Meine Standardlösung für diese Probleme besteht normalerweise darin, nur einen Regex-Split für ein bekanntes Stammobjekt durchzuführen, aber in meinem Fall war dies unmöglich. Die einzige Möglichkeit, dies generisch zu tun, besteht darin, einen geeigneten Tokenizer zu implementieren .

Nachdem ich keine ausreichend generische und einigermaßen leistungsfähige Lösung gefunden hatte, beendete ich dies selbst und schrieb das splitstreamModul. Es ist ein Pre-Tokenizer, der JSON und XML versteht und einen kontinuierlichen Stream zum Parsen in mehrere Blöcke aufteilt (die eigentliche Analyse bleibt jedoch Ihnen überlassen). Um eine gewisse Leistung zu erzielen, wird es als C-Modul geschrieben.

Beispiel:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)
Krumelur
quelle
Das ist großartig. Danke, dass du es geteilt hast.
Jeremy
Dies ist die endgültige Lösung. Ich hoffe du aktualisierst es weiter.
Bartvds
Es funktioniert einfach. Vielen Dank für die Bereitstellung eines so nützlichen Moduls.
Vinod Sharma
1
Könnten Sie eine kompilierte .py-Version hochladen? Ich habe versucht, das Modul zu erstellen und zu installieren, aber ... es führt zu einer Reihe von Fehlern bei der Neudefinition von Konstanten und dergleichen.
SirJames
Das Modul ist in C geschrieben. Die Portierung auf reines Python bleibt als Übung jedem überlassen, der für die Aufgabe bereit ist :). Es wird wahrscheinlich zu langsam für den Zweck sein, für den es geschrieben wurde. Wenn Sie Probleme beim Kompilieren haben, müssen Sie wahrscheinlich das Python-Dev-Paket installieren.
Krumelur
25

Sicher können Sie das tun. Sie müssen nur raw_decodedirekt nehmen. Diese Implementierung lädt die gesamte Datei in den Speicher und verarbeitet diese Zeichenfolge (ähnlich wie json.load). Wenn Sie große Dateien haben, können Sie diese so ändern, dass sie nur nach Bedarf ohne große Schwierigkeiten aus der Datei gelesen werden können.

import json
from json.decoder import WHITESPACE

def iterload(string_or_fp, cls=json.JSONDecoder, **kwargs):
    if isinstance(string_or_fp, file):
        string = string_or_fp.read()
    else:
        string = str(string_or_fp)

    decoder = cls(**kwargs)
    idx = WHITESPACE.match(string, 0).end()
    while idx < len(string):
        obj, end = decoder.raw_decode(string, idx)
        yield obj
        idx = WHITESPACE.match(string, end).end()

Verwendung: Genau wie Sie es gewünscht haben, ist es ein Generator.

Jeremy Roman
quelle
2
Es scheint, dass der schwierige Teil darin besteht, sicherzustellen, dass die Streaming-Lesevorgänge so viel von der Datei einbringen, dass Sie ein ganzes Objekt zum Dekodieren haben. Dies ist also ein einfacher Ansatz, der funktioniert, wenn Sie beispielsweise annehmen, dass Objekte niemals Zeilenumbrüche enthalten. Aber wenn Sie der Datei nicht diese Art von zusätzlicher Struktur auferlegen, die das OP zu vermeiden versucht, brauchen Sie anscheinend eine solche Lösung von @Benedict
nealmcb
24

Dies ist ein ziemlich unangenehmes Problem, da Sie in Zeilen streamen müssen, aber Musterübereinstimmungen über mehrere Zeilen hinweg mit geschweiften Klammern, aber auch Musterübereinstimmungen json. Es ist eine Art JSON-Vorbereitung, gefolgt von einer JSON-Analyse. Json ist im Vergleich zu anderen Formaten einfach zu analysieren, so dass es nicht immer notwendig ist, eine Analysebibliothek zu verwenden. Wie sollten wir diese widersprüchlichen Probleme lösen?

Generatoren zur Rettung!

Das Schöne an Generatoren für ein Problem wie dieses ist, dass Sie sie übereinander stapeln können, um die Schwierigkeit des Problems schrittweise zu beseitigen und gleichzeitig die Faulheit zu bewahren. Ich habe auch überlegt, den Mechanismus zum Zurückgeben von Werten an einen Generator (send ()) zu verwenden, aber zum Glück musste ich das nicht verwenden.

Um das erste der Probleme zu lösen, benötigen Sie eine Art Streamingfinditer als Streaming-Version von re.finditer. Mein Versuch, dies unten zu tun, zieht nach Bedarf Zeilen ein (kommentieren Sie die Debug-Anweisung aus, um sie zu sehen), während Sie immer noch Übereinstimmungen zurückgeben. Ich habe es dann tatsächlich leicht modifiziert, um nicht übereinstimmende Linien sowie Übereinstimmungen zu erhalten (im ersten Teil des erhaltenen Tupels als 0 oder 1 markiert).

import re

def streamingfinditer(pat,stream):
  for s in stream:
#    print "Read next line: " + s
    while 1:
      m = re.search(pat,s)
      if not m:
        yield (0,s)
        break
      yield (1,m.group())
      s = re.split(pat,s,1)[1]

Damit ist es dann möglich, bis zu geschweiften Klammern abzugleichen, jedes Mal zu berücksichtigen, ob die Klammern ausgeglichen sind, und dann entweder einfache oder zusammengesetzte Objekte zurückzugeben.

braces='{}[]'
whitespaceesc=' \t'
bracesesc='\\'+'\\'.join(braces)
balancemap=dict(zip(braces,[1,-1,1,-1]))
bracespat='['+bracesesc+']'
nobracespat='[^'+bracesesc+']*'
untilbracespat=nobracespat+bracespat

def simpleorcompoundobjects(stream):
  obj = ""
  unbalanced = 0
  for (c,m) in streamingfinditer(re.compile(untilbracespat),stream):
    if (c == 0): # remainder of line returned, nothing interesting
      if (unbalanced == 0):
        yield (0,m)
      else:
        obj += m
    if (c == 1): # match returned
      if (unbalanced == 0):
        yield (0,m[:-1])
        obj += m[-1]
      else:
        obj += m
      unbalanced += balancemap[m[-1]]
      if (unbalanced == 0):
        yield (1,obj)
        obj="" 

Dies gibt Tupel wie folgt zurück:

(0,"String of simple non-braced objects easy to parse")
(1,"{ 'Compound' : 'objects' }")

Im Grunde ist das der böse Teil. Wir müssen jetzt nur noch die letzte Stufe des Parsens durchführen, wie wir es für richtig halten. Zum Beispiel können wir die Iterload-Funktion von Jeremy Roman (Danke!) Verwenden, um eine einzelne Zeile zu analysieren:

def streamingiterload(stream):
  for c,o in simpleorcompoundobjects(stream):
    for x in iterload(o):
      yield x 

Probier es aus:

of = open("test.json","w") 
of.write("""[ "hello" ] { "goodbye" : 1 } 1 2 {
} 2
9 78
 4 5 { "animals" : [ "dog" , "lots of mice" ,
 "cat" ] }
""")
of.close()
// open & stream the json
f = open("test.json","r")
for o in streamingiterload(f.readlines()):
  print o
f.close()

Ich erhalte diese Ergebnisse (und wenn Sie diese Debug-Zeile aktivieren, werden Sie sehen, dass sie bei Bedarf in den Zeilen eingezogen wird):

[u'hello']
{u'goodbye': 1}
1
2
{}
2
9
78
4
5
{u'animals': [u'dog', u'lots of mice', u'cat']}

Dies funktioniert nicht in allen Situationen. Aufgrund der Implementierung der jsonBibliothek ist es unmöglich , vollständig korrekt zu arbeiten, ohne den Parser selbst neu zu implementieren.

Benedikt
quelle
8
Wenn Sie dies richtig machen möchten, müssen Sie auch auf Klammern und Klammern in Strings achten. Und dann auch auf entkommene Zitate achten. Bevor Sie es wissen, wird der "Preparser" fast so kompliziert wie ein vollständiger JSON-Parser.
Petr Viktorin
Danke Jeremy. Es war eine schöne Herausforderung einer Frage! Ja Petr - Sie haben natürlich absolut Recht :)
Benedict
1
Schön gemacht. Verhält sich dies korrekt, wenn Zeichen JSON-Zeichenfolgen mögen "}"und in diesen "]"vorkommen? Ich denke, dies ist eine allgemeine Einschränkung beim Parsen mit Regex.
Thomas K
2
Beim Stöbern stellte ich fest, dass die Hauptanalysefunktion so aufgebaut ist, dass es unmöglich ist, sie träge zu verwenden, sodass Sie kein perfektes Ergebnis erzielen, ohne selbst einen vollständigen Parser zu implementieren. Diese Antwort zeigt einige nützliche relevante Dinge und behandelt einfache Fälle gut.
Jeremy
3
Diese Antwort ist schrecklich und ich habe keine Ahnung, warum sie positiv bewertet wird. Der Autor gibt zu, dass es nicht für alle Eingaben funktioniert, daher ist es per Definition nicht einmal eine richtige Antwort, und es wird ein komplexer regulärer Ausdruck verwendet, der berechnet wird , sodass wir nicht einmal lesen können, was es ist. Was nützt eine Funktion, die manchmal das richtige Ergebnis liefert?
Tom Swirly
10

Ich glaube, ein besserer Weg wäre die Verwendung einer Zustandsmaschine. Unten finden Sie einen Beispielcode, den ich durch Konvertieren eines NodeJS-Codes unter dem folgenden Link in Python 3 ausgearbeitet habe (verwendetes nicht lokales Schlüsselwort nur in Python 3 verfügbar, Code funktioniert in Python 2 nicht).

Edit-1: Code aktualisiert und mit Python 2 kompatibel gemacht

Edit-2: Eine reine Python3-Version wurde ebenfalls aktualisiert und hinzugefügt

https://gist.github.com/creationix/5992451

Nur Python 3-Version

# A streaming byte oriented JSON parser.  Feed it a single byte at a time and
# it will emit complete objects as it comes across them.  Whitespace within and
# between objects is ignored.  This means it can parse newline delimited JSON.
import math


def json_machine(emit, next_func=None):
    def _value(byte_data):
        if not byte_data:
            return

        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _value  # Ignore whitespace

        if byte_data == 0x22:  # "
            return string_machine(on_value)

        if byte_data == 0x2d or (0x30 <= byte_data < 0x40):  # - or 0-9
            return number_machine(byte_data, on_number)

        if byte_data == 0x7b:  #:
            return object_machine(on_value)

        if byte_data == 0x5b:  # [
            return array_machine(on_value)

        if byte_data == 0x74:  # t
            return constant_machine(TRUE, True, on_value)

        if byte_data == 0x66:  # f
            return constant_machine(FALSE, False, on_value)

        if byte_data == 0x6e:  # n
            return constant_machine(NULL, None, on_value)

        if next_func == _value:
            raise Exception("Unexpected 0x" + str(byte_data))

        return next_func(byte_data)

    def on_value(value):
        emit(value)
        return next_func

    def on_number(number, byte):
        emit(number)
        return _value(byte)

    next_func = next_func or _value
    return _value


TRUE = [0x72, 0x75, 0x65]
FALSE = [0x61, 0x6c, 0x73, 0x65]
NULL = [0x75, 0x6c, 0x6c]


def constant_machine(bytes_data, value, emit):
    i = 0
    length = len(bytes_data)

    def _constant(byte_data):
        nonlocal i
        if byte_data != bytes_data[i]:
            i += 1
            raise Exception("Unexpected 0x" + str(byte_data))

        i += 1
        if i < length:
            return _constant
        return emit(value)

    return _constant


def string_machine(emit):
    string = ""

    def _string(byte_data):
        nonlocal string

        if byte_data == 0x22:  # "
            return emit(string)

        if byte_data == 0x5c:  # \
            return _escaped_string

        if byte_data & 0x80:  # UTF-8 handling
            return utf8_machine(byte_data, on_char_code)

        if byte_data < 0x20:  # ASCII control character
            raise Exception("Unexpected control character: 0x" + str(byte_data))

        string += chr(byte_data)
        return _string

    def _escaped_string(byte_data):
        nonlocal string

        if byte_data == 0x22 or byte_data == 0x5c or byte_data == 0x2f:  # " \ /
            string += chr(byte_data)
            return _string

        if byte_data == 0x62:  # b
            string += "\b"
            return _string

        if byte_data == 0x66:  # f
            string += "\f"
            return _string

        if byte_data == 0x6e:  # n
            string += "\n"
            return _string

        if byte_data == 0x72:  # r
            string += "\r"
            return _string

        if byte_data == 0x74:  # t
            string += "\t"
            return _string

        if byte_data == 0x75:  # u
            return hex_machine(on_char_code)

    def on_char_code(char_code):
        nonlocal string
        string += chr(char_code)
        return _string

    return _string


# Nestable state machine for UTF-8 Decoding.
def utf8_machine(byte_data, emit):
    left = 0
    num = 0

    def _utf8(byte_data):
        nonlocal num, left
        if (byte_data & 0xc0) != 0x80:
            raise Exception("Invalid byte in UTF-8 character: 0x" + byte_data.toString(16))

        left = left - 1

        num |= (byte_data & 0x3f) << (left * 6)
        if left:
            return _utf8
        return emit(num)

    if 0xc0 <= byte_data < 0xe0:  # 2-byte UTF-8 Character
        left = 1
        num = (byte_data & 0x1f) << 6
        return _utf8

    if 0xe0 <= byte_data < 0xf0:  # 3-byte UTF-8 Character
        left = 2
        num = (byte_data & 0xf) << 12
        return _utf8

    if 0xf0 <= byte_data < 0xf8:  # 4-byte UTF-8 Character
        left = 3
        num = (byte_data & 0x07) << 18
        return _utf8

    raise Exception("Invalid byte in UTF-8 string: 0x" + str(byte_data))


# Nestable state machine for hex escaped characters
def hex_machine(emit):
    left = 4
    num = 0

    def _hex(byte_data):
        nonlocal num, left

        if 0x30 <= byte_data < 0x40:
            i = byte_data - 0x30
        elif 0x61 <= byte_data <= 0x66:
            i = byte_data - 0x57
        elif 0x41 <= byte_data <= 0x46:
            i = byte_data - 0x37
        else:
            raise Exception("Expected hex char in string hex escape")

        left -= 1
        num |= i << (left * 4)

        if left:
            return _hex
        return emit(num)

    return _hex


def number_machine(byte_data, emit):
    sign = 1
    number = 0
    decimal = 0
    esign = 1
    exponent = 0

    def _mid(byte_data):
        if byte_data == 0x2e:  # .
            return _decimal

        return _later(byte_data)

    def _number(byte_data):
        nonlocal number
        if 0x30 <= byte_data < 0x40:
            number = number * 10 + (byte_data - 0x30)
            return _number

        return _mid(byte_data)

    def _start(byte_data):
        if byte_data == 0x30:
            return _mid

        if 0x30 < byte_data < 0x40:
            return _number(byte_data)

        raise Exception("Invalid number: 0x" + str(byte_data))

    if byte_data == 0x2d:  # -
        sign = -1
        return _start

    def _decimal(byte_data):
        nonlocal decimal
        if 0x30 <= byte_data < 0x40:
            decimal = (decimal + byte_data - 0x30) / 10
            return _decimal

        return _later(byte_data)

    def _later(byte_data):
        if byte_data == 0x45 or byte_data == 0x65:  # E e
            return _esign

        return _done(byte_data)

    def _esign(byte_data):
        nonlocal esign
        if byte_data == 0x2b:  # +
            return _exponent

        if byte_data == 0x2d:  # -
            esign = -1
            return _exponent

        return _exponent(byte_data)

    def _exponent(byte_data):
        nonlocal exponent
        if 0x30 <= byte_data < 0x40:
            exponent = exponent * 10 + (byte_data - 0x30)
            return _exponent

        return _done(byte_data)

    def _done(byte_data):
        value = sign * (number + decimal)
        if exponent:
            value *= math.pow(10, esign * exponent)

        return emit(value, byte_data)

    return _start(byte_data)


def array_machine(emit):
    array_data = []

    def _array(byte_data):
        if byte_data == 0x5d:  # ]
            return emit(array_data)

        return json_machine(on_value, _comma)(byte_data)

    def on_value(value):
        array_data.append(value)

    def _comma(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _comma  # Ignore whitespace

        if byte_data == 0x2c:  # ,
            return json_machine(on_value, _comma)

        if byte_data == 0x5d:  # ]
            return emit(array_data)

        raise Exception("Unexpected byte: 0x" + str(byte_data) + " in array body")

    return _array


def object_machine(emit):
    object_data = {}
    key = None

    def _object(byte_data):
        if byte_data == 0x7d:  #
            return emit(object_data)

        return _key(byte_data)

    def _key(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _object  # Ignore whitespace

        if byte_data == 0x22:
            return string_machine(on_key)

        raise Exception("Unexpected byte: 0x" + str(byte_data))

    def on_key(result):
        nonlocal key
        key = result
        return _colon

    def _colon(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _colon  # Ignore whitespace

        if byte_data == 0x3a:  # :
            return json_machine(on_value, _comma)

        raise Exception("Unexpected byte: 0x" + str(byte_data))

    def on_value(value):
        object_data[key] = value

    def _comma(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _comma  # Ignore whitespace

        if byte_data == 0x2c:  # ,
            return _key

        if byte_data == 0x7d:  #
            return emit(object_data)

        raise Exception("Unexpected byte: 0x" + str(byte_data))

    return _object

Python 2-kompatible Version

# A streaming byte oriented JSON parser.  Feed it a single byte at a time and
# it will emit complete objects as it comes across them.  Whitespace within and
# between objects is ignored.  This means it can parse newline delimited JSON.
import math


def json_machine(emit, next_func=None):
    def _value(byte_data):
        if not byte_data:
            return

        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _value  # Ignore whitespace

        if byte_data == 0x22:  # "
            return string_machine(on_value)

        if byte_data == 0x2d or (0x30 <= byte_data < 0x40):  # - or 0-9
            return number_machine(byte_data, on_number)

        if byte_data == 0x7b:  #:
            return object_machine(on_value)

        if byte_data == 0x5b:  # [
            return array_machine(on_value)

        if byte_data == 0x74:  # t
            return constant_machine(TRUE, True, on_value)

        if byte_data == 0x66:  # f
            return constant_machine(FALSE, False, on_value)

        if byte_data == 0x6e:  # n
            return constant_machine(NULL, None, on_value)

        if next_func == _value:
            raise Exception("Unexpected 0x" + str(byte_data))

        return next_func(byte_data)

    def on_value(value):
        emit(value)
        return next_func

    def on_number(number, byte):
        emit(number)
        return _value(byte)

    next_func = next_func or _value
    return _value


TRUE = [0x72, 0x75, 0x65]
FALSE = [0x61, 0x6c, 0x73, 0x65]
NULL = [0x75, 0x6c, 0x6c]


def constant_machine(bytes_data, value, emit):
    local_data = {"i": 0, "length": len(bytes_data)}

    def _constant(byte_data):
        # nonlocal i, length
        if byte_data != bytes_data[local_data["i"]]:
            local_data["i"] += 1
            raise Exception("Unexpected 0x" + byte_data.toString(16))

        local_data["i"] += 1

        if local_data["i"] < local_data["length"]:
            return _constant
        return emit(value)

    return _constant


def string_machine(emit):
    local_data = {"string": ""}

    def _string(byte_data):
        # nonlocal string

        if byte_data == 0x22:  # "
            return emit(local_data["string"])

        if byte_data == 0x5c:  # \
            return _escaped_string

        if byte_data & 0x80:  # UTF-8 handling
            return utf8_machine(byte_data, on_char_code)

        if byte_data < 0x20:  # ASCII control character
            raise Exception("Unexpected control character: 0x" + byte_data.toString(16))

        local_data["string"] += chr(byte_data)
        return _string

    def _escaped_string(byte_data):
        # nonlocal string

        if byte_data == 0x22 or byte_data == 0x5c or byte_data == 0x2f:  # " \ /
            local_data["string"] += chr(byte_data)
            return _string

        if byte_data == 0x62:  # b
            local_data["string"] += "\b"
            return _string

        if byte_data == 0x66:  # f
            local_data["string"] += "\f"
            return _string

        if byte_data == 0x6e:  # n
            local_data["string"] += "\n"
            return _string

        if byte_data == 0x72:  # r
            local_data["string"] += "\r"
            return _string

        if byte_data == 0x74:  # t
            local_data["string"] += "\t"
            return _string

        if byte_data == 0x75:  # u
            return hex_machine(on_char_code)

    def on_char_code(char_code):
        # nonlocal string
        local_data["string"] += chr(char_code)
        return _string

    return _string


# Nestable state machine for UTF-8 Decoding.
def utf8_machine(byte_data, emit):
    local_data = {"left": 0, "num": 0}

    def _utf8(byte_data):
        # nonlocal num, left
        if (byte_data & 0xc0) != 0x80:
            raise Exception("Invalid byte in UTF-8 character: 0x" + byte_data.toString(16))

        local_data["left"] -= 1

        local_data["num"] |= (byte_data & 0x3f) << (local_data["left"] * 6)
        if local_data["left"]:
            return _utf8
        return emit(local_data["num"])

    if 0xc0 <= byte_data < 0xe0:  # 2-byte UTF-8 Character
        local_data["left"] = 1
        local_data["num"] = (byte_data & 0x1f) << 6
        return _utf8

    if 0xe0 <= byte_data < 0xf0:  # 3-byte UTF-8 Character
        local_data["left"] = 2
        local_data["num"] = (byte_data & 0xf) << 12
        return _utf8

    if 0xf0 <= byte_data < 0xf8:  # 4-byte UTF-8 Character
        local_data["left"] = 3
        local_data["num"] = (byte_data & 0x07) << 18
        return _utf8

    raise Exception("Invalid byte in UTF-8 string: 0x" + str(byte_data))


# Nestable state machine for hex escaped characters
def hex_machine(emit):
    local_data = {"left": 4, "num": 0}

    def _hex(byte_data):
        # nonlocal num, left
        i = 0  # Parse the hex byte
        if 0x30 <= byte_data < 0x40:
            i = byte_data - 0x30
        elif 0x61 <= byte_data <= 0x66:
            i = byte_data - 0x57
        elif 0x41 <= byte_data <= 0x46:
            i = byte_data - 0x37
        else:
            raise Exception("Expected hex char in string hex escape")

        local_data["left"] -= 1
        local_data["num"] |= i << (local_data["left"] * 4)

        if local_data["left"]:
            return _hex
        return emit(local_data["num"])

    return _hex


def number_machine(byte_data, emit):
    local_data = {"sign": 1, "number": 0, "decimal": 0, "esign": 1, "exponent": 0}

    def _mid(byte_data):
        if byte_data == 0x2e:  # .
            return _decimal

        return _later(byte_data)

    def _number(byte_data):
        # nonlocal number
        if 0x30 <= byte_data < 0x40:
            local_data["number"] = local_data["number"] * 10 + (byte_data - 0x30)
            return _number

        return _mid(byte_data)

    def _start(byte_data):
        if byte_data == 0x30:
            return _mid

        if 0x30 < byte_data < 0x40:
            return _number(byte_data)

        raise Exception("Invalid number: 0x" + byte_data.toString(16))

    if byte_data == 0x2d:  # -
        local_data["sign"] = -1
        return _start

    def _decimal(byte_data):
        # nonlocal decimal
        if 0x30 <= byte_data < 0x40:
            local_data["decimal"] = (local_data["decimal"] + byte_data - 0x30) / 10
            return _decimal

        return _later(byte_data)

    def _later(byte_data):
        if byte_data == 0x45 or byte_data == 0x65:  # E e
            return _esign

        return _done(byte_data)

    def _esign(byte_data):
        # nonlocal esign
        if byte_data == 0x2b:  # +
            return _exponent

        if byte_data == 0x2d:  # -
            local_data["esign"] = -1
            return _exponent

        return _exponent(byte_data)

    def _exponent(byte_data):
        # nonlocal exponent
        if 0x30 <= byte_data < 0x40:
            local_data["exponent"] = local_data["exponent"] * 10 + (byte_data - 0x30)
            return _exponent

        return _done(byte_data)

    def _done(byte_data):
        value = local_data["sign"] * (local_data["number"] + local_data["decimal"])
        if local_data["exponent"]:
            value *= math.pow(10, local_data["esign"] * local_data["exponent"])

        return emit(value, byte_data)

    return _start(byte_data)


def array_machine(emit):
    local_data = {"array_data": []}

    def _array(byte_data):
        if byte_data == 0x5d:  # ]
            return emit(local_data["array_data"])

        return json_machine(on_value, _comma)(byte_data)

    def on_value(value):
        # nonlocal array_data
        local_data["array_data"].append(value)

    def _comma(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _comma  # Ignore whitespace

        if byte_data == 0x2c:  # ,
            return json_machine(on_value, _comma)

        if byte_data == 0x5d:  # ]
            return emit(local_data["array_data"])

        raise Exception("Unexpected byte: 0x" + str(byte_data) + " in array body")

    return _array


def object_machine(emit):
    local_data = {"object_data": {}, "key": ""}

    def _object(byte_data):
        # nonlocal object_data, key
        if byte_data == 0x7d:  #
            return emit(local_data["object_data"])

        return _key(byte_data)

    def _key(byte_data):
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _object  # Ignore whitespace

        if byte_data == 0x22:
            return string_machine(on_key)

        raise Exception("Unexpected byte: 0x" + byte_data.toString(16))

    def on_key(result):
        # nonlocal object_data, key
        local_data["key"] = result
        return _colon

    def _colon(byte_data):
        # nonlocal object_data, key
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _colon  # Ignore whitespace

        if byte_data == 0x3a:  # :
            return json_machine(on_value, _comma)

        raise Exception("Unexpected byte: 0x" + str(byte_data))

    def on_value(value):
        # nonlocal object_data, key
        local_data["object_data"][local_data["key"]] = value

    def _comma(byte_data):
        # nonlocal object_data
        if byte_data == 0x09 or byte_data == 0x0a or byte_data == 0x0d or byte_data == 0x20:
            return _comma  # Ignore whitespace

        if byte_data == 0x2c:  # ,
            return _key

        if byte_data == 0x7d:  #
            return emit(local_data["object_data"])

        raise Exception("Unexpected byte: 0x" + str(byte_data))

    return _object

Testen Sie es

if __name__ == "__main__":
    test_json = """[1,2,"3"] {"name": 
    "tarun"} 1 2 
    3 [{"name":"a", 
    "data": [1,
    null,2]}]
"""
    def found_json(data):
        print(data)

    state = json_machine(found_json)

    for char in test_json:
        state = state(ord(char))

Die Ausgabe davon ist

[1, 2, '3']
{'name': 'tarun'}
1
2
3
[{'name': 'a', 'data': [1, None, 2]}]
Tarun Lalwani
quelle
Schöne Lösung! Ich werde später genauer hinsehen, aber das ist sehr vielversprechend. Aber für das, was es wert ist, habe ich die reine Python 3-Version bevorzugt. Die Verwendung von Diktaten für alle Ihre lokalen Variablen ist etwas umständlich, und ich bin froh, Python 2 in der Vergangenheit verlassen zu haben. ;)
Jeremy
@ JeremyBanks, sicher wusste ich nicht, auf welche Version du abzielst. Jetzt habe ich eine Nur-Python3-Version und eine Py2-kompatible Version hinzugefügt, auch in der Antwort für jemanden, der möglicherweise noch auf Python 2 ist
Tarun Lalwani
@ JeremyBanks, nur noch 1 Tag mit dem Kopfgeld, hoffe, Sie können die Antwort überprüfen und Feedback geben
Tarun Lalwani
Es sieht so aus, als wäre Tarun der einzige, der das Problem wirklich verstanden hat. Die Effizienz des Parsens hängt von der Anzahl der Durchgänge ab, die an der Eingabe stattfinden. Die meisten Antworten verwenden Regex oder lesen vorher eine Zeile (dies kann auch gefährlich sein) oder schlagen, schlimmer noch, das Parsen eine unbekannte Anzahl von Malen fehl. Schade, dass dies nicht Teil von Python ist.
mschonaker
4

Ich möchte eine Lösung anbieten. Der Schlüsselgedanke ist, zu versuchen, zu dekodieren: Wenn dies fehlschlägt, geben Sie ihm mehr Vorschub, andernfalls verwenden Sie die Versatzinformationen, um die nächste Dekodierung vorzubereiten.

Das aktuelle JSON-Modul toleriert jedoch nicht, dass SPACE im Kopf des zu decodierenden Strings verwendet wird, sodass ich sie entfernen muss.

import sys
import json

def iterload(file):
    buffer = ""
    dec = json.JSONDecoder()
    for line in file:         
        buffer = buffer.strip(" \n\r\t") + line.strip(" \n\r\t")
        while(True):
            try:
                r = dec.raw_decode(buffer)
            except:
                break
            yield r[0]
            buffer = buffer[r[1]:].strip(" \n\r\t")


for o in iterload(sys.stdin):
    print("Working on a", type(o),  o)

========================= Ich habe mehrere txt-Dateien getestet und es funktioniert einwandfrei. (in1.txt)

{"foo": ["bar", "baz"]
}
 1 2 [
  ]  4
{"foo1": ["bar1", {"foo2":{"A":1, "B":3}, "DDD":4}]
}
 5   6

(in2.txt)

{"foo"
: ["bar",
  "baz"]
  } 
1 2 [
] 4 5 6

(in.txt, Ihre Initiale)

{"foo": ["bar", "baz"]} 1 2 [] 4 5 6

(Ausgabe für Benedikts Testfall)

python test.py < in.txt
('Working on a', <type 'list'>, [u'hello'])
('Working on a', <type 'dict'>, {u'goodbye': 1})
('Working on a', <type 'int'>, 1)
('Working on a', <type 'int'>, 2)
('Working on a', <type 'dict'>, {})
('Working on a', <type 'int'>, 2)
('Working on a', <type 'int'>, 9)
('Working on a', <type 'int'>, 78)
('Working on a', <type 'int'>, 4)
('Working on a', <type 'int'>, 5)
('Working on a', <type 'dict'>, {u'animals': [u'dog', u'lots of mice', u'cat']})
Wuliang
quelle
3

Hier ist meins:

import simplejson as json
from simplejson import JSONDecodeError
class StreamJsonListLoader():
    """
    When you have a big JSON file containint a list, such as

    [{
        ...
    },
    {
        ...
    },
    {
        ...
    },
    ...
    ]

    And it's too big to be practically loaded into memory and parsed by json.load,
    This class comes to the rescue. It lets you lazy-load the large json list.
    """

    def __init__(self, filename_or_stream):
        if type(filename_or_stream) == str:
            self.stream = open(filename_or_stream)
        else:
            self.stream = filename_or_stream

        if not self.stream.read(1) == '[':
            raise NotImplementedError('Only JSON-streams of lists (that start with a [) are supported.')

    def __iter__(self):
        return self

    def next(self):
        read_buffer = self.stream.read(1)
        while True:
            try:
                json_obj = json.loads(read_buffer)

                if not self.stream.read(1) in [',',']']:
                    raise Exception('JSON seems to be malformed: object is not followed by comma (,) or end of list (]).')
                return json_obj
            except JSONDecodeError:
                next_char = self.stream.read(1)
                read_buffer += next_char
                while next_char != '}':
                    next_char = self.stream.read(1)
                    if next_char == '':
                        raise StopIteration
                    read_buffer += next_char
user3542882
quelle
Hallo, das ist super nützlich, aber können Sie zeigen, wie ich die Klasse zum Laden der JSON-Datei verwenden kann?
song0089
3

Ich habe die elegante Lösung von @ wuilang verwendet. Der einfache Ansatz - ein Byte lesen, versuchen zu dekodieren, ein Byte lesen, versuchen zu dekodieren, ... - funktionierte, war aber leider sehr langsam.

In meinem Fall habe ich versucht, "hübsch gedruckte" JSON-Objekte desselben Objekttyps aus einer Datei zu lesen. Dadurch konnte ich den Ansatz optimieren; Ich konnte die Datei Zeile für Zeile lesen und nur dekodieren, wenn ich eine Zeile fand, die genau "}" enthielt:

def iterload(stream):
    buf = ""
    dec = json.JSONDecoder()
    for line in stream:
        line = line.rstrip()
        buf = buf + line
        if line == "}":
            yield dec.raw_decode(buf)
            buf = ""

Wenn Sie zufällig mit einem kompakten JSON mit einer Zeile arbeiten, der Zeilenumbrüchen in Zeichenfolgenliteralen entgeht, können Sie diesen Ansatz noch sicherer vereinfachen:

def iterload(stream):
    dec = json.JSONDecoder()
    for line in stream:
        yield dec.raw_decode(line)

Offensichtlich funktionieren diese einfachen Ansätze nur für ganz bestimmte Arten von JSON. Wenn diese Annahmen jedoch zutreffen, funktionieren diese Lösungen korrekt und schnell.

sigpwned
quelle
2

Wenn Sie eine json.JSONDecoder-Instanz verwenden, können Sie die raw_decodeMember-Funktion verwenden. Es gibt ein Tupel der Python-Darstellung des JSON-Werts und einen Index zurück, an dem die Analyse gestoppt wurde. Dies macht es einfach, die verbleibenden JSON-Werte zu schneiden (oder in einem Stream-Objekt zu suchen). Ich bin nicht so glücklich über die zusätzliche while-Schleife, um den Leerraum zwischen den verschiedenen JSON-Werten in der Eingabe zu überspringen, aber meiner Meinung nach wird die Aufgabe erledigt.

import json

def yield_multiple_value(f):
    '''
    parses multiple JSON values from a file.
    '''
    vals_str = f.read()
    decoder = json.JSONDecoder()
    try:
        nread = 0
        while nread < len(vals_str):
            val, n = decoder.raw_decode(vals_str[nread:])
            nread += n
            # Skip over whitespace because of bug, below.
            while nread < len(vals_str) and vals_str[nread].isspace():
                nread += 1
            yield val
    except json.JSONDecodeError as e:
        pass
    return

Die nächste Version ist viel kürzer und frisst den Teil der Zeichenfolge, der bereits analysiert wurde. Es scheint, dass aus irgendeinem Grund ein zweiter Aufruf von json.JSONDecoder.raw_decode () fehlschlägt, wenn das erste Zeichen in der Zeichenfolge ein Leerzeichen ist. Dies ist auch der Grund, warum ich das Leerzeichen im Whileloop oben überspringe ...

def yield_multiple_value(f):
    '''
    parses multiple JSON values from a file.
    '''
    vals_str = f.read()
    decoder = json.JSONDecoder()
    while vals_str:
        val, n = decoder.raw_decode(vals_str)
        #remove the read characters from the start.
        vals_str = vals_str[n:]
        # remove leading white space because a second call to decoder.raw_decode()
        # fails when the string starts with whitespace, and
        # I don't understand why...
        vals_str = vals_str.lstrip()
        yield val
    return

In der Dokumentation zur Klasse json.JSONDecoder enthält die Methode raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders Folgendes:

Dies kann verwendet werden, um ein JSON-Dokument aus einer Zeichenfolge zu dekodieren, die am Ende möglicherweise fremde Daten enthält.

Und diese fremden Daten können leicht ein weiterer JSON-Wert sein. Mit anderen Worten, die Methode könnte zu diesem Zweck geschrieben werden.

Mit der input.txt unter Verwendung der oberen Funktion erhalte ich die Beispielausgabe wie in der ursprünglichen Frage dargestellt.

Hetepeperfan
quelle
0

Sie können https://pypi.org/project/json-stream-parser/ für genau diesen Zweck verwenden.

import sys
from json_stream_parser import load_iter
for obj in load_iter(sys.stdin):
    print(obj)

Ausgabe

{'foo': ['bar', 'baz']}
1
2
[]
4
5
6
user5203
quelle