Skip to content

So speichern Sie kleine Textdateien effizient ​

Vorbereitung ​

Lassen Sie uns ein Testdataset mit vielen Textdateien erstellen.

python
import os
import json
import random
import string
from datetime import datetime

workspace_dir = '/tmp/data'
output_dir = f'{workspace_dir}/jsons'
os.makedirs(output_dir, exist_ok=True)


def generate_random_string(length):
    return ''.join(random.choices(string.ascii_letters + string.digits + string.punctuation + ' ', k=length))


for i in range(1000000):
    data = {
        'text': generate_random_string(random.randint(0, 1000)),
        'created_at': int(datetime.now().timestamp())
    }
    filename = os.path.join(output_dir, f'{i}.json')
    with open(filename, 'w') as json_file:
        json.dump(data, json_file, separators=(',', ':'))

Zum Beispiel werden wir 1 000 000 JSON-Dateien mit der Struktur erzeugen (zur Effizienz haben sie keine Leerzeichen in den Dateien, erreichbar mit dem separators=(',', ':') Parameter in der json.dump Methode):

json
{
  "text": "random string",
  "created_at": 1727625937
}

Lassen Sie uns die tatsächliche Größe messen:

shell
du -sk /tmp/data/jsons
4000000 jsons

Es sind 4GB, aber die Größe sollte <= 1GB sein (1000000 * 1KB). Dies liegt daran, dass ich ein Dateisystem mit einer 4KB Aufzeichnungsgröße verwende (ich benutze OS X, aber unter Linux wird die Situation ähnlich sein), also habe ich 1000000 * 4KB = 4GB.

Erstellen der zusammengeführten Textdatei ​

Wir benötigen zuerst einen Generator, um Dateien zu iterieren:

python
import os
import json


def json_file_reader(directory):
    json_files = [filename for filename in os.listdir(directory) if filename.endswith('.json')]
    sorted_files = sorted(json_files, key=lambda x: int(os.path.splitext(x)[0]))
    for filename in sorted_files:
        file_path = os.path.join(directory, filename)
        if os.path.isfile(file_path):
            with open(file_path, 'r', encoding='utf-8') as json_file:
                yield os.path.splitext(filename)[0], json_file.read()

Außerdem sortiere ich nach Dateinamen, aber das ist nicht notwendig.

Zusammengeführtes Skript, wir müssen der JSON-Dict auch eine ID hinzufügen, sonst wäre es unmöglich, die Quelldatei zu verstehen:

python
with open(f'{workspace_dir}/merged.json', 'w', encoding='utf-8') as infile:
    for id, data in json_file_reader(output_dir):
        dict = json.loads(data)
        dict['id'] = int(id)
        infile.write(f'{json.dumps(dict, ensure_ascii=False, separators=(',', ':'))}\n')
shell
du -k /tmp/data/merged.json 
557060	merged.json

Jetzt sind es nur noch 557MB! Mehr als 7-fache Differenz! Wir verschwenden keine Cluster mehr. Die durchschnittliche Dateigröße beträgt 557b, was perfekt zu unserer Inhaltsfunktion random.randint(0, 1000) passt.

Binärformat ​

Nun versuchen wir, eine optimierte Binärstruktur zu verwenden.

Zuerst müssen wir unsere Struktur serialisieren:

python
import struct


def pack(data):
    text_bytes = data['text'].encode('utf-8')
    format_string = f'iiH{len(text_bytes)}s'
    return struct.pack(format_string, data['id'], data['created_at'], len(text_bytes), text_bytes)


def unpack(data):
    offset = 10
    i, ts, l = struct.unpack('iiH', data[:offset])
    text = struct.unpack(f'{l}s', data[offset:offset + l])[0].decode()
    return {
        'id': i,
        'created_at': ts,
        'text': text
    }


# test
packed = pack({"id": 1, "created_at": 1727625937, "text": "Hey!"})
print(f"{packed} -> {unpack(packed)}")

Jetzt erstellen Sie eine Binärdatei:

python
# %%
with open(f'{workspace_dir}/merged.struct.bin', 'wb') as infile:
    for id, data in json_file_reader(output_dir):
        dict = json.loads(data)
        dict['id'] = int(id)
        infile.write(pack(dict))
shell
du -k /tmp/data/merged.struct.bin 
507908	merged.struct.bin

Wir haben die Datei weiter auf ~ 508M reduziert.

MessagePack ​

Manchmal ist es nicht möglich, ein vordefiniertes Schema zu haben, besonders wenn wir beliebiges JSON speichern möchten. Lassen Sie uns MessagePack ausprobieren.

Installieren Sie es zuerst: pip install msgpack.

Jetzt generieren Sie die zusammengeführte Binärdatei:

python
import msgpack

with open(f'{workspace_dir}/merged.msgpack.bin', 'wb') as infile:
    for id, data in json_file_reader(output_dir):
        dict = json.loads(data)
        dict['id'] = int(id)
        infile.write(msgpack.packb(dict))
shell
du -k /tmp/data/merged.msgpack.bin 
524292	merged.msgpack.bin

Die Größe ist etwas größer als beim benutzerdefinierten Protokoll ~524M, aber ich denke, es ist ein angemessener Preis für Schemalosigkeit. MessagePack bietet ein sehr praktisches Tool für den Direktzugriff:

python
with open(f'{workspace_dir}/merged.msgpack.bin', 'rb') as file:
    file.seek(0)

    unpacker = msgpack.Unpacker(file)
    obj = unpacker.unpack()
    print(obj)
    print(unpacker.tell())  # aktueller Versatz

Kompression ​

Lassen Sie uns weitergehen und versuchen, diese Dateien mit lz4 und zstd zu komprimieren:

shell
# dir
-> time tar -cf - jsons | lz4 - jsons.tar.lz4
Compressed 3360880640 bytes into 628357651 bytes ==> 18.70%                    
tar -cf - jsons  19.93s user 299.75s system 61% cpu 8:38.46 total
lz4 - jsons.tar.lz4  2.20s user 1.41s system 0% cpu 8:38.46 total
-> time tar -cf - jsons | zstd -o jsons.tar.zst

-> time lz4 merged.json
Compressed 558739918 bytes into 522708179 bytes ==> 93.55%                     
lz4 merged.json  0.47s user 0.35s system 152% cpu 0.534 total

Ergebnisse ​

NameRohgrößeVerringerunglz4 Größelz4 Zeitzstd Größezstd Zeit
Dateien im Verzeichnis4G1x628M8:38464M8:55
Zusammengeführte Textdatei557M7.18x523M0.53s409M1.62s
Zusammengeführte Binärdatei (struct)508M7.87x510M0.46410M1.47s
Zusammengeführte Binärdatei (msgpack)524M7.63x514M0.36s411M1.6s