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