ฉันจะอ่านค่า JSON หลายค่าจากไฟล์ / สตรีมใน Python อย่างเกียจคร้านได้อย่างไร


104

ฉันต้องการอ่านออบเจ็กต์ JSON หลายรายการจากไฟล์ / สตรีมใน Python ทีละรายการ น่าเสียดายที่json.load()เพิ่ง.read()ถึงจุดสิ้นสุดของไฟล์ ดูเหมือนจะไม่มีวิธีใดเลยที่จะใช้เพื่ออ่านวัตถุชิ้นเดียวหรืออ่านซ้ำบนวัตถุอย่างเกียจคร้าน

มีวิธีใดบ้างที่จะทำเช่นนี้? การใช้ไลบรารีมาตรฐานจะดีที่สุด แต่ถ้ามีไลบรารีของบุคคลที่สามฉันจะใช้สิ่งนั้นแทน

ในขณะนี้ฉันวางวัตถุแต่ละชิ้นไว้ในบรรทัดแยกกันและใช้json.loads(f.readline())งาน แต่ฉันไม่ต้องการทำสิ่งนี้จริงๆ

ตัวอย่างการใช้งาน

example.py

import my_json as json
import sys

for o in json.iterload(sys.stdin):
    print("Working on a", type(o))

ใน. txt

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

ตัวอย่างเซสชัน

$ 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

คุณช่วยเพิ่มตัวอย่างพฤติกรรมที่คุณต้องการจากวัตถุที่ซ้อนกันได้ไหม
Tim McNamara

@TimMcNamara: พฤติกรรมของวัตถุที่ซ้อนกันไม่ควรเปลี่ยนแปลง อย่างไรก็ตามเมื่อเรามาถึงจุดสิ้นสุดของออบเจ็กต์ระดับบนสุดตัวแรก ( {"foo": ["bar", "baz"]}ในตัวอย่างของฉัน) มันควรจะyieldเป็นแล้วไปยังวัตถุถัดไป ( 1)
Jeremy

1
ทำไมต้องหลีกเลี่ยง "เส้น json"? เป็นไปได้เสมอที่จะทำให้วัตถุเป็นอนุกรมลงใน json เพื่อให้ไม่มี'\n'(ขึ้นบรรทัดใหม่เดียวไม่ใช่อักขระสองตัว) ในการแทนค่า json เนื่องจาก'\n'ต้องหลีกเลี่ยงภายในสตริง json ดังนั้นจึง'\n'อาจใช้สำหรับการจัดรูปแบบเท่านั้นเช่นฉันเชื่อว่าjson.dumps()doesn ' t แนะนำ'\n'โดยค่าเริ่มต้น ระวังว่า Unicode newlines เช่น U + 0085 อาจไม่ใช้ Escape ภายในสตริง json
jfs

2
ijsonห้องสมุดอาจจะมีประโยชน์ในกรณีนี้ pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Boris Chervenkov

1
ชื่อเรื่องไม่ควรเป็น "ฉันจะอ่านค่า JSON หลายค่าจากไฟล์ / สตรีมใน Python ได้อย่างไร" เนื่องจากออบเจ็กต์เป็นค่าเช่นเดียวกับ json int สตริง ฯลฯ ในขณะที่สิ่งที่กลับกันไม่จำเป็นจริงหรือ?
heteperfan

คำตอบ:


21

นี่เป็นวิธีแก้ปัญหาที่ง่ายกว่ามาก เคล็ดลับคือการพยายามล้มเหลวและใช้ข้อมูลในข้อยกเว้นเพื่อแยกวิเคราะห์อย่างถูกต้อง ข้อ จำกัด เพียงอย่างเดียวคือต้องค้นหาไฟล์ได้

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

แก้ไข: เพิ่งสังเกตว่าสิ่งนี้ใช้ได้กับ Python> = 3.5 เท่านั้น สำหรับก่อนหน้านี้ความล้มเหลวจะคืนค่า ValueError และคุณต้องแยกวิเคราะห์ตำแหน่งจากสตริงเช่น

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

ยินดีต้อนรับสู่ Stack Overflow และขอบคุณสำหรับคำตอบ! ใกล้เคียงกับสิ่งที่ฉันหวังไว้มาก ฉันควรจะปรับให้เข้ากับประเภทของกรณีที่ฉันกำลังคิดอยู่แม้ว่าจะไม่ได้ให้การค้นหาโดยตรงก็ตาม
Jeremy

มันใช้reไม่ได้ - แบ็กสแลชจำเป็นต้องหนี r'...'พิจารณาสตริงดิบ
Tom Swirly

2
ฉันต้องการสิ่งนี้สำหรับงานของฉันเองดังนั้น II จึงสร้างไลบรารี python ขึ้นมาเพื่อทำสิ่งนี้โดยใช้เทคนิคของคุณไม่มากก็น้อยพร้อมรายละเอียดที่นี่: pypi.python.org/pypi/Streamy
Tom Swirly

2
หากคุณใช้ujsonแทนคุณjsonจะได้รับการเร่งความเร็วอย่างมาก
OddNorg

40

โดยทั่วไป JSON ไม่ค่อยดีนักสำหรับการใช้งานที่เพิ่มขึ้นประเภทนี้ ไม่มีวิธีมาตรฐานในการต่ออนุกรมวัตถุหลายชิ้นเพื่อให้โหลดทีละชิ้นได้อย่างง่ายดายโดยไม่ต้องแยกวิเคราะห์ล็อตทั้งหมด

โซลูชันออบเจ็กต์ต่อบรรทัดที่คุณใช้อยู่จะเห็นที่อื่น Scrapy เรียกมันว่า 'เส้น JSON':

คุณสามารถทำได้มากกว่า Pythonically เล็กน้อย:

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

ฉันคิดว่านี่เป็นวิธีที่ดีที่สุด - ไม่ต้องพึ่งพาไลบรารีของบุคคลที่สามใด ๆ และง่ายต่อการเข้าใจว่าเกิดอะไรขึ้น ฉันเคยใช้มันในโค้ดของฉันเองเช่นกัน


4
re: "no standard way": ฉันไม่เห็นปัญหานี้ดูเหมือนว่าไวยากรณ์จะทำให้หลาย ๆ ออบเจ็กต์ต่อเนื่องกันไม่คลุมเครือตราบเท่าที่คุณมีบัฟเฟอร์อักขระเดียว ขอบคุณที่ชี้ให้เห็นว่าคนอื่น ๆ ใช้ "เส้น JSON" ตอนนี้ฉันรู้สึกแย่น้อยลงที่จะใช้มัน
Jeremy

31

อาจจะช้าไปหน่อย แต่ฉันมีปัญหาตรงนี้ (ดีไม่มากก็น้อย) วิธีแก้ปัญหามาตรฐานของฉันสำหรับปัญหาเหล่านี้มักจะแค่ทำการแยก regex บนวัตถุรูทที่รู้จักกันดี แต่ในกรณีของฉันมันเป็นไปไม่ได้ วิธีเดียวที่เป็นไปได้ที่จะทำนี้คือโดยทั่วไปจะใช้ tokenizer

หลังจากไม่พบวิธีแก้ปัญหาทั่วไปที่เพียงพอและมีประสิทธิภาพพอสมควรฉันก็ทำสิ่งนี้ด้วยตัวเองโดยเขียนไฟล์ splitstreamโมดูล เป็นตัวสร้างโทเค็นล่วงหน้าที่เข้าใจ JSON และ XML และแยกสตรีมที่ต่อเนื่องออกเป็นหลาย ๆ ส่วนสำหรับการแยกวิเคราะห์ เพื่อให้ได้ประสิทธิภาพบางอย่างมันถูกเขียนเป็นโมดูล C

ตัวอย่าง:

from splitstream import splitfile

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

ที่น่ากลัว. ขอบคุณสำหรับการแบ่งปัน
Jeremy

นี่คือทางออกที่ชัดเจน ฉันหวังว่าคุณจะอัปเดตต่อไป
Bartvds

มันใช้งานได้จริง ขอขอบคุณที่ให้โมดูลที่มีประโยชน์เช่นนี้
Vinod Sharma

1
คุณสามารถอัปโหลดเวอร์ชัน. py ที่คอมไพล์ได้หรือไม่? ฉันพยายามสร้างและติดตั้งโมดูล แต่ ... มันเกิดข้อผิดพลาดมากมายเกี่ยวกับการกำหนดค่าคงที่ใหม่และอื่น ๆ
SirJames

โมดูลนี้เขียนด้วย C การพอร์ตไปยัง Python บริสุทธิ์จะถูกปล่อยให้เป็นแบบฝึกหัดสำหรับใครก็ตามที่พร้อมสำหรับงาน :) อาจจะช้าเกินไปสำหรับจุดประสงค์ที่เขียนไว้ หากคุณมีปัญหาในการรวบรวมคุณอาจต้องติดตั้ง python-dev packge
Krumelur

25

แน่ใจว่าทำได้ คุณเพียงแค่ต้องดำเนินการraw_decodeโดยตรง การใช้งานนี้จะโหลดไฟล์ทั้งหมดลงในหน่วยความจำและดำเนินการกับสตริงนั้น (เท่าที่มีjson.load) หากคุณมีไฟล์ขนาดใหญ่คุณสามารถแก้ไขให้อ่านจากไฟล์เท่าที่จำเป็นโดยไม่ยุ่งยาก

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

การใช้งาน: ตามที่คุณร้องขอมันเป็นเครื่องกำเนิดไฟฟ้า


2
ดูเหมือนว่าส่วนที่ยุ่งยากจะทำให้มั่นใจได้ว่าการอ่านสตรีมมิงจะนำไฟล์มาเพียงพอที่คุณมีวัตถุทั้งหมดในการถอดรหัส นี่เป็นวิธีง่ายๆที่ใช้ได้ผลถ้าคุณสมมติว่าวัตถุไม่เคยมีบรรทัดใหม่อยู่ในนั้น แต่ถ้าคุณไม่กำหนดโครงสร้างเพิ่มเติมในไฟล์ซึ่ง OP พยายามหลีกเลี่ยงดูเหมือนว่าคุณต้องการวิธีแก้ปัญหาเช่นนั้นจาก @Benedict
nealmcb

24

นี่เป็นปัญหาที่ค่อนข้างน่ารังเกียจจริงๆเพราะคุณต้องสตรีมเป็นเส้น แต่รูปแบบจะจับคู่หลายบรรทัดกับวงเล็บปีกกา แต่ยังจับคู่รูปแบบ json ด้วย มันเป็นประเภทของ json-preparse ตามด้วย json parse Json เปรียบเทียบกับรูปแบบอื่น ๆ ง่ายต่อการแยกวิเคราะห์ดังนั้นจึงไม่จำเป็นต้องไปที่ไลบรารีการแยกวิเคราะห์เสมอไปอย่างไรก็ตามเราจะแก้ปัญหาที่ขัดแย้งกันเหล่านี้ได้อย่างไร?

เครื่องปั่นไฟเพื่อช่วยเหลือ!

ความสวยงามของเครื่องกำเนิดไฟฟ้าสำหรับปัญหาเช่นนี้คือคุณสามารถวางซ้อนทับกันโดยค่อยๆขจัดความยากลำบากของปัญหาออกไปในขณะที่ยังคงความเกียจคร้านไว้ ฉันยังพิจารณาใช้กลไกในการส่งค่าย้อนกลับไปยังเครื่องกำเนิดไฟฟ้า (ส่ง ()) แต่โชคดีที่พบว่าฉันไม่จำเป็นต้องใช้สิ่งนั้น

ในการแก้ปัญหาแรกคุณต้องมีตัวค้นหาสตรีมมิ่งเป็น re.finditer เวอร์ชันสตรีมมิ่ง ความพยายามของฉันในสิ่งนี้ด้านล่างดึงเป็นบรรทัดตามต้องการ (ยกเลิกการใส่ข้อคิดเห็นคำสั่งการดีบักเพื่อดู) ในขณะที่ยังคงกลับมาตรงกัน จากนั้นฉันก็แก้ไขเล็กน้อยเพื่อให้ได้เส้นที่ไม่ตรงกันและตรงกัน (ทำเครื่องหมายเป็น 0 หรือ 1 ในส่วนแรกของทูเปิลที่ให้ผล)

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]

ด้วยเหตุนี้จึงเป็นไปได้ที่จะจับคู่จนกว่าวงเล็บปีกกาจะพิจารณาทุกครั้งว่าวงเล็บปีกกามีความสมดุลหรือไม่จากนั้นจึงส่งคืนวัตถุอย่างง่ายหรือแบบผสมตามความเหมาะสม

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

สิ่งนี้ส่งคืนสิ่งที่เป็นสิ่งต่อไปนี้:

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

โดยพื้นฐานแล้วนั่นเป็นส่วนที่น่ารังเกียจที่ทำ ตอนนี้เราต้องทำการแยกวิเคราะห์ในระดับสุดท้ายตามที่เห็นสมควร ตัวอย่างเช่นเราสามารถใช้ฟังก์ชัน iterload ของ Jeremy Roman (ขอบคุณ!) เพื่อแยกวิเคราะห์สำหรับบรรทัดเดียว:

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

ทดสอบ:

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

ฉันได้รับผลลัพธ์เหล่านี้ (และถ้าคุณเปิดบรรทัดดีบักนั้นคุณจะเห็นว่ามันดึงเข้ามาในบรรทัดตามต้องการ):

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

วิธีนี้ใช้ไม่ได้กับทุกสถานการณ์ เนื่องจากการใช้งานjsonไลบรารีจึงเป็นไปไม่ได้ที่จะทำงานได้อย่างถูกต้องทั้งหมดโดยไม่ต้องติดตั้งโปรแกรมแยกวิเคราะห์ใหม่ด้วยตัวคุณเอง


8
หากคุณต้องการทำอย่างถูกต้องคุณต้องระวังวงเล็บปีกกาและวงเล็บภายในสตริงด้วย และระวังเครื่องหมายคำพูดที่ไม่ใช้ ก่อนที่คุณจะรู้ "ตัวเตรียมล่วงหน้า" จะซับซ้อนพอ ๆ กับตัวแยกวิเคราะห์ JSON แบบเต็ม
Petr Viktorin

ขอบคุณ Jeremy เป็นความท้าทายที่ดีสำหรับคำถาม! ใช่ Petr - คุณพูดถูกแน่นอน :)
เบเนดิกต์

1
ทำได้ดีมาก สิ่งนี้จะทำงานได้อย่างถูกต้องหรือไม่หากอักขระเช่น"}"และ"]"เกิดขึ้นภายในสตริง JSON ฉันคิดว่านี่เป็นข้อ จำกัด ทั่วไปของการแยกวิเคราะห์ด้วยนิพจน์ทั่วไป
Thomas K

2
เมื่อมองไปรอบ ๆ ฉันพบว่าฟังก์ชันการแยกวิเคราะห์หลักถูกสร้างขึ้นในลักษณะที่ไม่สามารถใช้งานได้อย่างถูกต้องอย่างเฉื่อยชาดังนั้นคุณจะไม่ได้ผลลัพธ์ที่สมบูรณ์แบบโดยไม่ต้องใช้โปรแกรมแยกวิเคราะห์ที่สมบูรณ์ด้วยตัวเอง คำตอบนี้แสดงให้เห็นถึงสิ่งที่เกี่ยวข้องที่เป็นประโยชน์หลายประการและจัดการกับกรณีง่ายๆได้เป็นอย่างดี
Jeremy

3
คำตอบนี้น่ากลัวและฉันไม่รู้ว่าทำไมมันถึงถูกโหวต ผู้เขียนยอมรับว่ามันไม่ได้ใช้งานได้จริงสำหรับอินพุตทั้งหมดดังนั้นตามคำจำกัดความมันจึงไม่ใช่คำตอบที่ถูกต้องและมันใช้นิพจน์ทั่วไปที่ซับซ้อนซึ่งคำนวณแล้วเราจึงไม่สามารถอ่านได้ว่ามันคืออะไร ฟังก์ชั่นที่ดีบางครั้งให้ผลลัพธ์ที่ถูกต้องคืออะไร?
Tom Swirly

10

ฉันเชื่อว่าวิธีที่ดีกว่าคือการใช้เครื่องของรัฐ ด้านล่างนี้เป็นโค้ดตัวอย่างที่ฉันได้ทำโดยการแปลงรหัส NodeJS ที่ลิงค์ด้านล่างเป็น Python 3 (ใช้คำหลักที่ไม่ใช่เฉพาะที่มีอยู่ใน Python 3 เท่านั้นรหัสจะไม่ทำงานบน Python 2)

แก้ไข -1: อัปเดตและทำให้โค้ดเข้ากันได้กับ Python 2

แก้ไข -2: อัปเดตและเพิ่มเวอร์ชัน Python3 เท่านั้นเช่นกัน

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

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

เวอร์ชันที่เข้ากันได้กับ 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

กำลังทดสอบ

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

ผลลัพธ์ที่เหมือนกันคือ

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

ทางออกที่ดี! ฉันจะตรวจสอบอย่างละเอียดในภายหลัง แต่มีแนวโน้มดีมาก แต่สำหรับสิ่งที่คุ้มค่าฉันชอบเวอร์ชัน Python 3 เท่านั้น การใช้คำสั่งสำหรับตัวแปรในพื้นที่ทั้งหมดของคุณเป็นเรื่องที่น่าอึดอัดใจและฉันยินดีที่จะทิ้ง Python 2 ไว้ในอดีต ;)
Jeremy

@JeremyBanks แน่ใจว่าฉันไม่ทราบว่าคุณกำหนดเป้าหมายเวอร์ชันใด ตอนนี้ฉันได้เพิ่มเวอร์ชัน Python3 เท่านั้นและ Py2 เข้ากันได้กับคำตอบสำหรับคนอื่นที่อาจยังอยู่ใน Python 2
Tarun Lalwani

@JeremyBanks เหลือเวลาอีกเพียง 1 วันหวังว่าคุณจะสามารถตรวจสอบและให้ข้อเสนอแนะเกี่ยวกับคำตอบได้
Tarun Lalwani

ดูเหมือนคนเดียวที่เข้าใจปัญหาจริงๆคือ Tarun ประสิทธิภาพของการแยกวิเคราะห์ขึ้นอยู่กับจำนวนรอบที่เกิดขึ้นกับอินพุต คำตอบส่วนใหญ่ใช้ regex หรืออ่านบรรทัดก่อนล่วงหน้า (ซึ่งอาจเป็นอันตรายได้เช่นกัน) หรือแย่กว่านั้นคือล้มเหลวในการแยกวิเคราะห์จำนวนครั้งที่ไม่รู้จัก น่าเสียดายที่นี่ไม่ใช่ส่วนหนึ่งของ Python
mschonaker

4

ฉันต้องการให้วิธีแก้ปัญหา แนวคิดหลักคือการ "พยายาม" ถอดรหัส: หากล้มเหลวให้ป้อนข้อมูลเพิ่มเติมหรือใช้ข้อมูลออฟเซ็ตเพื่อเตรียมการถอดรหัสครั้งต่อไป

อย่างไรก็ตามโมดูล json ปัจจุบันไม่สามารถทนต่อ SPACE ในส่วนหัวของสตริงที่จะถอดรหัสได้ดังนั้นฉันจึงต้องถอดมันออก

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)

========================= ฉันได้ทดสอบไฟล์ txt หลายไฟล์และใช้งานได้ดี (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 ชื่อย่อของคุณ)

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

(ผลลัพธ์สำหรับกรณีทดสอบของเบเนดิกต์)

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

นี่ของฉัน:

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

สวัสดีนี่มีประโยชน์มาก แต่ช่วยแสดงให้ดูว่าฉันจะใช้คลาสเพื่อโหลดไฟล์ json ได้อย่างไร
song0089

3

ฉันใช้โซลูชันที่หรูหราของ @ wuilang วิธีง่ายๆ - อ่านไบต์พยายามถอดรหัสอ่านไบต์พยายามถอดรหัส ... - ใช้งานได้ แต่น่าเสียดายที่มันช้ามาก

ในกรณีของฉันฉันพยายามอ่านออบเจ็กต์ JSON "พิมพ์สวย" ของอ็อบเจ็กต์ประเภทเดียวกันจากไฟล์ สิ่งนี้ทำให้ฉันสามารถปรับแนวทางให้เหมาะสมได้ ฉันสามารถอ่านไฟล์ทีละบรรทัดโดยจะถอดรหัสเฉพาะเมื่อฉันพบบรรทัดที่มี "}" เท่านั้น:

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

หากคุณกำลังทำงานกับ JSON ขนาดกะทัดรัดหนึ่งบรรทัดต่อบรรทัดที่หลีกเลี่ยงการขึ้นบรรทัดใหม่ในตัวอักษรสตริงคุณสามารถลดความซับซ้อนของแนวทางนี้ได้อย่างปลอดภัย:

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

เห็นได้ชัดว่าวิธีง่ายๆเหล่านี้ใช้ได้กับ JSON ที่เฉพาะเจาะจงมากเท่านั้น อย่างไรก็ตามหากมีข้อสันนิษฐานเหล่านี้การแก้ปัญหาเหล่านี้จะทำงานได้อย่างถูกต้องและรวดเร็ว


2

หากคุณใช้อินสแตนซ์ json.JSONDecoder คุณสามารถใช้raw_decodeฟังก์ชันสมาชิกได้ จะส่งคืนค่า tuple ของ python แทนค่า JSON และดัชนีที่หยุดการแยกวิเคราะห์ สิ่งนี้ทำให้ง่ายต่อการแบ่ง (หรือค้นหาในวัตถุสตรีม) ค่า JSON ที่เหลือ ฉันไม่ค่อยมีความสุขกับสิ่งพิเศษในขณะที่วนซ้ำเพื่อข้ามช่องว่างระหว่างค่า JSON ที่แตกต่างกันในอินพุต แต่มันทำให้งานเสร็จในความคิดของฉัน

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

เวอร์ชันถัดไปจะสั้นกว่ามากและกินส่วนของสตริงที่แยกวิเคราะห์แล้ว ดูเหมือนว่าด้วยเหตุผลบางอย่างการโทรครั้งที่สอง json.JSONDecoder.raw_decode () ดูเหมือนจะล้มเหลวเมื่ออักขระตัวแรกในสตริงเป็นช่องว่างนั่นเป็นเหตุผลว่าทำไมฉันถึงข้ามช่องว่างใน whileloop ด้านบน ...

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

ในเอกสารประกอบเกี่ยวกับคลาส json.JSONDecoder เมธอด raw_decode https://docs.python.org/3/library/json.html#encoders-and-decodersมีดังต่อไปนี้:

สามารถใช้เพื่อถอดรหัสเอกสาร JSON จากสตริงที่อาจมีข้อมูลที่ไม่เกี่ยวข้องในตอนท้าย

และข้อมูลภายนอกนี้สามารถเป็นค่า JSON อื่นได้อย่างง่ายดาย กล่าวอีกนัยหนึ่งวิธีการนี้อาจเขียนขึ้นโดยคำนึงถึงจุดประสงค์นี้

ด้วย input.txt โดยใช้ฟังก์ชันด้านบนฉันจะได้ผลลัพธ์ตัวอย่างตามที่นำเสนอในคำถามเดิม


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.