Як я можу лінь читати кілька значень JSON з файлу / потоку в Python?


101

Я хотів би прочитати кілька об’єктів 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))

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

Чи можете ви додати приклад поведінки, яку ви хотіли б від вкладених об'єктів?
Тім Макнамара

@TimMcNamara: Поведінка вкладеного об'єкта не повинна змінюватися. Однак, як тільки ми дійшли до кінця першого об'єкта верхнього рівня ( {"foo": ["bar", "baz"]}на моєму прикладі), він повинен yieldце робити, а потім перейти до наступного ( 1).
Джеремі

1
чому уникати "рядків json"? Завжди можна серіалізувати об’єкт у json таким чином, щоб '\n'у його json-представленні не було жодної (однієї нової рядки, ані двох символів), оскільки він '\n'повинен бути утворений всередині рядка json і тому '\n'може використовуватися лише для форматування, наприклад, я вважаю, що це json.dumps()не ' t ввести '\n'за замовчуванням. Слідкуйте за тим, щоб нові рядки Unicode, такі як U + 0085, могли бути відміненими у рядках json.
jfs

2
Бібліотека ijson може бути корисною в цьому випадку. pypi.python.org/pypi/ijson github.com/isagalaev/ijson
Борис Червенков

1
Чи не повинно бути заголовком "Як я можу лінь читати кілька значень JSON з файлу / потоку в Python?" Оскільки об'єкт є також величиною, як і json int, рядок тощо, тоді як зворотний не потрібен?
hetepeperfan

Відповіді:


20

Ось набагато простіше рішення. Секрет полягає у спробі, невдачі та використанні інформації за винятком правильного розбору. Єдине обмеження полягає в тому, що файл повинен бути шуканим.

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

Ласкаво просимо в стек переповнення і дякуємо за відповідь! Це набагато ближче до того, що я сподівався знайти. Я повинен бути в змозі адаптувати це до типів справ, про які я думав, навіть якщо вони безпосередньо не пропонують шукати.
Джеремі

Це reне спрацює - відхилення від косого кута потребують втечі. Розглянемо сирий рядок r'...'.
Tom Swirly

2
Мені це було потрібно для моєї власної роботи, тому я створив невелику бібліотеку пітонів, щоб зробити це, використовуючи більш-менш вашу техніку з деякими деталями, і це тут: pypi.python.org/pypi/Streamy
Tom Swirly

2
Якщо ви використовуєте ujsonзамість jsonвас, ви отримаєте величезну швидкість
OddNorg

40

JSON, як правило, не дуже підходить для подібного покрокового використання; не існує стандартного способу серіалізації декількох об'єктів, щоб їх можна було легко завантажувати один за одним, без аналізу всього лоту.

Рішення об'єкта на рядок, яке ви використовуєте, також бачиться в іншому місці. Scrap називає це «лінії JSON»:

Ви можете це зробити трохи пітонічніше:

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

Я думаю, що це найкращий спосіб - він не покладається на жодні сторонні бібліотеки, і легко зрозуміти, що відбувається. Я також використовував його в якомусь власному коді.


4
re: "немає стандартного способу": я не бачу проблеми, синтаксис, як видається, робить декілька послідовних об'єктів однозначними, якщо у вас буфер з одним символом. Дякую за те, що вказали, що інші користуються "лініями JSON", я зараз погано користуюся ним.
Джеремі

31

Можливо, трохи пізно, але у мене була така точна проблема (ну, більш-менш). Моє стандартне рішення цих проблем, як правило, просто зробити розбиття регулярного виразка на якомусь відомій кореневому об'єкті, але в моєму випадку це було неможливо. Єдиний можливий спосіб зробити це загальним чином - це застосувати належний токенізатор .

Не знайшовши достатньо загального та досить добре виконаного рішення, я закінчив це робити сам, написавши splitstreamмодуль. Це попередній токенізатор, який розуміє JSON та XML і розбиває безперервний потік на кілька фрагментів для розбору (він фактично розбирає вас, однак). Щоб отримати якусь продуктивність від цього, він записується як модуль C.

Приклад:

from splitstream import splitfile

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

Це круто. Дякуємо, що поділилися цим.
Джеремі

Це остаточне рішення. Я сподіваюся, ви продовжуєте її оновлювати.
Bartvds

Це просто працює. Дякуємо, що надали такий корисний модуль.
Вінод Шарма

1
Чи можете ви завантажити компільовану версію .py? Я намагався створити та встановити модуль, але ... він створює купу помилок щодо переосмислення констант тощо.
SirJames

Модуль написаний на C. Перенесення його на чистий Python залишається як вправа тому, хто вирішує завдання :). Це, мабуть, буде занадто повільним для тієї мети, для якої було написано. Якщо у вас є проблеми зі складанням, вам, ймовірно, потрібно встановити пакет python-dev.
Крумельюр

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
Здається, що складною частиною було б забезпечення того, що потокові читання приносять достатньо файлу, який у вас є весь об'єкт для декодування. Отже, це простий підхід, який працює, якщо ви, наприклад, припускаєте, що об'єкти ніколи не містять нових рядків. Але якщо ви не накладете на файл такий тип додаткової структури, якого ОП намагається уникнути, схоже, вам знадобиться таке рішення від @Benedict
nealmcb

24

Це досить неприємна проблема насправді тому, що вам потрібно передавати лінії, але візерунок узгоджується через декілька ліній проти брекетів, а також json відповідності шаблону. Це свого роду json-preparse з подальшим розбором json. Json, порівняно з іншими форматами, легко проаналізувати, тому не завжди потрібно шукати бібліотеку для розбору, але, як нам вирішити ці конфліктні питання?

Генератори на допомогу!

Краса генераторів для подібної проблеми - ви можете складати їх один на одного, поступово зменшуючи труднощі проблеми, зберігаючи лінь. Я також розглядав можливість використання механізму передачі зворотних значень в генератор (send ()), але, на щастя, виявив, що мені не потрібно це використовувати.

Для вирішення першої з проблем вам потрібен якийсь потоковий перелік, як потокова версія 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' }")

В основному це жахливо зроблено. Нам зараз залишається зробити остаточний рівень розбору, як вважаємо за потрібне. Наприклад, ми можемо використовувати функцію завантаження Джеремі Романа (Спасибі!) Для розбору одного рядка:

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.
Петро Вікторін

Спасибі Джеремі. Це був приємний виклик питання! Так, Петро - ти, безумовно, маєш рацію :)
Бенедикт

1
Чудово зроблено. Чи буде це правильно, якщо символи подобаються "}"і "]"зустрічаються всередині рядків JSON? Я думаю, що це загальне обмеження розбору з регулярним виразом.
Thomas K

2
Обмірковуючи, я виявив, що основна функція розбору побудована таким чином, що неможливо правильно використовувати її ліниво, тому ви не збираєтеся домогтися ідеального результату без самостійного впровадження повного аналізу. Ця відповідь демонструє кілька корисних релевантних речей і прекрасно обробляє прості випадки.
Джеремі

3
Ця відповідь жахлива, і я не маю поняття, чому її підтримують. Автор визнає, що він насправді не працює для всіх вхідних даних, тому, за визначенням, це навіть не правильна відповідь, і він використовує складний регулярний вираз, який обчислюється , тому ми навіть не можемо прочитати, що це таке. Яка користь - це функція, яка іноді дає правильний результат?
Том Свірлі

10

Я вважаю, що кращим способом зробити це буде використання державної машини. Нижче наведено зразок коду, який я опрацював, перетворивши код NodeJS на посилання нижче на Python 3 (використовується нелокальне ключове слово, доступне лише в Python 3, код не працюватиме на Python 2)

Edit-1: оновлений і зроблений код сумісний з Python 2

Edit-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-only. Використання диктовок для всіх ваших локальних змінних начебто незручне, і я для одного радий залишити Python 2 у минулому. ;)
Джеремі

@ JeremyBanks, впевнений, що я не знав, на яку версію ви націлили. Тепер я додав версію лише для Python3 та сумісну з Py2 відповідь для когось іншого, хто може все ще бути на Python 2
Tarun Lalwani

@JeremyBanks, з баунті залишився лише 1 день, сподіваємось, що ви можете переглянути та надіслати відгук про відповідь
Tarun Lalwani

Здається, єдиний, хто реально зрозумів проблему, був Тарун. Ефективність аналізу залежить від кількості пропусків, які трапляються на вході. Більшість відповідей використовують регулярний вираз або читають рядок заздалегідь (це також може бути небезпечно) або, що ще гірше, не вдасться проаналізувати невідому кількість разів. Шкода, що це не частина Python.
mschonaker

4

Я хотів би надати рішення. Ключова думка - "спробувати" розшифрувати: якщо вона не вдасться, дайте їй більше каналу, інакше використовуйте інформацію про зміщення для підготовки наступного розшифровки.

Однак поточний модуль json не може терпіти розшифровку пробілу в заголовку рядка, тому мені доведеться зняти їх.

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?
пісня0089

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функцію-член. Він повертає кортеж пітонного подання значення 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 (), здається, не вдається, коли перший символ у рядку є пробілом, це також причина, чому я пропускаю пробіл у проміжку зверху ...

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 за допомогою верхньої функції я отримую приклад виводу, як це представлено в оригінальному запитанні.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.