Como posso ler preguiçosamente vários valores JSON de um arquivo / fluxo em Python?

101

Gostaria de ler vários objetos JSON de um arquivo / fluxo em Python, um de cada vez. Infelizmente, json.load()apenas .read()s até o final do arquivo; não parece haver nenhuma maneira de usá-lo para ler um único objeto ou para iterar preguiçosamente sobre os objetos.

Há alguma maneira de fazer isso? Usar a biblioteca padrão seria o ideal, mas se houver uma biblioteca de terceiros, eu a usaria.

No momento, estou colocando cada objeto em uma linha separada e usando json.loads(f.readline()), mas realmente prefiro não precisar fazer isso.

Exemplo de uso

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

sessão de exemplo

$ 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
fonte
Você poderia adicionar um exemplo do comportamento que gostaria de objetos aninhados, por favor?
Tim McNamara
@TimMcNamara: O comportamento do objeto aninhado não deve mudar. No entanto, uma vez que alcançamos o final do primeiro objeto de nível superior ( {"foo": ["bar", "baz"]}no meu exemplo), ele deve yieldchegar e então continuar para o próximo ( 1).
Jeremy
1
por que evitar as "linhas json"? É sempre possível serializar um objeto em json de modo que não tenha '\n'(uma única nova linha, não dois caracteres) em sua representação json porque '\n'deve ser escapado dentro de uma string json e, portanto, '\n'pode ser usado apenas para formatação, por exemplo, eu acredito que json.dumps()não t introduzir '\n'por padrão. Esteja ciente de que novas linhas Unicode como U + 0085 podem não ter escape dentro de strings json.
jfs de
2
A biblioteca ijson pode ser útil neste caso. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov
1
O título não deveria ser "Como posso ler preguiçosamente vários valores JSON de um arquivo / fluxo em Python?" Visto que um objeto também é um valor, assim como um json int, string etc., enquanto o inverso não é necessariamente verdadeiro?
hetepeperfan

Respostas:

20

Esta é uma solução muito mais simples. O segredo é tentar, falhar e usar as informações da exceção para analisar corretamente. A única limitação é que o arquivo deve ser pesquisável.

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: acabei de notar que isso só funcionará para Python> = 3.5. Anteriormente, as falhas retornam um ValueError, e você deve analisar a posição da string, por exemplo

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
fonte
Bem-vindo ao Stack Overflow e obrigado pela resposta! Isso é muito mais próximo do que eu esperava encontrar. Devo ser capaz de adaptar isso para os tipos de casos em que estava pensando, mesmo que não forneçam diretamente a busca.
Jeremy
Isso renão vai funcionar - as barras invertidas precisam escapar. Considere uma string bruta r'...'.
Tom Swirly
2
Eu precisava disso para meu próprio trabalho, então criei uma pequena biblioteca python para fazer isso, usando mais ou menos sua técnica com alguns detalhes, e está aqui: pypi.python.org/pypi/Streamy
Tom Swirly
2
Se você usar em ujsonvez de, jsonterá uma grande aceleração
OddNorg
40

JSON geralmente não é muito bom para esse tipo de uso incremental; não há uma maneira padrão de serializar vários objetos para que eles possam ser carregados facilmente um por vez, sem analisar todo o lote.

A solução de objeto por linha que você está usando também é vista em outro lugar. Scrapy chama isso de 'linhas JSON':

Você pode fazer isso um pouco mais Pythonically:

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

Acho que essa é a melhor maneira - não depende de nenhuma biblioteca de terceiros e é fácil de entender o que está acontecendo. Eu o usei em alguns de meus próprios códigos também.

Thomas K
fonte
4
re: "sem forma padrão": não vejo o problema, a sintaxe parece tornar vários objetos consecutivos inequívocos, desde que você tenha um buffer de um caractere. Obrigado por apontar que outras pessoas usam "linhas JSON", eu me sinto menos mal em usar isso agora.
Jeremy
31

Um pouco tarde talvez, mas eu tive exatamente esse problema (bem, mais ou menos). Minha solução padrão para esses problemas é geralmente fazer uma divisão de regex em algum objeto raiz conhecido, mas no meu caso era impossível. A única maneira viável de fazer isso genericamente é implementar um tokenizer adequado .

Depois de não encontrar uma solução genérica o suficiente e com desempenho razoável, acabei fazendo isso sozinho, escrevendo o splitstream módulo. É um pré-tokenizador que entende JSON e XML e divide um fluxo contínuo em vários blocos para análise (no entanto, deixa a análise real para você). Para obter algum tipo de desempenho com isso, ele é escrito como um módulo C.

Exemplo:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)
Krumelur
fonte
Fantástico. Obrigado por compartilhar isso.
Jeremy
Esta é a solução definitiva. Espero que você continue atualizando.
Bartvds
Simplesmente funciona. Obrigado por fornecer um módulo tão útil.
Vinod Sharma
1
Você poderia fazer upload de uma versão compilada .py? Eu tentei construir e instalar o módulo, mas ... ele produz um monte de erros em relação à redefinição de constantes e tal.
SirJames
O módulo é escrito em C. Portá-lo para Python puro é deixado como um exercício para quem está preparado para a tarefa :). Provavelmente será muito lento para o propósito para o qual foi escrito. Se você tiver problemas para compilar, provavelmente precisará instalar o pacote python-dev.
Krumelur
25

Claro que você pode fazer isso. Você apenas tem que atender raw_decodediretamente. Essa implementação carrega todo o arquivo na memória e opera nessa string (da mesma forma que json.loadfaz); se você tiver arquivos grandes, pode modificá-lo para ler apenas o arquivo conforme necessário, sem muita dificuldade.

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

Utilização: tal como solicitou, é um gerador.

Jeremy Roman
fonte
2
Parece que a parte complicada seria garantir que as leituras de streaming tragam o arquivo suficiente para que você tenha um objeto inteiro para decodificar. Portanto, esta é uma abordagem simples que funciona se você, por exemplo, assumir que os objetos nunca possuem novas linhas. Mas, a menos que você imponha esse tipo de estrutura adicional ao arquivo, o que o OP está tentando evitar, parece que você precisa de uma solução como essa de @Benedict
nealmcb
24

Este é um problema bastante desagradável, na verdade, porque você tem que fazer o stream em linhas, mas a correspondência de padrões em várias linhas contra chaves, mas também a correspondência de padrões json. É uma espécie de preparação json seguida por uma análise json. Json é, em comparação com outros formatos, fácil de analisar, então nem sempre é necessário ir para uma biblioteca de análise, no entanto, como devemos resolver esses problemas conflitantes?

Geradores para o resgate!

A beleza dos geradores para um problema como esse é que você pode empilhá-los um em cima do outro, abstraindo gradualmente a dificuldade do problema enquanto mantém a preguiça. Também considerei usar o mecanismo para passar os valores de volta para um gerador (send ()), mas felizmente descobri que não precisava usá-lo.

Para resolver o primeiro dos problemas, você precisa de algum tipo de streamingfinditer, como uma versão de streaming de re.finditer. Minha tentativa de fazer isso abaixo puxa as linhas conforme necessário (descomente a instrução de depuração para ver) enquanto ainda retorna correspondências. Na verdade, eu o modifiquei ligeiramente para produzir linhas não correspondidas, bem como correspondências (marcadas como 0 ou 1 na primeira parte da tupla produzida).

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]

Com isso, é possível combinar até chaves, levar em conta cada vez se as chaves estão balanceadas e retornar objetos simples ou compostos, conforme apropriado.

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

Isso retorna tuplas da seguinte maneira:

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

Basicamente, essa é a parte desagradável feita. Agora só temos que fazer o nível final de análise conforme acharmos adequado. Por exemplo, podemos usar a função iterload de Jeremy Roman (Obrigado!) Para fazer a análise de uma única linha:

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

Teste-o:

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

Recebo estes resultados (e se você ativar a linha de depuração, verá que ela puxa as linhas conforme necessário):

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

Isso não funcionará em todas as situações. Devido à implementação da jsonbiblioteca, é impossível funcionar totalmente corretamente sem reimplementar o analisador você mesmo.

Benedict
fonte
8
Se você quiser fazer isso corretamente, também deve estar atento a chaves e colchetes dentro das strings. E também tome cuidado com as citações que escapam. Antes que você perceba, o “pré-carregador” ficará quase tão complicado quanto um analisador JSON completo.
Petr Viktorin,
Obrigado Jeremy. Foi um belo desafio de pergunta! Sim, Petr - você está absolutamente certo, é claro :)
Bento
1
Bem feito. Isso se comportará corretamente se os caracteres forem semelhantes "}"e "]"ocorrerem dentro de strings JSON? Acho que essa é uma limitação geral da análise com regex.
Thomas K,
2
Ao dar uma olhada, descobri que a função de análise principal é construída de tal forma que é impossível usá-la apropriadamente de forma preguiçosa, então você não obterá um resultado perfeito sem implementar um analisador completo sozinho. Esta resposta demonstra várias coisas úteis relevantes e trata bem casos simples.
Jeremy
3
Essa resposta é horrível e não tenho ideia de por que foi votada a favor. O autor admite que na verdade não funciona para todas as entradas, então, por definição, nem é uma resposta certa e usa uma expressão regular complexa que é calculada , então nem podemos ler o que é. De que serve uma função que às vezes dá o resultado certo?
Tom Swirly de
10

Acredito que a melhor maneira de fazer isso seria usar uma máquina de estado. Abaixo está um código de amostra que desenvolvi convertendo um código NodeJS no link abaixo para Python 3 (palavra-chave não local usada disponível apenas em Python 3, o código não funcionará em Python 2)

Edit-1: Código atualizado e compatível com Python 2

Edit-2: Atualizado e adicionado uma versão somente Python3 também

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

Python 3 apenas versão

# 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

Versão compatível com 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

Testando

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

A saída do mesmo é

[1, 2, '3']
{'name': 'tarun'}
1
2
3
[{'name': 'a', 'data': [1, None, 2]}]
Tarun Lalwani
fonte
Ótima solução! Vou olhar mais de perto mais tarde, mas isso é muito promissor. Mas, pelo que vale a pena, eu preferi a versão Python 3 apenas. Usar dicts para todas as suas variáveis ​​locais é meio estranho, e eu, pelo menos, fico feliz em deixar o Python 2 no passado. ;)
Jeremy
@JeremyBanks, claro que não sabia qual versão você almejava. Agora eu adicionei uma versão somente Python3 e compatível com Py2 também na resposta para outra pessoa que ainda pode estar no Python 2
Tarun Lalwani
@JeremyBanks, falta apenas 1 dia para o bounty, espero que você possa revisar e fornecer feedback sobre a resposta
Tarun Lalwani
Parece que o único que realmente entendeu o problema foi Tarun. A eficiência da análise depende do número de passagens que acontecem na entrada. A maioria das respostas usa regex ou lê uma linha de antemão (isso também pode ser perigoso) ou, pior, falha na análise um número desconhecido de vezes. Pena que isso não faz parte do Python.
mschonaker
4

Eu gostaria de fornecer uma solução. O pensamento principal é "tentar" decodificar: se falhar, forneça mais alimentação, caso contrário, use as informações de deslocamento para preparar a próxima decodificação.

No entanto, o módulo json atual não pode tolerar que ESPAÇO no cabeçalho da string seja decodificado, então eu tenho que retirá-los.

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)

============================= Eu testei vários arquivos txt e funciona bem. (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, sua inicial)

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

(saída para o caso de teste 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
fonte
3

Aqui está o meu:

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
fonte
Olá, isso é muito útil, mas você poderia mostrar como posso usar a classe para carregar o arquivo json?
song0089
3

Usei a solução elegante de @wuilang. A abordagem simples - ler um byte, tentar decodificar, ler um byte, tentar decodificar, ... - funcionou, mas infelizmente era muito lenta.

No meu caso, eu estava tentando ler objetos JSON "bem impressos" do mesmo tipo de objeto de um arquivo. Isso me permitiu otimizar a abordagem; Pude ler o arquivo linha por linha, decodificando apenas quando encontrei uma linha que continha exatamente "}":

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 você estiver trabalhando com um JSON compacto por linha que escapa de novas linhas em literais de string, poderá simplificar ainda mais essa abordagem com segurança:

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

Obviamente, essas abordagens simples funcionam apenas para tipos muito específicos de JSON. No entanto, se essas suposições forem válidas, essas soluções funcionarão correta e rapidamente.

assinado
fonte
2

Se você usar uma instância json.JSONDecoder, poderá usar a raw_decodefunção de membro. Ele retorna uma tupla de representação python do valor JSON e um índice de onde a análise parou. Isso torna mais fácil dividir (ou buscar em um objeto de fluxo) os valores JSON restantes. Não estou muito feliz com o loop while extra para pular o espaço em branco entre os diferentes valores JSON na entrada, mas na minha opinião ele dá conta do recado.

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

A próxima versão é muito mais curta e come a parte da string que já foi analisada. Parece que, por algum motivo, uma segunda chamada json.JSONDecoder.raw_decode () parece falhar quando o primeiro caractere na string é um espaço em branco, essa também é a razão pela qual pulo o espaço em branco no whileloop acima ...

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

Na documentação sobre a classe json.JSONDecoder, o método raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders contém o seguinte:

Isso pode ser usado para decodificar um documento JSON de uma string que pode conter dados estranhos no final.

E esses dados estranhos podem facilmente ser outro valor JSON. Em outras palavras, o método pode ser escrito com esse propósito em mente.

Com o input.txt usando a função superior, obtenho a saída de exemplo apresentada na questão original.

Hetepeperfan
fonte
0

Você pode usar https://pypi.org/project/json-stream-parser/ exatamente para essa finalidade.

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

resultado

{'foo': ['bar', 'baz']}
1
2
[]
4
5
6
usuário5203
fonte