Come posso leggere pigramente più valori JSON da un file / flusso in Python?


101

Mi piacerebbe leggere più oggetti JSON da un file / flusso in Python, uno alla volta. Purtroppo json.load()solo .read()fino alla fine del file; non sembra esserci alcun modo per usarlo per leggere un singolo oggetto o per iterare pigramente sugli oggetti.

C'è un modo per fare questo? L'utilizzo della libreria standard sarebbe l'ideale, ma se esiste una libreria di terze parti, la userei invece.

Al momento sto mettendo ogni oggetto su una riga separata e lo sto usando json.loads(f.readline()), ma preferirei davvero non aver bisogno di farlo.

Esempio di utilizzo

esempio.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

sessione di esempio

$ 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

Potresti aggiungere un esempio del comportamento che vorresti dagli oggetti annidati per favore?
Tim McNamara

@TimMcNamara: il comportamento dell'oggetto nidificato non dovrebbe cambiare. Tuttavia, una volta raggiunta la fine del primo oggetto di primo livello ( {"foo": ["bar", "baz"]}nel mio esempio), dovrebbe yieldfarlo e quindi continuare con quello successivo ( 1).
Jeremy

1
perché evitare le "righe json"? È sempre possibile serializzare un oggetto in json in modo tale che non abbia '\n'(un solo ritorno a capo, non due caratteri) nella sua rappresentazione json perché '\n'deve essere sottoposto a escape all'interno di una stringa json e quindi '\n'può essere utilizzato solo per la formattazione, ad es json.dumps(). t introdurre '\n'per impostazione predefinita. Attenzione che le nuove righe Unicode come U + 0085 potrebbero non avere caratteri di escape all'interno di stringhe json.
jfs

2
La libreria ijson potrebbe essere utile in questo caso. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov

1
Il titolo non dovrebbe essere "Come posso leggere pigramente più valori JSON da un file / flusso in Python?" Poiché un oggetto è anche un valore, come lo è un json int, una stringa, ecc.
hetepeperfan

Risposte:


20

Ecco una soluzione molto, molto più semplice. Il segreto è provare, fallire e utilizzare le informazioni nell'eccezione per analizzare correttamente. L'unica limitazione è che il file deve essere ricercabile.

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

Modifica: ho appena notato che funzionerà solo per Python> = 3.5. Per le prime, i fallimenti restituiscono un ValueError, e devi analizzare la posizione dalla stringa, ad es

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

Benvenuto in Stack Overflow e grazie per la risposta! È molto più vicino a quello che speravo di trovare. Dovrei essere in grado di adattarlo ai tipi di casi a cui stavo pensando, anche se non forniscono direttamente la ricerca.
Jeremy

Non refunzionerà: le barre rovesciate devono essere eliminate. Considera una stringa grezza r'...'.
Tom Swirly

2
Ne avevo bisogno per il mio lavoro, quindi ho creato una piccola libreria Python per farlo, usando più o meno la tua tecnica con alcuni dettagli, ed è qui: pypi.python.org/pypi/Streamy
Tom Swirly

2
Se usi ujsoninvece di jsonte otterrai un enorme aumento di velocità
OddNorg

40

JSON generalmente non è molto buono per questo tipo di utilizzo incrementale; non esiste un modo standard per serializzare più oggetti in modo che possano essere facilmente caricati uno alla volta, senza analizzare l'intero lotto.

La soluzione oggetto per riga che stai utilizzando è vista anche altrove. Scrapy le chiama "linee JSON":

Puoi farlo leggermente più Pythonically:

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

Penso che questo sia il modo migliore: non si basa su librerie di terze parti ed è facile capire cosa sta succedendo. L'ho usato anche in alcuni dei miei codici.


4
ri: "no standard way": non vedo il problema, la sintassi sembra rendere non ambigui più oggetti consecutivi fintanto che si dispone di un buffer di un carattere. Grazie per aver sottolineato che altre persone usano "linee JSON", per ora mi sento meno a disagio nell'usarle.
Jeremy

31

Un po 'tardi forse, ma ho avuto questo problema esatto (beh, più o meno). La mia soluzione standard per questi problemi è di solito fare solo una divisione regex su qualche noto oggetto root, ma nel mio caso era impossibile. L'unico modo fattibile per farlo genericamente è implementare un tokenizer appropriato .

Dopo non aver trovato una soluzione sufficientemente generica e con buone prestazioni, ho finito di farlo da solo, scrivendo il splitstreammodulo. È un pre-tokenizer che comprende JSON e XML e divide un flusso continuo in più blocchi per l'analisi (lascia tuttavia a te l'analisi effettiva). Per ottenere un qualche tipo di prestazione da esso, è scritto come un modulo C.

Esempio:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)

È fantastico. Grazie per averlo condiviso.
Jeremy

Questa è la soluzione definitiva. Spero che continui ad aggiornarlo.
Bartvds

Funziona semplicemente. Grazie per aver fornito un modulo così utile.
Vinod Sharma

1
Potresti caricare una versione compilata di .py? Ho provato a compilare e installare il modulo ma ... produce un sacco di errori riguardanti la ridefinizione delle costanti e simili.
SirJames

Il modulo è scritto in C. Portarlo su puro Python è lasciato come esercizio a chiunque sia pronto per il compito :). Probabilmente sarà troppo lento per lo scopo per cui è stato scritto. Se hai problemi con la compilazione, probabilmente devi installare il pacchetto python-dev.
Krumelur

25

Certo che puoi farlo. Devi solo prendere raw_decodedirettamente. Questa implementazione carica l'intero file in memoria e opera su quella stringa (proprio come json.loadfa); se hai file di grandi dimensioni puoi modificarlo per leggere dal file solo se necessario senza troppe difficoltà.

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

Utilizzo: proprio come richiesto, è un generatore.


2
Sembra che la parte difficile sarebbe assicurarsi che le letture in streaming portino abbastanza file da avere un intero oggetto da decodificare. Quindi questo è un approccio semplice che funziona se ad esempio si presume che gli oggetti non abbiano mai una nuova riga in essi. Ma a meno che tu non imponga quel tipo di struttura aggiuntiva al file, che l'OP sta cercando di evitare, sembra che tu abbia bisogno di una soluzione come quella da @Benedict
nealmcb

24

Questo è un problema piuttosto brutto in realtà perché devi eseguire lo streaming in linee, ma il pattern match su più linee contro le parentesi graffe, ma anche il pattern match json. È una sorta di json-preparse seguito da un json parse. Json è, rispetto ad altri formati, facile da analizzare, quindi non è sempre necessario cercare una libreria di analisi, tuttavia, come dovremmo risolvere questi problemi in conflitto?

Generatori in soccorso!

La bellezza dei generatori per un problema come questo è che puoi impilarli uno sull'altro gradualmente astraendo la difficoltà del problema pur mantenendo la pigrizia. Ho anche considerato di utilizzare il meccanismo per restituire i valori a un generatore (send ()), ma fortunatamente ho scoperto che non avevo bisogno di usarlo.

Per risolvere il primo dei problemi hai bisogno di una sorta di streamingfinditer, come versione in streaming di re.finditer. Il mio tentativo di fare questo di seguito inserisce le righe secondo necessità (decommenta l'istruzione di debug per vederla) mentre restituisce ancora le corrispondenze. In realtà l'ho modificato leggermente per produrre linee non abbinate e corrispondenze (contrassegnate come 0 o 1 nella prima parte della tupla restituita).

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]

Con ciò, è quindi possibile abbinare fino alle parentesi graffe, tenere conto ogni volta se le parentesi sono bilanciate e quindi restituire oggetti semplici o composti a seconda dei casi.

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="" 

Questo restituisce le tuple come segue:

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

Fondamentalmente questa è la parte brutta fatta. Ora dobbiamo solo eseguire il livello finale di analisi come riteniamo opportuno. Ad esempio, possiamo usare la funzione iterload di Jeremy Roman (grazie!) Per analizzare una singola riga:

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

Provalo:

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

Ottengo questi risultati (e se attivi quella riga di debug, vedrai che inserisce le righe secondo necessità):

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

Questo non funzionerà per tutte le situazioni. A causa dell'implementazione della jsonlibreria, è impossibile lavorare completamente correttamente senza reimplementare il parser da soli.


8
Se vuoi farlo correttamente, devi anche fare attenzione alle parentesi graffe e alle parentesi all'interno delle stringhe. E poi fai attenzione anche alle citazioni sfuggite. Prima che tu te ne accorga, il "preparser" diventerà complicato quasi quanto un parser JSON completo.
Petr Viktorin

Grazie Jeremy. È stata una bella domanda! Sì Petr - hai assolutamente ragione ovviamente :)
Benedict

1
Ben fatto. Questo si comporterà correttamente se i caratteri piacciono "}"e si "]"verificano all'interno di stringhe JSON? Penso che questa sia una limitazione generale dell'analisi con regex.
Thomas K

2
Quando ho frugato in giro ho scoperto che la funzione di analisi principale è costruita in modo tale che è impossibile usarla correttamente con pigrizia, quindi non otterrai un risultato perfetto senza implementare un parser completo da solo. Questa risposta dimostra diverse cose utili e rilevanti e gestisce bene casi semplici.
Jeremy

3
Questa risposta è orribile e non ho idea del motivo per cui è stata votata. L'autore ammette che in realtà non funziona per tutti gli input, quindi per definizione non è nemmeno una risposta corretta e utilizza un'espressione regolare complessa che viene calcolata , quindi non possiamo nemmeno leggere di cosa si tratta. A che serve una funzione che a volte dà il giusto risultato?
Tom Swirly

10

Credo che un modo migliore per farlo sarebbe usare una macchina a stati. Di seguito è riportato un codice di esempio che ho elaborato convertendo un codice NodeJS sul link sottostante in Python 3 (parola chiave non locale usata disponibile solo in Python 3, il codice non funzionerà su Python 2)

Modifica-1: codice aggiornato e reso compatibile con Python 2

Modifica-2: aggiornata e aggiunta anche una versione solo per Python3

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

Solo versione Python 3

# 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

Versione compatibile con Python 2

# 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

Testarlo

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

L'output dello stesso è

[1, 2, '3']
{'name': 'tarun'}
1
2
3
[{'name': 'a', 'data': [1, None, 2]}]

Bella soluzione! Guarderò più da vicino più tardi, ma questo è molto promettente. Ma per quel che vale, ho preferito la versione solo per Python 3. Usare i dict per tutte le variabili locali è piuttosto imbarazzante, e io per primo sono felice di lasciare Python 2 in passato. ;)
Jeremy

@ JeremyBanks, certo di non sapere quale versione hai scelto come target. Ora ho aggiunto una versione solo Python3 e una compatibile con Py2 anche nella risposta per qualcun altro che potrebbe essere ancora su Python 2
Tarun Lalwani

@JeremyBanks, è rimasto solo 1 giorno con la taglia, spero che tu possa rivedere e fornire un feedback sulla risposta
Tarun Lalwani

Sembra che l'unico che abbia veramente capito il problema sia stato Tarun. L'efficienza dell'analisi si basa sul numero di passaggi che avvengono sull'input. La maggior parte delle risposte usa regex o legge una riga in anticipo (questo potrebbe anche essere pericoloso) o, peggio, fallisce l'analisi un numero imprecisato di volte. Peccato che questo non faccia parte di Python.
mschonaker

4

Vorrei fornire una soluzione. Il pensiero chiave è "provare" a decodificare: se fallisce, dagli più feed, altrimenti usa le informazioni di offset per preparare la decodifica successiva.

Tuttavia l'attuale modulo json non può tollerare la decodifica di SPAZIO nell'head of string, quindi devo toglierli.

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)

========================= Ho testato diversi file txt e funziona bene. (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, la tua iniziale)

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

(output per il testcase di Benedict)

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

3

Ecco il mio:

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

Ciao, è super utile, ma potresti mostrare come posso usare la classe per caricare il file json?
song0089

3

Ho usato l'elegante soluzione di @ wuilang. L'approccio semplice - leggere un byte, provare a decodificare, leggere un byte, provare a decodificare, ... - ha funzionato, ma sfortunatamente è stato molto lento.

Nel mio caso, stavo cercando di leggere oggetti JSON "abbastanza stampati" dello stesso tipo di oggetto da un file. Questo mi ha permesso di ottimizzare l'approccio; Potevo leggere il file riga per riga, decodificando solo quando ho trovato una riga che conteneva esattamente "}":

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 = ""

Se ti capita di lavorare con JSON compatto una per riga che sfugge alle nuove righe in stringhe letterali, puoi tranquillamente semplificare ulteriormente questo approccio:

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

Ovviamente, questi semplici approcci funzionano solo per tipi molto specifici di JSON. Tuttavia, se queste ipotesi sono valide, queste soluzioni funzionano correttamente e rapidamente.


2

Se utilizzi un'istanza json.JSONDecoder, puoi utilizzare la raw_decodefunzione membro. Restituisce una tupla di rappresentazione Python del valore JSON e un indice in cui si è interrotta l'analisi. Ciò semplifica il sezionamento (o la ricerca in un oggetto flusso) dei valori JSON rimanenti. Non sono così contento del ciclo while extra per saltare lo spazio bianco tra i diversi valori JSON nell'input, ma a mio parere fa il lavoro.

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

La versione successiva è molto più breve e mangia la parte della stringa che è già stata analizzata. Sembra che per qualche motivo una seconda chiamata json.JSONDecoder.raw_decode () sembra fallire quando il primo carattere nella stringa è uno spazio bianco, questo è anche il motivo per cui salto gli spazi bianchi nel whileloop sopra ...

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

Nella documentazione sulla classe json.JSONDecoder il metodo raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders contiene quanto segue:

Può essere utilizzato per decodificare un documento JSON da una stringa che potrebbe contenere dati estranei alla fine.

E questi dati estranei possono facilmente essere un altro valore JSON. In altre parole, il metodo potrebbe essere scritto con questo scopo in mente.

Con input.txt utilizzando la funzione superiore ottengo l'output di esempio come presentato nella domanda originale.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.