Wie das Zip-Archiv aussieht und was wir damit machen können. Teil 2 - Datenbeschreibung und Komprimierung

Fortsetzung des Artikels Wie das Zip-Archiv aussieht und was wir damit machen können .


Vorwort


Guten Tag.
Und wieder auf Sendung haben wir unkonventionelle Programmierung in PHP.


In einem früheren Artikel waren angesehene Leser an Zip-Komprimierung und Zip-Streaming interessiert. Heute werden wir versuchen, dieses Thema ein wenig zu eröffnen.


Werfen wir einen Blick darauf

Code aus dem letzten Artikel
<?php //        (1.txt  2.txt)   : $entries = [ '1.txt' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc id ante ultrices, fermentum nibh eleifend, ullamcorper nunc. Sed dignissim ut odio et imperdiet. Nunc id felis et ligula viverra blandit a sit amet magna. Vestibulum facilisis venenatis enim sed bibendum. Duis maximus felis in suscipit bibendum. Mauris suscipit turpis eleifend nibh commodo imperdiet. Donec tincidunt porta interdum. Aenean interdum condimentum ligula, vitae ornare lorem auctor in. Suspendisse metus ipsum, porttitor et sapien id, fringilla aliquam nibh. Curabitur sem lacus, ultrices quis felis sed, blandit commodo metus. Duis tincidunt vel mauris at accumsan. Integer et ipsum fermentum leo viverra blandit.', '2.txt' => 'Mauris in purus sit amet ante tempor finibus nec sed justo. Integer ac nibh tempus, mollis sem vel, consequat diam. Pellentesque ut condimentum ex. Praesent finibus volutpat gravida. Vivamus eleifend neque sit amet diam scelerisque lacinia. Nunc imperdiet augue in suscipit lacinia. Curabitur orci diam, iaculis non ligula vitae, porta pellentesque est. Duis dolor erat, placerat a lacus eu, scelerisque egestas massa. Aliquam molestie pulvinar faucibus. Quisque consequat, dolor mattis lacinia pretium, eros eros tempor neque, volutpat consectetur elit elit non diam. In faucibus nulla justo, non dignissim erat maximus consectetur. Sed porttitor turpis nisl, elementum aliquam dui tincidunt nec. Nunc eu enim at nibh molestie porta ut ac erat. Sed tortor sem, mollis eget sodales vel, faucibus in dolor.', ]; //      Lorem.zip,      cwd (      ) $destination = 'Lorem.zip'; $handle = fopen($destination, 'w'); //      ,    ,     ,   "" Central Directory File Header $written = 0; $dictionary = []; foreach ($entries as $filename => $content) { //         Local File Header,     //        ,      . $fileInfo = [ //     'versionToExtract' => 10, //   0,        - 'generalPurposeBitFlag' => 0, //      ,    0 'compressionMethod' => 0, // -    mtime ,    ,      ? 'modificationTime' => 28021, //   , ? 'modificationDate' => 20072, //      .     ,       ,   ? 'crc32' => hexdec(hash('crc32b', $content)), //     .        . //       :) 'compressedSize' => $size = strlen($content), 'uncompressedSize' => $size, //    'filenameLength' => strlen($filename), //  .    ,   0. 'extraFieldLength' => 0, ]; //      . $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, //  Local File Header ] + $fileInfo + ['filename' => $filename])); //       ,       Central Directory File Header $dictionary[$filename] = [ 'signature' => 0x02014b50, //  Central Directory File Header 'versionMadeBy' => 798, //  .    ,  -  . ] + $fileInfo + [ 'fileCommentLength' => 0, //    . No comments 'diskNumber' => 0, //     0,        'internalFileAttributes' => 0, //    'externalFileAttributes' => 2176057344, //    'localFileHeaderOffset' => $written, //      Local File Header 'filename' => $filename, //  . ]; //      $written += fwrite($handle, $LFH); //    $written += fwrite($handle, $content); } // ,     ,    . //          End of central directory record (EOCD) $EOCD = [ //  EOCD 'signature' => 0x06054b50, //  .    ,   0 'diskNumber' => 0, //      -  0 'startDiskNumber' => 0, //       . 'numberCentralDirectoryRecord' => $records = count($dictionary), //    .    ,     'totalCentralDirectoryRecord' => $records, //   Central Directory Record. //      ,      'sizeOfCentralDirectory' => 0, // ,    Central Directory Records 'centralDirectoryOffset' => $written, //     'commentLength' => 0 ]; //     !   foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } // ,   .  ,    $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; //     End of central directory record $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); //  . fclose($handle); echo '  : ' . $written . ' ' . PHP_EOL; echo '     `unzip -tq ' . $destination . '`' . PHP_EOL; echo PHP_EOL; 


Was ist sein Problem? Nun, fairerweise ist es erwähnenswert, dass sein einziger Vorteil darin besteht, dass es funktioniert und es dort Probleme gibt, aber immer noch.

Meiner Meinung nach besteht das Hauptproblem darin, dass wir zuerst den lokalen Dateikopf (Local File Header, LFH) mit crc32 und der Dateilänge und dann den Inhalt der Datei selbst schreiben müssen.
Was bedroht das? Oder wir laden die gesamte Datei in den Speicher, ziehen crc32 in Betracht, schreiben LFH , und dann ist der Inhalt der Datei aus Sicht der E / A wirtschaftlich, bei großen Dateien jedoch nicht akzeptabel. Oder wir lesen die Datei zweimal - zuerst, um den Hash zu berechnen, und dann, um den Inhalt zu lesen und in das Archiv zu schreiben - wirtschaftlich aus Sicht des RAM, aber zum Beispiel verdoppelt sie zum einen die Belastung des Laufwerks, das nicht unbedingt eine SSD ist.

Und wenn sich die Datei remote befindet und ihr Volumen beispielsweise 1,5 GB beträgt? Nun, Sie müssen entweder alle 1,5 GB in den Speicher laden oder warten, bis alle diese 1,5 GB heruntergeladen sind, und wir werden den Hash berechnen und sie dann erneut herunterladen, um den Inhalt zu erhalten. Wenn wir beispielsweise im laufenden Betrieb eine Dump-Datenbank bereitstellen möchten, die wir beispielsweise aus stdout lesen, ist dies im Allgemeinen nicht akzeptabel. Die Daten in der Datenbank haben sich geändert, die Dump-Daten ändern sich, es gibt einen völlig anderen Hash und wir erhalten ein ungültiges Archiv. Ja, die Dinge sind natürlich schlecht.


Datenbeschreibungsstruktur für das Streaming von Archivdatensätzen


Aber lassen Sie sich nicht entmutigen, die ZIP-Spezifikation ermöglicht es uns, zuerst Daten zu schreiben und dann die Data Descriptor (DD) -Struktur nach den Daten zu kleben, die bereits crc32, die Länge der gepackten Daten und die Länge der Daten ohne Komprimierung enthalten. Dazu müssen wir in LFH nur dreimal täglich auf nüchternen Magen generalPurposeBitFlag gleich 0x0008 angeben , und crc32 , compressSize und unkomprimierte Größe geben 0 an . Dann schreiben wir nach den Daten die DD- Struktur, die ungefähr so ​​aussieht:



 pack('LLLL', ...array_values([ 'signature' => 0x08074b50, //  Data Descriptor 'crc32' => $crc32, //  crc32    'compressedSize' => $compressedSize, //    'uncompressedSize' => $uncompressedSize, //    . ])); 

Und im Central Directory File Header (CDFH) ändert sich nur der generalPurposeBitFlag , der Rest der Daten muss real sein. Dies ist jedoch kein Problem, da wir nach allen Daten CDFH schreiben und Hashes mit Datenlängen in jedem Fall bekannt sind.


Das ist natürlich alles gut. Es bleibt nur in PHP zu implementieren.
Und die Standard- Hash- Bibliothek wird uns sehr helfen. Wir können einen Hash-Kontext erstellen, in dem es ausreicht, Chunks mit Daten zu füllen und am Ende den Hash-Wert zu erhalten. Natürlich ist diese Lösung etwas umständlicher als Hash ('crc32b', $ content) , aber sie spart uns nur eine unvorstellbare Menge an Ressourcen und Zeit.

Es sieht ungefähr so ​​aus:

 $hashCtx = hash_init('crc32b'); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $chunk = null; } $hash = hash_final($hashCtx); 

Wenn alles richtig gemacht wurde, unterscheidet sich der Wert überhaupt nicht von hash_file ('crc32b', $ source) oder hash ('crc32b', file_get_content ($ source)) .

Lassen Sie uns versuchen, dies alles irgendwie in einer Funktion zusammenzufassen, damit wir die Datei auf bequeme Weise für uns lesen und am Ende ihren Hash und ihre Länge erhalten können. Und die Generatoren helfen uns dabei:

 function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; } 

und jetzt können wir einfach

 $reader = read('https://speed.hetzner.de/1GB.bin'); foreach ($reader as $chunk) { // -   . } //      . ['length' => $length, 'crc32' => $crc32] = $reader->getReturn(); echo round(memory_get_peak_usage(true) / 1024 / 1024, 2) . 'MB - Memory Peak Usage' . PHP_EOL; 

Meiner Meinung nach ist es ganz einfach und bequem. Bei einer 1-GB-Datei betrug mein maximaler Speicherverbrauch 2 MB.

Versuchen wir nun, den Code aus dem vorherigen Artikel so zu ändern, dass wir diese Funktion verwenden können.

Endgültiges Skript
 <?php function read(string $path): \Generator { $length = 0; $handle = fopen($path, 'r'); $hashCtx = hash_init('crc32b'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); $length += strlen($chunk); hash_update($hashCtx, $chunk); yield $chunk; $chunk = null; } fclose($handle); return ['length' => $length, 'crc32' => hexdec(hash_final($hashCtx))]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10, //       Data Descriptor,     00008, //   00000    . 'generalPurposeBitFlag' => 0x0008, 'compressionMethod' => 0, 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => 0, 'compressedSize' => 0, 'uncompressedSize' => 0, 'filenameLength' => strlen($filename), 'extraFieldLength' => 0, ]; $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, ] + $fileInfo + ['filename' => $filename])); $fileOffset = $written; $written += fwrite($handle, $LFH); //     $reader = read($entry); foreach ($reader as $chunk) { //      $written += fwrite($handle, $chunk); $chunk = null; } //       ['length' => $length, 'crc32' => $crc32] = $reader->getReturn(); //    fileInfo,     CDFH $fileInfo['crc32'] = $crc32; $fileInfo['compressedSize'] = $length; $fileInfo['uncompressedSize'] = $length; //  Data Descriptor $DD = pack('LLLL', ...array_values([ 'signature' => 0x08074b50, 'crc32' => $fileInfo['crc32'], 'compressedSize' => $fileInfo['compressedSize'], 'uncompressedSize' => $fileInfo['uncompressedSize'], ])); $written += fwrite($handle, $DD); $dictionary[$filename] = [ 'signature' => 0x02014b50, 'versionMadeBy' => 798, ] + $fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $fileOffset, 'filename' => $filename, ]; } $EOCD = [ 'signature' => 0x06054b50, 'diskNumber' => 0, 'startDiskNumber' => 0, 'numberCentralDirectoryRecord' => $records = count($dictionary), 'totalCentralDirectoryRecord' => $records, 'sizeOfCentralDirectory' => 0, 'centralDirectoryOffset' => $written, 'commentLength' => 0 ]; foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); fclose($handle); echo '  : ' . memory_get_peak_usage(true) . ' ' . PHP_EOL; echo '  : ' . $written . ' ' . PHP_EOL; echo '   `unzip -tq ' . $destination . '`: ' . PHP_EOL; echo '> ' . exec('unzip -tq ' . $destination) . PHP_EOL; echo PHP_EOL; 


Bei der Ausgabe sollten wir ein Zip-Archiv mit dem Namen test.zip erhalten, in dem sich eine Datei mit dem obigen Skript und 100 MB.Bin befindet, die ungefähr 100 MB groß ist.

Komprimierung in Zip-Archiven


Jetzt haben wir praktisch alles, um die Daten zu komprimieren und dies auch im laufenden Betrieb zu tun.
So wie wir einen Hash erhalten, indem wir Funktionen kleine Blöcke zuweisen , können wir dank der wunderbaren Zlib- Bibliothek und ihrer Funktionen deflate_init und deflate_add auch Daten komprimieren.


Es sieht ungefähr so ​​aus:

 $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($source, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); yield deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $chunk = null; } 

Ich habe eine Option wie diese getroffen, die im Vergleich zur vorherigen am Ende einige Nullen hinzufügt.
Spoiler Überschrift
 while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH); 

Aber entpacken schwor, also musste ich solche Vereinfachung loswerden.

Korrigieren wir unseren Reader so, dass er unsere Daten sofort komprimiert und am Ende einen Hash zurückgibt, die Länge der Daten ohne Komprimierung und die Länge der Daten mit Komprimierung:

 function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; } 

und probieren Sie eine 100-MB-Datei an:

 $reader = read('https://speed.hetzner.de/100MB.bin'); foreach ($reader as $chunk) { // -   . } ['uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => $crc32] = $reader->getReturn(); echo 'Uncompressed size: ' . $uncompressedSize . PHP_EOL; echo 'Compressed size: ' . $compressedSize . PHP_EOL; echo round(memory_get_peak_usage(true) / 1024 / 1024, 2) . 'MB - Memory Peak Usage' . PHP_EOL; 

Der Speicherverbrauch zeigt immer noch, dass wir nicht die gesamte Datei in den Speicher geladen haben.

Lassen Sie uns alles zusammenfügen und endlich einen wirklich echten Skriptarchivierer bekommen.
Im Gegensatz zur vorherigen Version ändert sich unser generalPurposeBitFlag - jetzt ist sein Wert 0x0018 sowie die Komprimierungsmethode - 8 (was " Deflate" bedeutet).

Endgültiges Skript
 <?php function read(string $path): \Generator { $uncompressedSize = 0; $compressedSize = 0; $hashCtx = hash_init('crc32b'); $deflateCtx = deflate_init(ZLIB_ENCODING_RAW, ['level' => 6]); $handle = fopen($path, 'r'); while (!feof($handle)) { $chunk = fread($handle, 8 * 1024); hash_update($hashCtx, $chunk); $compressedChunk = deflate_add($deflateCtx, $chunk, feof($handle) ? ZLIB_FINISH : ZLIB_SYNC_FLUSH); $uncompressedSize += strlen($chunk); $compressedSize += strlen($compressedChunk); yield $compressedChunk; $chunk = null; $compressedChunk = null; } fclose($handle); return [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => hexdec(hash_final($hashCtx)) ]; } $entries = ['https://speed.hetzner.de/100MB.bin', __FILE__]; $destination = 'test.zip'; $handle = fopen($destination, 'w'); $written = 0; $dictionary = []; foreach ($entries as $entry) { $filename = basename($entry); $fileInfo = [ 'versionToExtract' => 10, //   ,        0x0018  0x0008 'generalPurposeBitFlag' => 0x0018, 'compressionMethod' => 8, //      : 8 - Deflate 'modificationTime' => 28021, 'modificationDate' => 20072, 'crc32' => 0, 'compressedSize' => 0, 'uncompressedSize' => 0, 'filenameLength' => strlen($filename), 'extraFieldLength' => 0, ]; $LFH = pack('LSSSSSLLLSSa*', ...array_values([ 'signature' => 0x04034b50, ] + $fileInfo + ['filename' => $filename])); $fileOffset = $written; $written += fwrite($handle, $LFH); $reader = read($entry); foreach ($reader as $chunk) { $written += fwrite($handle, $chunk); $chunk = null; } [ 'uncompressedSize' => $uncompressedSize, 'compressedSize' => $compressedSize, 'crc32' => $crc32 ] = $reader->getReturn(); $fileInfo['crc32'] = $crc32; $fileInfo['compressedSize'] = $compressedSize; $fileInfo['uncompressedSize'] = $uncompressedSize; $DD = pack('LLLL', ...array_values([ 'signature' => 0x08074b50, 'crc32' => $fileInfo['crc32'], 'compressedSize' => $fileInfo['compressedSize'], 'uncompressedSize' => $fileInfo['uncompressedSize'], ])); $written += fwrite($handle, $DD); $dictionary[$filename] = [ 'signature' => 0x02014b50, 'versionMadeBy' => 798, ] + $fileInfo + [ 'fileCommentLength' => 0, 'diskNumber' => 0, 'internalFileAttributes' => 0, 'externalFileAttributes' => 2176057344, 'localFileHeaderOffset' => $fileOffset, 'filename' => $filename, ]; } $EOCD = [ 'signature' => 0x06054b50, 'diskNumber' => 0, 'startDiskNumber' => 0, 'numberCentralDirectoryRecord' => $records = count($dictionary), 'totalCentralDirectoryRecord' => $records, 'sizeOfCentralDirectory' => 0, 'centralDirectoryOffset' => $written, 'commentLength' => 0 ]; foreach ($dictionary as $entryInfo) { $CDFH = pack('LSSSSSSLLLSSSSSLLa*', ...array_values($entryInfo)); $written += fwrite($handle, $CDFH); } $EOCD['sizeOfCentralDirectory'] = $written - $EOCD['centralDirectoryOffset']; $EOCD = pack('LSSSSLLS', ...array_values($EOCD)); $written += fwrite($handle, $EOCD); fclose($handle); echo '  : ' . memory_get_peak_usage(true) . ' ' . PHP_EOL; echo '  : ' . $written . ' ' . PHP_EOL; echo '   `unzip -tq ' . $destination . '`: ' . PHP_EOL; echo '> ' . exec('unzip -tq ' . $destination) . PHP_EOL; echo PHP_EOL; 

Als Ergebnis erhielt ich ein Archiv mit einer Größe von 360183 Bytes (unsere 100-MB-Datei wurde sehr gut komprimiert, was höchstwahrscheinlich nur ein Satz identischer Bytes ist), und das Entpacken zeigte, dass im Archiv keine Fehler gefunden wurden.

Fazit


Wenn ich genug Energie und Zeit für einen anderen Artikel habe, werde ich versuchen zu zeigen, wie und vor allem warum all dies genutzt werden kann.

Wenn Sie an etwas anderem zu diesem Thema interessiert sind - schlagen Sie in den Kommentaren vor, ich werde versuchen, eine Antwort auf Ihre Frage zu geben. Höchstwahrscheinlich werden wir uns nicht mit Verschlüsselung befassen, da das Skript bereits gewachsen ist und solche Archive im wirklichen Leben meines Erachtens nicht sehr oft verwendet werden.



Vielen Dank für Ihre Aufmerksamkeit und Ihre Kommentare.

Source: https://habr.com/ru/post/de472966/


All Articles