Как я могу лениво читать несколько значений 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, string и т. Д., Тогда как обратное не обязательно верно?
hetepeperfan 07

Ответы:

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
Ник Уотсон
источник
Добро пожаловать в Stack Overflow и спасибо за ответ! Это намного ближе к тому, что я надеялся найти. Я должен быть в состоянии адаптировать это к типам случаев, о которых я думал, даже если они напрямую не обеспечивают поиск.
Джереми
Это reне сработает - нужно убрать обратную косую черту. Рассмотрим необработанную строку r'...'.
Tom Swirly
2
Мне это было нужно для моей работы, поэтому я создал небольшую библиотеку python для этого, используя более или менее вашу технику с некоторыми деталями, и она здесь: pypi.python.org/pypi/Streamy
Tom Swirly
2
Если вы используете ujsonвместо этого, jsonвы получите огромное ускорение
OddNorg
40

JSON обычно не очень подходит для такого рода инкрементального использования; не существует стандартного способа сериализации нескольких объектов, чтобы их можно было легко загружать по одному, без синтаксического анализа всей партии.

Решение «объект на строку», которое вы используете, можно увидеть и в другом месте. Scrapy называет это «строками JSON»:

Вы можете сделать это немного более Python:

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.
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, за которой следует синтаксический анализ json. Json, по сравнению с другими форматами, легко анализировать, поэтому не всегда нужно использовать библиотеку синтаксического анализа, тем не менее, как мы должны решить эти конфликтующие проблемы?

Генераторы спешат на помощь!

Прелесть генераторов для подобных задач в том, что вы можете складывать их друг на друга, постепенно абстрагируя сложность задачи, сохраняя при этом лень. Я также рассматривал возможность использования механизма для передачи значений обратно в генератор (send ()), но, к счастью, обнаружил, что мне это не нужно.

Для решения первой проблемы вам понадобится какой-то streamingfinditer, как потоковая версия 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 Джереми Романа (спасибо!) Для синтаксического анализа одной строки:

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
Этот ответ ужасен, и я понятия не имею, почему за него проголосовали. Автор признает, что на самом деле это не работает для всех входных данных, поэтому по определению это даже неправильный ответ, и он использует сложное регулярное выражение, которое вычисляется , поэтому мы даже не можем прочитать, что это такое. Что хорошего в функции, которая иногда дает правильный результат?
Tom Swirly
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. Использование dicts для всех ваших локальных переменных довольно неудобно, и я, например, счастлив оставить Python 2 в прошлом. ;)
Джереми
@JeremyBanks, конечно, я не знал, на какую версию вы нацелились. Теперь я добавил версию только для Python3 и версию, совместимую с Py2, также в ответ для кого-то еще, кто может все еще использовать Python 2
Тарун Лалвани
@JeremyBanks, остался только 1 день с наградой, надеюсь, вы сможете просмотреть и
оставить
Похоже, единственным, кто действительно понимал проблему, был Тарун. Эффективность синтаксического анализа зависит от количества проходов на входе. В большинстве ответов используется регулярное выражение или заранее читается строка (это тоже может быть опасно) или, что еще хуже, не удается выполнить синтаксический анализ неизвестное количество раз. Жаль, что это не часть 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']})
Wuliang
источник
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
пользователь3542882
источник
Привет, это очень полезно, но не могли бы вы показать, как я могу использовать класс для загрузки файла 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. Однако, если эти предположения верны, эти решения работают правильно и быстро.

sigpwned
источник
2

Если вы используете экземпляр json.JSONDecoder, вы можете использовать raw_decodeфункцию-член. Он возвращает кортеж Python-представления значения JSON и индекс остановки синтаксического анализа. Это позволяет легко нарезать (или искать в объекте потока) оставшиеся значения JSON. Я не очень доволен дополнительным циклом while, позволяющим пропускать пробелы между различными значениями 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 (), кажется, не работает, когда первый символ в строке является пробелом, это также причина, по которой я пропускаю пробелы в цикле while выше ...

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

hetepeperfan
источник
0

Вы можете использовать https://pypi.org/project/json-stream-parser/ именно для этой цели.

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

вывод

{'foo': ['bar', 'baz']}
1
2
[]
4
5
6
user5203
источник