Jak mogę leniwie odczytać wiele wartości JSON z pliku / strumienia w Pythonie?


101

Chciałbym czytać wiele obiektów JSON z pliku / strumienia w Pythonie, po jednym naraz. Niestety json.load()tylko .read()do końca pliku; wydaje się, że nie ma sposobu, aby użyć go do odczytania pojedynczego obiektu lub leniwego iterowania po obiektach.

Czy jest na to sposób? Korzystanie z biblioteki standardowej byłoby idealne, ale jeśli istnieje biblioteka innej firmy, użyłbym jej zamiast tego.

W tej chwili umieszczam każdy obiekt w osobnej linii i używam json.loads(f.readline()), ale naprawdę wolałbym nie potrzebować tego robić.

Przykładowe zastosowanie

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

przykładowa sesja

$ 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

Czy możesz dodać przykład zachowania, które chciałbyś, od zagnieżdżonych obiektów?
Tim McNamara

@TimMcNamara: zachowanie obiektu zagnieżdżonego nie powinno się zmieniać. Jednak gdy już dotarliśmy do końca pierwszego obiektu najwyższego poziomu ( {"foo": ["bar", "baz"]}w moim przykładzie), powinien yieldto zrobić, a następnie przejść do następnego ( 1).
Jeremy

1
dlaczego unikać „linii json”? Zawsze jest możliwe serializowanie obiektu do json tak, że nie ma on '\n'(pojedynczej nowej linii, a nie dwóch znaków) w swojej reprezentacji json, ponieważ '\n'musi być umieszczony wewnątrz łańcucha json i dlatego '\n'może być używany tylko do formatowania np. Uważam, że json.dumps()nie t wprowadzaj '\n'domyślnie. Uważaj, że znaki nowej linii Unicode, takie jak U + 0085, mogą znajdować się wewnątrz ciągów json.
jfs

2
W tym przypadku przydatna może być biblioteka ijson . pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov

1
Czy tytuł nie powinien brzmieć „Jak mogę leniwie odczytać wiele wartości JSON z pliku / strumienia w Pythonie?” Ponieważ obiekt jest również wartością, podobnie jak json int, string itp., Podczas gdy odwrotność nie jest konieczna?
hetepeperfan

Odpowiedzi:


20

Oto dużo, dużo prostsze rozwiązanie. Sekret polega na tym, aby spróbować, nie powiodło się i użyć informacji z wyjątku do poprawnego przeanalizowania. Jedynym ograniczeniem jest to, że plik musi być możliwy do przeszukania.

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

Edycja: właśnie zauważyłem, że będzie to działać tylko dla Pythona> = 3.5. Wcześniej błędy zwracają błąd ValueError i trzeba wyodrębnić pozycję z ciągu, np

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

Witamy w Stack Overflow i dziękujemy za odpowiedź! To znacznie bliżej tego, co chciałem znaleźć. Powinienem być w stanie dostosować to do typów przypadków, o których myślałem, nawet jeśli nie zapewniają one bezpośrednio poszukiwania.
Jeremy

To renie zadziała - ukośniki odwrotne muszą uciec. Rozważmy surowy ciąg r'...'.
Tom Swirly

2
Potrzebowałem tego do mojej własnej pracy, więc stworzyłem małą bibliotekę Pythona, aby to zrobić, używając mniej więcej twojej techniki z pewnymi szczegółami, i jest tutaj: pypi.python.org/pypi/Streamy
Tom Swirly

2
Jeśli użyjesz ujsonzamiast jsonciebie, uzyskasz ogromne przyspieszenie
OddNorg

40

JSON generalnie nie jest zbyt dobry do tego rodzaju przyrostowego użycia; nie ma standardowego sposobu serializacji wielu obiektów, aby można je było łatwo ładować pojedynczo, bez analizowania całej partii.

Rozwiązanie obiektu na linię, którego używasz, jest również widoczne w innym miejscu. Scrapy nazywa to „liniami JSON”:

Możesz to zrobić nieco bardziej w Pythonie:

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

Myślę, że to najlepszy sposób - nie opiera się na bibliotekach innych firm i łatwo jest zrozumieć, co się dzieje. Użyłem go również w moim własnym kodzie.


4
re: "brak standardowego sposobu": nie widzę problemu, składnia wydaje się, że wiele kolejnych obiektów jest jednoznacznych, o ile masz jednoznakowy bufor. Dziękuję za wskazanie, że inni ludzie używają „linii JSON”, teraz jest mi mniej przykro z tego powodu.
Jeremy

31

Może trochę późno, ale miałem dokładnie ten problem (cóż, mniej więcej). Moim standardowym rozwiązaniem tych problemów jest zwykle podzielenie wyrażeń regularnych na dobrze znanym obiekcie root, ale w moim przypadku było to niemożliwe. Jedynym możliwym sposobem zrobienia tego w sposób ogólny jest zaimplementowanie odpowiedniego tokenizera .

Po tym, jak nie znalazłem wystarczająco ogólnego i dość dobrego rozwiązania, zakończyłem to samodzielnie, pisząc plik splitstream moduł. Jest to pre-tokenizer, który rozumie JSON i XML i dzieli ciągły strumień na wiele fragmentów do analizy (pozostawia jednak faktyczną analizę Tobie). Aby uzyskać z tego jakąś wydajność, jest napisany jako moduł C.

Przykład:

from splitstream import splitfile

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

To cudownie. Dzięki za udostępnienie.
Jeremy

To jest ostateczne rozwiązanie. Mam nadzieję, że nadal go aktualizujesz.
Bartvds

Po prostu działa. Dzięki za udostępnienie tak przydatnego modułu.
Vinod Sharma

1
Czy możesz przesłać skompilowaną wersję .py? Próbowałem zbudować i zainstalować moduł, ale ... generuje kilka błędów dotyczących przedefiniowania stałych i tym podobnych.
SirJames

Moduł jest napisany w C. Przeniesienie go do czystego Pythona jest pozostawione jako ćwiczenie każdemu, kto jest gotowy do wykonania zadania :). Prawdopodobnie będzie jednak zbyt wolny do celu, dla którego został napisany. Jeśli masz problemy z kompilacją, prawdopodobnie musisz zainstalować pakiet python-dev.
Krumelur

25

Jasne, że możesz to zrobić. Musisz tylko wziąć raw_decodebezpośrednio. Ta implementacja ładuje cały plik do pamięci i operuje na tym ciągu (podobnie jak json.load); jeśli masz duże pliki, możesz je zmodyfikować, aby w razie potrzeby czytać tylko z pliku, bez większych trudności.

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

Sposób użycia: tak jak prosiłeś, jest to generator.


2
Wydaje się, że najtrudniejszą częścią byłoby upewnienie się, że odczyty strumieniowe dostarczają wystarczającą ilość pliku, abyś miał cały obiekt do zdekodowania. Jest to więc proste podejście, które działa, jeśli np. Założysz, że obiekty nigdy nie zawierają znaków nowej linii. Ale jeśli nie narzucisz tego rodzaju dodatkowej struktury na plik, którego OP stara się uniknąć, wydaje się, że potrzebujesz takiego rozwiązania z @Benedict
nealmcb

24

Jest to dość nieprzyjemny problem, ponieważ musisz przesyłać strumieniowo w wierszach, ale dopasowywanie wzorca w wielu wierszach z nawiasami klamrowymi, ale także json z dopasowaniem do wzorca. Jest to rodzaj json-preparse, po którym następuje analiza json. Json, w porównaniu do innych formatów, jest łatwy do przeanalizowania, więc nie zawsze jest konieczne sięganie po bibliotekę analizującą, niemniej jednak, jak rozwiązać te sprzeczne problemy?

Generatory na ratunek!

Piękno generatorów dla takiego problemu polega na tym, że można je układać jeden na drugim, stopniowo odejmując trudność problemu, zachowując jednocześnie lenistwo. Rozważałem również użycie mechanizmu przekazywania wartości z powrotem do generatora (send ()), ale na szczęście okazało się, że nie muszę tego używać.

Aby rozwiązać pierwszy z problemów, potrzebujesz jakiegoś narzędzia do wyszukiwania strumieniowego, jako strumieniowej wersji re.finditera. Poniższa próba polega na wciągnięciu wierszy w razie potrzeby (odkomentuj instrukcję debugowania, aby zobaczyć), jednocześnie zwracając dopasowania. Właściwie zmodyfikowałem go nieco, aby uzyskać niedopasowane wiersze, a także dopasowania (oznaczone jako 0 lub 1 w pierwszej części uzyskanej krotki).

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]

Dzięki temu możliwe jest dopasowanie do nawiasów klamrowych, za każdym razem uwzględnienie, czy nawiasy są zrównoważone, a następnie zwrócenie odpowiednio obiektów prostych lub złożonych.

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

Zwraca krotki w następujący sposób:

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

Zasadniczo to jest paskudna część zrobiona. Teraz musimy po prostu wykonać ostatni poziom analizy, jaki uznamy za stosowny. Na przykład możemy użyć funkcji iterload Jeremy'ego Romana (dzięki!), Aby przeanalizować pojedynczą linię:

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

Sprawdź to:

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

Otrzymuję następujące wyniki (a jeśli włączysz tę linię debugowania, zobaczysz, że w razie potrzeby wciąga linie):

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

To nie zadziała we wszystkich sytuacjach. Ze względu na implementację jsonbiblioteki niemożliwe jest całkowicie poprawne działanie bez ponownego zaimplementowania parsera.


8
Jeśli chcesz to zrobić poprawnie, musisz również uważać na nawiasy klamrowe i nawiasy w łańcuchach. A potem uważaj również na uniknięte cytaty. Zanim się zorientujesz, „preparser” stanie się prawie tak skomplikowany, jak pełny parser JSON.
Petr Viktorin

Dzięki Jeremy. To było miłe wyzwanie w postaci pytania! Tak Petr - masz oczywiście całkowitą rację :)
Benedict

1
Ładnie wykonane. Czy będzie to działać poprawnie, jeśli znaki takie jak "}"i "]"występują w ciągach JSON? Myślę, że jest to ogólne ograniczenie analizowania za pomocą wyrażenia regularnego.
Thomas K

2
Przeglądając się, stwierdziłem, że główna funkcja parsująca jest zbudowana w taki sposób, że nie można jej leniwie używać, więc nie uzyskasz idealnego wyniku bez samodzielnego wdrożenia pełnego parsera. Ta odpowiedź pokazuje kilka przydatnych, istotnych rzeczy i ładnie radzi sobie z prostymi przypadkami.
Jeremy

3
Ta odpowiedź jest okropna i nie mam pojęcia, dlaczego jest przegłosowana. Autor przyznaje, że tak naprawdę nie działa dla wszystkich danych wejściowych, więc z definicji nie jest to nawet poprawna odpowiedź i używa złożonego wyrażenia regularnego, które jest obliczane , więc nie możemy nawet odczytać, co to jest. Po co jest funkcja, która czasami daje właściwy wynik?
Tom Swirly

10

Uważam, że lepszym sposobem na zrobienie tego byłoby użycie maszyny stanowej. Poniżej znajduje się przykładowy kod, który opracowałem, konwertując kod NodeJS na poniższy link do Python 3 (użyte słowo kluczowe nonlocal dostępne tylko w Pythonie 3, kod nie będzie działał w Pythonie 2)

Edit-1: Zaktualizowany i zgodny kod z Pythonem 2

Edit-2: Zaktualizowano i dodano również wersję tylko dla Pythona3

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

Tylko wersja 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

Wersja zgodna z 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

Testuję to

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

Wynik tego samego jest

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

Niezłe rozwiązanie! Przyjrzę się bliżej później, ale jest to bardzo obiecujące. Ale ze względu na to, co jest warte, wolałem wersję tylko dla Pythona 3. Używanie dykt dla wszystkich zmiennych lokalnych jest trochę niezręczne, a ja z przyjemnością opuszczam Python 2 w przeszłości. ;)
Jeremy

@JeremyBanks, na pewno nie wiedziałem, do której wersji jesteś skierowany. Teraz dodałem tylko wersję Python3 i wersję kompatybilną z Py2 również w odpowiedzi dla kogoś, kto może nadal korzystać z Pythona 2
Tarun Lalwani

@JeremyBanks, został tylko 1 dzień z nagrodą, mam nadzieję, że możesz przejrzeć i przekazać opinię na temat odpowiedzi
Tarun Lalwani,

Wygląda na to, że jedynym, który naprawdę zrozumiał problem, był Tarun. Efektywność analizowania zależy od liczby przebiegów, które mają miejsce na wejściu. Większość odpowiedzi używa wyrażenia regularnego lub odczytuje wcześniej linię (może to być niebezpieczne) lub, co gorsza, nie udaje się przeanalizować nieznanej liczby razy. Szkoda, że ​​to nie jest część Pythona.
mschonaker

4

Chciałbym przedstawić rozwiązanie. Kluczową myślą jest „próba” dekodowania: jeśli się nie powiedzie, podaj więcej sygnału, w przeciwnym razie użyj informacji o przesunięciu do przygotowania następnego dekodowania.

Jednak obecny moduł json nie może tolerować SPACJI w nagłówku ciągu do zdekodowania, więc muszę je usunąć.

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)

========================= Przetestowałem kilka plików txt i działa dobrze. (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, twoja inicjał)

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

(wyjście do przypadku testowego Benedicta)

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

To moje:

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

Cześć, jest to bardzo przydatne, ale czy możesz pokazać, jak mogę użyć tej klasy do załadowania pliku json?
song0089

3

Użyłem eleganckiego rozwiązania @ wuilang. Proste podejście - przeczytaj bajt, spróbuj zdekodować, przeczytaj bajt, spróbuj dekodować, ... - działało, ale niestety było bardzo wolne.

W moim przypadku próbowałem odczytać „ładnie wydrukowane” obiekty JSON tego samego typu z pliku. Pozwoliło mi to zoptymalizować podejście; Mogłem odczytać plik wiersz po wierszu, dekodując tylko wtedy, gdy znalazłem wiersz zawierający dokładnie „}”:

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

Jeśli zdarza się, że pracujesz z kompaktowym kodem JSON (jeden w każdym wierszu), który wymyka znaki nowej linii w literałach ciągów, możesz bezpiecznie uprościć to podejście jeszcze bardziej:

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

Oczywiście te proste metody działają tylko w przypadku bardzo konkretnych rodzajów JSON. Jeśli jednak te założenia się utrzymają, rozwiązania te działają poprawnie i szybko.


2

Jeśli używasz instancji json.JSONDecoder, możesz użyć raw_decodefunkcji składowej . Zwraca krotkę reprezentacji w języku Python wartości JSON i indeksu, do którego zatrzymano analizowanie. Ułatwia to wycinanie (lub wyszukiwanie w obiekcie strumienia) pozostałych wartości JSON. Nie jestem tak zadowolony z dodatkowej pętli while, aby pomijać białe znaki między różnymi wartościami JSON w danych wejściowych, ale moim zdaniem wykonuje to zadanie.

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

Następna wersja jest znacznie krótsza i zjada część ciągu, która jest już przeanalizowana. Wygląda na to, że z jakiegoś powodu drugie wywołanie json.JSONDecoder.raw_decode () wydaje się nie powieść, gdy pierwszy znak w ciągu jest białą spacją, jest to również powód, dla którego pomijam białe znaki w whileloop powyżej ...

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

W dokumentacji dotyczącej klasy json.JSONDecoder metoda raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders zawiera:

Może to służyć do dekodowania dokumentu JSON z ciągu, który może zawierać zbędne dane na końcu.

Te zbędne dane mogą z łatwością być kolejną wartością JSON. Innymi słowy, metoda może być napisana w tym celu.

Z input.txt używając górnej funkcji otrzymuję przykładowe dane wyjściowe przedstawione w pierwotnym pytaniu.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.