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