So speichern Sie kleine Textdateien effizient
Vorbereitung
Lassen Sie uns ein Testdataset mit vielen Textdateien erstellen.
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):
{
"text": "random string",
"created_at": 1727625937
}
Lassen Sie uns die tatsächliche Größe messen:
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:
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:
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')
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:
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:
# %%
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))
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:
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))
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:
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:
# 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
Name | Rohgröße | Verringerung | lz4 Größe | lz4 Zeit | zstd Größe | zstd Zeit |
---|---|---|---|---|---|---|
Dateien im Verzeichnis | 4G | 1x | 628M | 8:38 | 464M | 8:55 |
Zusammengeführte Textdatei | 557M | 7.18x | 523M | 0.53s | 409M | 1.62s |
Zusammengeführte Binärdatei (struct) | 508M | 7.87x | 510M | 0.46 | 410M | 1.47s |
Zusammengeführte Binärdatei (msgpack) | 524M | 7.63x | 514M | 0.36s | 411M | 1.6s |