2025, Oct 30 19:48
Как потоково парсить JSON из Zstandard (.json.zstd) в Python с ijson
Как разбирать большой JSON из потока Zstandard .json.zstd в Python: ijson для потокового парсинга и почему построчный подход не работает, что с pandas в деталях
Когда JSON-пакет слишком велик, чтобы поместиться в памяти, разумное решение — обрабатывать его потоково. Сложнее становится, если JSON лежит внутри файла .json.zstd и структура не разделена переводами строк. Перебирать по строкам бесполезно, когда весь JSON находится в одной строке, а загружать всё целиком — наоборот, противоречит задаче. Хорошая новость: поток распаковки zstd можно напрямую передать потоковому парсеру JSON и итерироваться по данным, не собирая документ целиком.
Постановка задачи
Предположим, вы по частям формируете крупный сжатый файл из фрагментов, которые могут быть либо строками, либо словарями с полем text. Минимальный пример данных может выглядеть так:
[{"text": "very very"}, " very very", " very very very", {"text": " very very long"}]Запись этих фрагментов в один JSON-объект с последующим сжатием zstd может выглядеть так:
import zstandard as zstd
# создает файл .json.zstd, содержащий {"text": "..."}
def dump_stream(zpath, pieces, lvl=22):
    comp = zstd.ZstdCompressor(level=lvl)
    with open(zpath, 'wb') as fp_out:
        with comp.stream_writer(fp_out) as zw:
            for pos, part in enumerate(pieces):
                if isinstance(part, str):
                    pass
                elif isinstance(part, dict):
                    part = part["text"]
                else:
                    raise ValueError(f"Unrecognized chunk {type(part)}")
                if pos == 0:
                    part = '{"text": "' + part
                zw.write(part.encode("utf-8"))
            zw.write('"}'.encode("utf-8"))
# демонстрационные данные
dump_stream(
    "test.json.zstd",
    [{"text": "very very"}, " very very", " very very very", {"text": " very very long"}],
    lvl=22,
)Простой подход — читать поток zstd построчно — не сработает, если весь JSON записан в одну строку:
import io, json
import zstandard as zstd
def scan_zstd_lines(infile):
    dec = zstd.ZstdDecompressor()
    with open(infile, 'rb') as raw:
        with dec.stream_reader(raw) as zr:
            txt = io.TextIOWrapper(zr, encoding='utf-8')
            for ln in txt:
                if ln.strip():
                    yield json.loads(ln)
# next(scan_zstd_lines("test.json.zstd"))  # не поможет в случае однострочного JSONПочему построчный подход здесь не работает
Построчная итерация предполагает, что в тексте есть границы. Однако сериализованный JSON-объект нередко занимает одну строку. Деление по переводу строки даст либо пустой результат, либо весь документ целиком — именно этого мы и избегаем. Чтение по символам через read(n) позволит идти потоком, но тогда придётся самим реализовать надёжную токенизацию, чтобы извлекать валидные элементы JSON и не спотыкаться о запятые внутри вложенных структур.
Потоковая обработка сжатого JSON с ijson
ijson умеет читать объекты, ведущие себя как файлы, в том числе то, что возвращают urlopen() и io.BytesIO(). Читатель потока zstd — тоже файловоподобный объект, значит его можно напрямую передать в ijson и итерироваться по мере поступления данных. Пустой путь указывает парсеру брать верхнеуровневую сущность JSON из потока.
import zstandard as zstd
import ijson
# итерируемся по элементам JSON прямо из zstd‑потока
def stream_zstd_json(zpath):
    dec = zstd.ZstdDecompressor()
    with open(zpath, 'rb') as fh:
        with dec.stream_reader(fh) as zstream:
            parser = ijson.items(zstream, '')  # пустой путь — читать верхнеуровневый элемент
            for obj in parser:
                yield obj
# использование
fname = 'test.json.zstd'
for obj in stream_zstd_json(fname):
    print(obj)
    print('---')Такой код перебирает элементы JSON, которые парсер выдаёт напрямую из распакованного потока. Если на верхнем уровне один объект вроде {"text": "..."}, в итераторе вы получите именно его.
Пара слов о pandas
pandas умеет читать из zstd «из коробки», если вас устраивает загрузка всего файла целиком:
import pandas as pd
df = pd.read_json('test.json.zstd')Пакетное чтение с параметрами lines=True и chunksize=... рассчитывает на формат newline-delimited JSON, где каждый объект находится на отдельной строке без внешних скобок и запятых. Такой формат несовместим с однострочным JSON из одного объекта.
Почему это важно
Когда объём данных превышает доступную память, не менее важна стратегия парсинга, чем формат сжатия. Передавая распакованные байты в потоковый парсер JSON, вы избегаете буферизации всего содержимого и при этом не вынуждены перекладывать данные в формат JSON с разделителями строк. Такой подход упрощает модель ввода‑вывода и предотвращает внезапные квадратичные всплески потребления памяти.
Выводы
Если у вас большой .json.zstd и JSON не разделён переводами строк, не проходите по строкам и не загружайте файл целиком. Откройте поток Zstandard и передайте его в ijson.items с пустым путём. Так вы получите элементы JSON прямо из сжатого потока, удерживая потребление памяти на постоянном уровне. Если требуется чанкинг в pandas, убедитесь, что данные — это многострочный JSON; в противном случае используйте потоковый парсер, который работает с файловоподобными объектами.