2025, Oct 30 20:02

मेमोरी में लोड किए बिना zstd-कंप्रेस्ड JSON को ijson से स्ट्रीम करें

बड़ी .json.zstd फ़ाइल को बिना पूरा लोड किए स्ट्रीम करें: zstandard डिकम्प्रेशन स्ट्रीम को ijson में पास करें, Python उदाहरण और क्यों line-by-line विफल होता है.

जब JSON पेलोड इतना बड़ा हो कि वह मेमोरी में फिट न हो, तो सबसे व्यावहारिक तरीका उसे स्ट्रीम करना है। मुश्किल तब बढ़ती है जब JSON एक .json.zstd फ़ाइल में हो और उसकी संरचना newline-delimited न हो। यदि पूरा JSON एक ही पंक्ति में है, तो लाइन-दर-लाइन इटरेशन बेअसर रहेगा, और सब कुछ एक साथ लोड करना उद्देश्य को ही विफल कर देता है। अच्छी बात यह है कि आप zstd डिकम्प्रेशन स्ट्रीम को सीधे किसी स्ट्रीमिंग JSON पार्सर में दे सकते हैं और पूरा दस्तावेज़ कभी भी मेमोरी में बनाए बिना उस पर इटरेट कर सकते हैं।

समस्या का सेटअप

मान लें कि आप स्ट्रिंग्स या ऐसे डिक्शनरी (जिनमें text फ़ील्ड हो) के टुकड़ों से क्रमवार एक बड़ा संपीड़ित फ़ाइल बनाते हैं। न्यूनतम उदाहरण कुछ ऐसा दिखेगा:

[{"text": "very very"}, " very very", " very very very", {"text": " very very long"}]

इन टुकड़ों को zstd से संपीड़ित एकल JSON ऑब्जेक्ट में लिखना कुछ ऐसा दिखेगा:

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 तत्व निकालने के लिए मज़बूत टोकनाइज़ेशन लागू करनी पड़ेगी।

ijson के साथ संपीड़ित JSON को स्ट्रीम करना

ijson फ़ाइल-जैसे ऑब्जेक्ट्स को पढ़ सकता है, जिनमें urlopen() और io.BytesIO() से बने ऑब्जेक्ट भी शामिल हैं। zstd स्ट्रीम रीडर भी फ़ाइल-जैसा ऑब्जेक्ट है, इसलिए आप उसे सीधे ijson को दे सकते हैं और क्रमशः इटरेट कर सकते हैं। खाली पाथ देने पर स्ट्रीम से शीर्ष-स्तरीय JSON एंटिटी चुनी जाती है।

import zstandard as zstd
import ijson
# zstd-संपीड़ित JSON स्ट्रीम से आइटम पर इटरेट करें
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 पार्सर में भेजने से पूरा पेलोड बफ़र करने की ज़रूरत नहीं पड़ती, और यह काम स्रोत को newline-delimited JSON में बदले बिना हो जाता है। इससे I/O मॉडल सरल रहता है और अनजाने में होने वाले अत्यधिक मेमोरी स्पाइक्स से बचाव होता है।

मुख्य बातें

यदि आपके पास बड़ी .json.zstd फ़ाइल है और JSON newline-delimited नहीं है, तो लाइनों पर इटरेट न करें और पूरी फ़ाइल लोड करने की कोशिश भी न करें। Zstandard स्ट्रीम खोलें और उसे खाली पाथ के साथ ijson.items को दें। इससे आपको संपीड़ित स्ट्रीम से सीधे JSON आइटम मिलेंगे और मेमोरी उपयोग स्थिर रहेगा। अगर pandas के साथ चंकिंग चाहिए, तो सुनिश्चित करें कि आपका डेटा मल्टीलाइन JSON हो; वरना ऐसे स्ट्रीमिंग पार्सर पर टिके रहें जो फ़ाइल-जैसे ऑब्जेक्ट्स के साथ काम करता है।

यह लेख StackOverflow के प्रश्न (लेखक: jeandut) और furas के उत्तर पर आधारित है।