Bagaimana saya bisa dengan malas membaca beberapa nilai JSON dari file / stream dengan Python?

101

Saya ingin membaca beberapa objek JSON dari file / stream dengan Python, satu per satu. Sayangnya json.load()hanya .read()sampai akhir file; sepertinya tidak ada cara untuk menggunakannya untuk membaca satu objek atau dengan malas mengulanginya di atas objek.

Apakah ada cara untuk melakukan ini? Menggunakan pustaka standar akan ideal, tetapi jika ada pustaka pihak ketiga, saya akan menggunakannya sebagai gantinya.

Saat ini saya meletakkan setiap objek pada baris terpisah dan menggunakan json.loads(f.readline()), tetapi saya benar-benar memilih untuk tidak melakukan ini.

Contoh Penggunaan

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

sesi contoh

$ 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
sumber
Bisakah Anda menambahkan contoh perilaku yang Anda inginkan dari objek bersarang?
Tim McNamara
@TimMcNamara: Perilaku objek bersarang tidak boleh berubah. Namun, setelah kita mencapai akhir objek tingkat atas pertama ( {"foo": ["bar", "baz"]}dalam contoh saya), itu harus yielditu dan kemudian melanjutkan ke yang berikutnya ( 1).
Jeremy
1
mengapa menghindari "garis json"? Itu selalu mungkin untuk menserialisasikan objek ke dalam json sedemikian rupa sehingga tidak memiliki '\n'(satu baris baru, bukan dua karakter) dalam representasi jsonnya karena '\n'harus di-escape di dalam string json dan oleh karena itu '\n'dapat digunakan untuk pemformatan hanya misalnya, saya percaya json.dumps()tidak ' t perkenalkan '\n'secara default. Berhati-hatilah karena baris baru Unicode seperti U + 0085 mungkin terhapus di dalam string json.
jfs
2
The ijson perpustakaan dapat berguna dalam hal ini. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov
1
Bukankah seharusnya judulnya adalah "Bagaimana saya bisa dengan malas membaca beberapa nilai JSON dari file / streaming dengan Python?" Karena objek adalah nilai juga seperti json int, string, dll. Sedangkan sebaliknya tidak perlu benar?
hetepeperfan

Jawaban:

20

Inilah solusi yang jauh lebih sederhana. Rahasianya adalah mencoba, gagal, dan menggunakan informasi dalam pengecualian untuk mengurai dengan benar. Satu-satunya batasan adalah file harus dapat dicari.

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

Edit: baru saja perhatikan bahwa ini hanya akan berfungsi untuk Python> = 3.5. Untuk sebelumnya, kegagalan mengembalikan ValueError, dan Anda harus mengurai posisi dari string, misalnya

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
sumber
Selamat datang di Stack Overflow dan terima kasih atas jawabannya! Itu lebih dekat dengan apa yang saya harapkan untuk ditemukan. Saya harus dapat menyesuaikan ini untuk jenis kasus yang saya pikirkan, bahkan jika mereka tidak secara langsung memberikan pencarian.
Jeremy
Itu retidak akan berhasil - garis miring terbalik harus dilepaskan. Pertimbangkan string mentah r'...'.
Tom Swirly
2
Saya membutuhkan ini untuk pekerjaan saya sendiri, jadi saya saya membuat pustaka python kecil untuk melakukan ini, menggunakan lebih atau kurang teknik Anda dengan beberapa detail, dan itu di sini: pypi.python.org/pypi/Streamy
Tom
2
Jika Anda menggunakan ujsonalih-alih jsonAnda akan mendapatkan percepatan besar
OddNorg
40

JSON umumnya tidak terlalu baik untuk penggunaan bertahap semacam ini; tidak ada cara standar untuk membuat serialisasi banyak objek sehingga dapat dengan mudah dimuat satu per satu, tanpa mengurai seluruhnya.

Solusi objek per baris yang Anda gunakan juga terlihat di tempat lain. Scrapy menyebutnya 'garis JSON':

Anda dapat melakukannya sedikit lebih banyak secara Python:

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

Saya pikir ini tentang cara terbaik - tidak bergantung pada perpustakaan pihak ketiga mana pun, dan mudah untuk memahami apa yang sedang terjadi. Saya telah menggunakannya di beberapa kode saya juga.

Thomas K
sumber
4
re: "tidak ada cara standar": Saya tidak melihat masalahnya, sintaksnya tampaknya membuat beberapa objek berurutan tidak ambigu selama Anda memiliki buffer satu karakter. Terima kasih telah menunjukkan bahwa orang lain menggunakan "garis JSON", saya merasa tidak terlalu buruk tentang menggunakannya untuk saat ini.
Jeremy
31

Mungkin agak terlambat, tetapi saya memiliki masalah yang sama persis (yah, kurang lebih). Solusi standar saya untuk masalah ini biasanya hanya melakukan pemisahan regex pada beberapa objek root yang terkenal, tetapi dalam kasus saya itu tidak mungkin. Satu-satunya cara yang layak untuk melakukan ini secara umum adalah dengan mengimplementasikan tokenizer yang tepat .

Setelah tidak menemukan solusi yang cukup umum dan berkinerja cukup baik, saya akhirnya melakukan ini sendiri, menulis splitstreammodul. Ini adalah pra-tokenizer yang memahami JSON dan XML dan membagi aliran berkelanjutan menjadi beberapa bagian untuk penguraian (meskipun demikian, penguraian yang sebenarnya terserah Anda). Untuk mendapatkan beberapa jenis kinerja darinya, itu ditulis sebagai modul C.

Contoh:

from splitstream import splitfile

for jsonstr in splitfile(sys.stdin, format="json")):
    yield json.loads(jsonstr)
Krumelur
sumber
Itu luar biasa. Terima kasih telah membagikannya.
Jeremy
Ini adalah solusi yang pasti. Saya harap Anda terus memperbaruinya.
Bartvds
Ini bekerja dengan mudah. Terima kasih telah menyediakan modul yang sangat berguna.
Vinod Sharma
1
Bisakah Anda mengunggah versi .py yang dikompilasi? Saya sudah mencoba membangun dan menginstal modul tetapi ... itu menghasilkan banyak kesalahan terkait mendefinisikan ulang konstanta dan semacamnya.
SirJames
Modul ini ditulis dalam C. Melakukan porting ke Python murni sebagai latihan bagi siapa pun yang siap untuk tugas tersebut :). Ini kemungkinan akan terlalu lambat untuk tujuan penulisannya. Jika Anda mengalami kesulitan dalam mengompilasi, Anda mungkin perlu menginstal paket python-dev.
Krumelur
25

Tentu kamu bisa melakukan ini. Anda hanya perlu mengambilnya raw_decodesecara langsung. Implementasi ini memuat seluruh file ke dalam memori dan beroperasi pada string itu (seperti json.loadhalnya); jika Anda memiliki file besar, Anda dapat memodifikasinya menjadi hanya membaca dari file seperlunya tanpa banyak kesulitan.

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

Penggunaan: seperti yang Anda minta, ini adalah generator.

Jeremy Roman
sumber
2
Tampaknya bagian yang rumit adalah memastikan bahwa streaming membaca membawa cukup banyak file sehingga Anda memiliki seluruh objek untuk didekode. Jadi ini adalah pendekatan sederhana yang bekerja jika Anda misalnya menganggap objek tidak pernah memiliki baris baru di dalamnya. Tetapi kecuali Anda memaksakan struktur tambahan semacam itu pada file, yang coba dihindari oleh OP, tampaknya Anda memerlukan solusi seperti itu dari @Benedict
nealmcb
24

Ini adalah masalah yang cukup buruk sebenarnya karena Anda harus melakukan streaming dalam garis, tetapi pola cocok di beberapa garis dengan tanda kurung, tetapi juga pola yang cocok json. Ini semacam json-preparse diikuti oleh json parse. Json, dibandingkan dengan format lain, mudah diurai sehingga tidak selalu perlu menggunakan parsing library, namun, bagaimana cara kami menyelesaikan masalah yang bertentangan ini?

Generator untuk menyelamatkan!

Keindahan generator untuk masalah seperti ini adalah Anda dapat menumpuknya satu sama lain secara bertahap menghilangkan kesulitan masalah sambil mempertahankan kemalasan. Saya juga mempertimbangkan untuk menggunakan mekanisme untuk mengirimkan kembali nilai ke generator (send ()) tetapi untungnya ternyata saya tidak perlu menggunakannya.

Untuk mengatasi masalah pertama, Anda memerlukan semacam streamingfinditer, sebagai versi streaming dari re.finditer. Upaya saya di bawah ini menarik baris sesuai kebutuhan (hapus komentar pernyataan debug untuk melihat) sementara masih mengembalikan pertandingan. Saya benar-benar kemudian memodifikasinya sedikit untuk menghasilkan garis yang tidak cocok serta kecocokan (ditandai sebagai 0 atau 1 di bagian pertama dari tupel yang dihasilkan).

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]

Dengan itu, maka dimungkinkan untuk mencocokkan hingga kurung kurawal, memperhitungkan setiap kali apakah kurung siku seimbang, dan kemudian mengembalikan objek sederhana atau gabungan yang sesuai.

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

Ini mengembalikan tupel sebagai berikut:

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

Pada dasarnya itulah bagian buruk yang dilakukan. Kita sekarang hanya perlu melakukan tingkat akhir penguraian sesuai keinginan kita. Misalnya kita dapat menggunakan fungsi iterload Jeremy Roman (Terima kasih!) Untuk melakukan parsing untuk satu baris:

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

Menguji:

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

Saya mendapatkan hasil ini (dan jika Anda mengaktifkan baris debug itu, Anda akan melihatnya menarik baris sesuai kebutuhan):

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

Ini tidak akan berhasil untuk semua situasi. Karena penerapan jsonpustaka, mustahil untuk bekerja sepenuhnya dengan benar tanpa menerapkan ulang parser sendiri.

Benediktus
sumber
8
Jika Anda ingin melakukan ini dengan benar, Anda juga perlu memperhatikan tanda kurung dan tanda kurung di dalam string. Dan kemudian hati-hati juga dengan kutipan yang lolos. Sebelum Anda menyadarinya, "preparser" akan menjadi hampir serumit pengurai JSON lengkap.
Petr Viktorin
Terima kasih Jeremy. Itu adalah tantangan pertanyaan yang bagus! Ya Petr - Anda tentu saja benar :)
Benediktus
1
Selamat. Akankah ini berperilaku dengan benar jika karakter suka "}"dan "]"terjadi di dalam string JSON? Saya rasa ini adalah batasan umum parsing dengan regex.
Thomas K
2
Ketika melihat-lihat, saya menemukan bahwa fungsi penguraian utama dibuat sedemikian rupa sehingga tidak mungkin untuk menggunakannya dengan benar dengan malas, jadi Anda tidak akan mendapatkan hasil yang sempurna tanpa menerapkan parser lengkap sendiri. Jawaban ini menunjukkan beberapa hal relevan yang berguna, dan menangani kasus sederhana dengan baik.
Jeremy
3
Jawaban ini mengerikan dan saya tidak tahu mengapa itu dipilih. Penulis mengakui bahwa itu tidak benar-benar berfungsi untuk semua input sehingga menurut definisi itu bahkan bukan jawaban yang benar, dan itu menggunakan ekspresi reguler kompleks yang dihitung , jadi kita bahkan tidak bisa membaca apa itu. Apa gunanya fungsi yang terkadang memberikan hasil yang benar?
Tom Swirly
10

Saya yakin cara yang lebih baik untuk melakukannya adalah dengan menggunakan mesin negara. Di bawah ini adalah contoh kode yang saya kerjakan dengan mengonversi kode NodeJS pada tautan di bawah ini ke Python 3 (menggunakan kata kunci nonlokal hanya tersedia di Python 3, kode tidak akan berfungsi pada Python 2)

Edit-1: Memperbarui dan membuat kode kompatibel dengan Python 2

Edit-2: Memperbarui dan menambahkan versi hanya Python3 juga

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

Versi hanya 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 yang kompatibel dengan 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

Mengujinya

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

Outputnya adalah

[1, 2, '3']
{'name': 'tarun'}
1
2
3
[{'name': 'a', 'data': [1, None, 2]}]
Tarun Lalwani
sumber
Solusi bagus! Saya akan melihat lebih dekat nanti tapi ini sangat menjanjikan. Tapi untuk apa nilainya, saya lebih suka versi Python 3-only. Menggunakan dicts untuk semua variabel lokal Anda agak canggung, dan saya dengan senang hati meninggalkan Python 2 di masa lalu. ;)
Jeremy
@JeremyBanks, tentu saya tidak tahu versi mana yang Anda targetkan. Sekarang saya telah menambahkan versi hanya Python3 dan yang kompatibel dengan Py2 juga sebagai jawaban untuk orang lain yang mungkin masih menggunakan Python 2
Tarun Lalwani
@JeremyBanks, tinggal 1 hari lagi dengan bountynya, semoga bisa mereview dan memberikan tanggapan atas jawabannya
Tarun Lalwani
Tampaknya satu-satunya yang benar-benar memahami masalahnya adalah Tarun. Efisiensi penguraian bergantung pada jumlah lintasan yang terjadi pada input. Sebagian besar jawaban menggunakan regex atau membaca baris sebelumnya (ini juga bisa berbahaya) atau, lebih buruk, gagal menguraikan beberapa kali. Sayang sekali ini bukan bagian dari Python.
mschonaker
4

Saya ingin memberikan solusi. Pikiran utamanya adalah "mencoba" untuk memecahkan kode: jika gagal, berikan lebih banyak umpan, jika tidak gunakan informasi offset untuk mempersiapkan pendekodean berikutnya.

Namun modul json saat ini tidak dapat mentolerir SPACE di head of string untuk didekodekan, jadi saya harus melepasnya.

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)

========================= Saya telah menguji beberapa file txt, dan berfungsi dengan baik. (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, inisial Anda)

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

(keluaran untuk testcase 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
sumber
3

Ini milik saya:

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
pengguna3542882
sumber
Hai, ini sangat berguna tetapi bisakah Anda menunjukkan bagaimana saya dapat menggunakan kelas untuk memuat file json?
song0089
3

Saya menggunakan solusi elegan @ wuilang. Pendekatan sederhana - membaca satu byte, mencoba memecahkan kode, membaca satu byte, mencoba memecahkan kode, ... - berhasil, tetapi sayangnya itu sangat lambat.

Dalam kasus saya, saya mencoba membaca objek JSON yang "dicetak dengan cantik" dari jenis objek yang sama dari sebuah file. Ini memungkinkan saya untuk mengoptimalkan pendekatan; Saya bisa membaca file baris demi baris, hanya mendekode ketika saya menemukan baris yang berisi persis "}":

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

Jika Anda kebetulan bekerja dengan JSON kompak satu per baris yang lolos dari baris baru dalam literal string, Anda dapat lebih menyederhanakan pendekatan ini dengan aman:

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

Jelas, pendekatan sederhana ini hanya bekerja untuk jenis JSON yang sangat spesifik. Namun, jika asumsi ini berlaku, solusi ini berfungsi dengan benar dan cepat.

sigpwned
sumber
2

Jika Anda menggunakan instance json.JSONDecoder, Anda dapat menggunakan raw_decodefungsi anggota. Ini mengembalikan tupel representasi python dari nilai JSON dan indeks ke tempat penguraian berhenti. Ini membuatnya mudah untuk memotong (atau mencari di objek aliran) nilai JSON yang tersisa. Saya tidak begitu senang dengan loop sementara ekstra untuk melewati ruang putih antara nilai JSON yang berbeda dalam masukan tetapi itu menyelesaikan pekerjaan menurut pendapat saya.

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

Versi selanjutnya jauh lebih pendek dan memakan bagian dari string yang sudah diurai. Tampaknya untuk beberapa alasan panggilan kedua json.JSONDecoder.raw_decode () tampaknya gagal ketika karakter pertama dalam string adalah spasi, itu juga alasan mengapa saya melewatkan spasi di whileloop di atas ...

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

Dalam dokumentasi tentang kelas json.JSONDecoder, metode raw_decode https://docs.python.org/3/library/json.html#encoders-and-decoders berisi yang berikut ini:

Ini dapat digunakan untuk memecahkan kode dokumen JSON dari string yang mungkin memiliki data asing di bagian akhir.

Dan data asing ini dapat dengan mudah menjadi nilai JSON lainnya. Dengan kata lain, metode tersebut mungkin ditulis dengan tujuan ini dalam pikiran.

Dengan input.txt menggunakan fungsi atas saya mendapatkan contoh keluaran seperti yang disajikan dalam pertanyaan asli.

hetepeperfan
sumber
0

Anda dapat menggunakan https://pypi.org/project/json-stream-parser/ untuk tujuan itu.

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

keluaran

{'foo': ['bar', 'baz']}
1
2
[]
4
5
6
pengguna5203
sumber