¿Cómo puedo leer perezosamente varios valores JSON de un archivo / flujo en Python?

100

Me gustaría leer varios objetos JSON de un archivo / flujo en Python, uno a la vez. Desafortunadamente, json.load()solo .read()s hasta el final del archivo; no parece haber ninguna forma de usarlo para leer un solo objeto o para iterar perezosamente sobre los objetos.

¿Hay alguna forma de hacer esto? Usar la biblioteca estándar sería ideal, pero si hay una biblioteca de terceros, la usaría en su lugar.

Por el momento, estoy poniendo cada objeto en una línea separada y lo estoy usando json.loads(f.readline()), pero realmente preferiría no tener que hacer esto.

Ejemplo de uso

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

sesión de ejemplo

$ 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
fuente
¿Podría agregar un ejemplo del comportamiento que le gustaría de los objetos anidados, por favor?
Tim McNamara
@TimMcNamara: El comportamiento del objeto anidado no debería cambiar. Sin embargo, una vez que llegamos al final del primer objeto de nivel superior ( {"foo": ["bar", "baz"]}en mi ejemplo), debería yieldhacerlo y luego continuar con el siguiente ( 1).
Jeremy
1
¿Por qué evitar las "líneas json"? Siempre es posible serializar un objeto en json de modo que no tenga '\n'(una sola línea nueva, no dos caracteres) en su representación json porque '\n'debe escaparse dentro de una cadena json y, por '\n'lo tanto, puede usarse para formatear solo, por ejemplo, creo que json.dumps()no ' t introducir '\n'por defecto. Tenga en cuenta que las nuevas líneas Unicode, como U + 0085, pueden no tener escape dentro de las cadenas json.
jfs
2
La biblioteca ijson podría ser útil en este caso. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov
1
¿No debería ser el título "¿Cómo puedo leer perezosamente varios valores JSON de un archivo / flujo en Python?" Dado que un objeto también es un valor, al igual que un json int, string, etc., mientras que lo contrario no es necesariamente cierto.
hetepeperfan

Respuestas:

20

Aquí tienes una solución mucho más sencilla. El secreto es probar, fallar y usar la información de la excepción para analizar correctamente. La única limitación es que el archivo debe poder buscarse.

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

Editar: acabo de notar que esto solo funcionará para Python> = 3.5. Para las anteriores, las fallas devuelven un ValueError, y debe analizar la posición de la cadena, por ejemplo

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
fuente
¡Bienvenido a Stack Overflow y gracias por la respuesta! Eso está mucho más cerca de lo que esperaba encontrar. Debería poder adaptar esto para los tipos de casos en los que estaba pensando, incluso si no proporcionan directamente la búsqueda.
Jeremy
Eso reno funcionará, las barras invertidas deben escapar. Considere una cuerda cruda r'...'.
Tom Swirly
2
Necesitaba esto para mi propio trabajo, así que creé una pequeña biblioteca de Python para hacer esto, usando más o menos su técnica con algunos detalles, y está aquí: pypi.python.org/pypi/Streamy
Tom Swirly
2
Si usa en ujsonlugar de jsonusted, obtendrá una gran aceleración
OddNorg
39

JSON generalmente no es muy bueno para este tipo de uso incremental; no existe una forma estándar de serializar varios objetos para que puedan cargarse fácilmente uno a la vez, sin analizar todo el lote.

El objeto por solución de línea que está utilizando también se ve en otra parte. Scrapy lo llama 'líneas JSON':

Puedes hacerlo un poco más Pythonically:

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

Creo que esta es la mejor manera: no depende de bibliotecas de terceros y es fácil entender lo que está sucediendo. También lo he usado en algunos de mis propios códigos.

Thomas K
fuente
4
re: "no estándar way": No veo el problema, la sintaxis parece hacer que varios objetos consecutivos sean inequívocos siempre que tenga un búfer de un carácter. Gracias por señalar que otras personas usan "líneas JSON", me siento menos mal por usarlo por ahora.
Jeremy
30

Quizás un poco tarde, pero tuve este problema exacto (bueno, más o menos). Mi solución estándar para estos problemas suele ser simplemente hacer una división de expresiones regulares en algún objeto raíz conocido, pero en mi caso fue imposible. La única forma factible de hacer esto de forma genérica es implementar un tokenizador adecuado .

Después de no encontrar una solución suficientemente genérica y con un rendimiento razonable, terminé haciendo esto yo mismo, escribiendo el splitstreammódulo. Es un pre-tokenizador que comprende JSON y XML y divide un flujo continuo en varios fragmentos para analizar (aunque deja el análisis real en sus manos). Para obtener algún tipo de rendimiento, está escrito como un módulo C.

Ejemplo:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)
Krumelur
fuente
Eso es genial. Gracias por compartirlo.
Jeremy
Esta es la solución definitiva. Espero que sigas actualizándolo.
Bartvds
Simplemente funciona. Gracias por proporcionar un módulo tan útil.
Vinod Sharma
1
¿Podría cargar una versión compilada .py? Intenté construir e instalar el módulo pero ... produce un montón de errores relacionados con la redefinición de constantes y demás.
SirJames
El módulo está escrito en C. Portarlo a Python puro se deja como un ejercicio para quien esté preparado para la tarea :). Sin embargo, es probable que sea demasiado lento para el propósito para el que fue escrito. Si tiene problemas para compilar, probablemente necesite instalar el paquete python-dev.
Krumelur
25

Seguro que puedes hacer esto. Solo tienes que ir raw_decodedirectamente. Esta implementación carga todo el archivo en la memoria y opera en esa cadena (tanto como lo json.loadhace); si tiene archivos grandes, puede modificarlo para que solo lea del archivo según sea necesario sin mucha dificultad.

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

Uso: tal como lo solicitó, es un generador.

Jeremy Roman
fuente
2
Parece que la parte complicada sería asegurarse de que las lecturas de transmisión traigan una cantidad suficiente del archivo para que tenga un objeto completo para decodificar. Así que este es un enfoque simple que funciona si, por ejemplo, asume que los objetos nunca tienen nuevas líneas. Pero a menos que imponga ese tipo de estructura adicional en el archivo, que el OP está tratando de evitar, parece que necesitaría una solución como esa de @Benedict
nealmcb
24

Este es un problema bastante desagradable en realidad porque tiene que transmitir en líneas, pero el patrón coincide en varias líneas contra llaves, pero también el patrón coincide con json. Es una especie de json-preparse seguido de un json parse. Json es, en comparación con otros formatos, fácil de analizar, por lo que no siempre es necesario buscar una biblioteca de análisis, sin embargo, ¿cómo deberíamos resolver estos problemas en conflicto?

¡Generadores al rescate!

La belleza de los generadores para un problema como este es que puede apilarlos uno encima del otro, abstrayendo gradualmente la dificultad del problema mientras mantiene la pereza. También consideré usar el mecanismo para devolver valores a un generador (send ()) pero, afortunadamente, descubrí que no necesitaba usarlo.

Para resolver el primero de los problemas, necesita algún tipo de streamfinditer, como una versión de streaming de re.finditer. Mi intento en esto a continuación extrae líneas según sea necesario (descomente la declaración de depuración para ver) mientras aún devuelve coincidencias. De hecho, lo modifiqué ligeramente para producir líneas no coincidentes y coincidencias (marcadas como 0 o 1 en la primera parte de la tupla producida).

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 eso, entonces es posible hacer coincidir los frenos, tener en cuenta cada vez si los frenos están equilibrados y luego devolver objetos simples o compuestos según corresponda.

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

Esto devuelve tuplas de la siguiente manera:

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

Básicamente, esa es la parte desagradable hecha. Ahora solo tenemos que hacer el nivel final de análisis como mejor nos parezca. Por ejemplo, podemos usar la función iterload de Jeremy Roman (¡Gracias!) Para analizar una sola línea:

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

Pruébalo:

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

Obtengo estos resultados (y si enciende esa línea de depuración, verá que tira de las líneas según sea necesario):

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

Esto no funcionará en todas las situaciones. Debido a la implementación de la jsonbiblioteca, es imposible trabajar completamente correctamente sin volver a implementar el analizador usted mismo.

Benedicto
fuente
8
Si desea hacer esto correctamente, también debe tener cuidado con las llaves y los corchetes dentro de las cadenas. Y luego también tenga cuidado con las citas que se escapan. Antes de que te des cuenta, el "preparador" se volverá casi tan complicado como un analizador JSON completo.
Petr Viktorin
Gracias Jeremy. ¡Fue un buen desafío de pregunta! Sí, Petr, tienes toda la razón, por supuesto :)
Benedict
1
Bien hecho. ¿Se comportará esto correctamente si a los caracteres les gusta "}"y "]"aparecen dentro de cadenas JSON? Creo que esta es una limitación general del análisis con expresiones regulares.
Thomas K
2
Al hurgar, descubrí que la función de análisis principal está construida de tal manera que es imposible usarla correctamente de manera perezosa, por lo que no obtendrá un resultado perfecto sin implementar un analizador completo por su cuenta. Esta respuesta demuestra varias cosas relevantes útiles y maneja muy bien los casos simples.
Jeremy
3
Esta respuesta es horrible y no tengo idea de por qué se vota a favor. El autor admite que en realidad no funciona para todas las entradas, por lo que, por definición, ni siquiera es una respuesta correcta, y utiliza una expresión regular compleja que se calcula , por lo que ni siquiera podemos leer lo que es. ¿De qué sirve una función que a veces da el resultado correcto?
Tom Swirly
10

Creo que una mejor forma de hacerlo sería utilizar una máquina de estado. A continuación se muestra un código de muestra que resolví convirtiendo un código NodeJS en el enlace de abajo a Python 3 (se usó una palabra clave no local solo disponible en Python 3, el código no funcionará en Python 2)

Edit-1: código actualizado y compatible con Python 2

Edit-2: actualizado y agregado una versión solo de Python3 también

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

Versión única de 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

Versión compatible 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

Probándolo

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

La salida de la misma es

[1, 2, '3']
{'name': 'tarun'}
1
2
3
[{'name': 'a', 'data': [1, None, 2]}]
Tarun Lalwani
fuente
¡Buena solución! Lo echaré un vistazo más de cerca más tarde, pero esto es muy prometedor. Pero por lo que vale, preferí la versión solo de Python 3. Usar dicts para todas sus variables locales es un poco incómodo y, por mi parte, estoy feliz de dejar Python 2 en el pasado. ;)
Jeremy
@JeremyBanks, seguro que no sabía a qué versión apuntaba. Ahora he agregado una versión única de Python3 y una compatible con Py2 también en la respuesta para otra persona que todavía puede estar en Python 2
Tarun Lalwani
@JeremyBanks, solo queda 1 día con la recompensa, espero que pueda revisar y proporcionar comentarios sobre la respuesta
Tarun Lalwani
Parece que el único que realmente entendió el problema fue Tarun. La eficiencia del análisis se basa en el número de pasadas que suceden en la entrada. La mayoría de las respuestas usan expresiones regulares o leen una línea de antemano (esto también podría ser peligroso) o, peor aún, fallan el análisis un número desconocido de veces. Lástima que esto no sea parte de Python.
mschonaker
4

Me gustaría dar una solución. El pensamiento clave es "intentar" decodificar: si falla, dele más alimentación; de lo contrario, utilice la información de compensación para preparar la siguiente decodificación.

Sin embargo, el módulo json actual no puede tolerar el ESPACIO en la cabeza de la cadena para decodificar, así que tengo que quitarlos.

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)

========================= He probado varios archivos txt y funciona bien. (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

(en.txt, su inicial)

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

(salida para el caso de prueba de 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']})
wuliang
fuente
3

Aquí está el mío:

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
usuario3542882
fuente
Hola, esto es muy útil, pero ¿podría mostrar cómo puedo usar la clase para cargar el archivo json?
song0089
3

Usé la elegante solución de @ wuilang. El enfoque simple - leer un byte, intentar decodificar, leer un byte, intentar decodificar, ... - funcionó, pero desafortunadamente fue muy lento.

En mi caso, estaba intentando leer objetos JSON "bastante impresos" del mismo tipo de objeto desde un archivo. Esto me permitió optimizar el enfoque; Pude leer el archivo línea por línea, solo decodificando cuando encontré una línea que contenía exactamente "}":

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

Si trabaja con JSON compacto de una por línea que escapa de las líneas nuevas en los literales de cadena, puede simplificar aún más este enfoque de manera segura:

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

Obviamente, estos enfoques simples solo funcionan para tipos muy específicos de JSON. Sin embargo, si se cumplen estas suposiciones, estas soluciones funcionan correcta y rápidamente.

sigpwned
fuente
2

Si usa una instancia json.JSONDecoder, puede usar la raw_decodefunción miembro. Devuelve una tupla de representación de Python del valor JSON y un índice donde se detuvo el análisis. Esto facilita la división (o la búsqueda en un objeto de flujo) de los valores JSON restantes. No estoy tan contento con el ciclo while adicional para omitir el espacio en blanco entre los diferentes valores JSON en la entrada, pero en mi opinión, hace el trabajo.

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 siguiente versión es mucho más corta y se come la parte de la cadena que ya está analizada. Parece que, por alguna razón, una segunda llamada json.JSONDecoder.raw_decode () parece fallar cuando el primer carácter de la cadena es un espacio en blanco, esa es también la razón por la que omito el espacio en blanco en el whileloop anterior ...

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

En la documentación sobre la clase json.JSONDecoder, el método raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders contiene lo siguiente:

Esto se puede usar para decodificar un documento JSON a partir de una cadena que puede tener datos extraños al final.

Y estos datos extraños pueden ser fácilmente otro valor JSON. En otras palabras, el método podría estar escrito con este propósito en mente.

Con input.txt usando la función superior, obtengo el resultado de ejemplo como se presenta en la pregunta original.

hetepeperfan
fuente