كيف يبدو أرشيف الرمز البريدي وما الذي يمكننا فعله به. الجزء 2 - واصف البيانات وضغطها

استمرار المقال كيف يبدو أرشيف الرمز البريدي وماذا يمكننا أن نفعل به .


مقدمة


يوم جيد.
ومرة أخرى على الهواء لدينا برامج غير تقليدية في PHP.


في مقال سابق ، كان القراء المحترمون مهتمين بالضغط والرمز البريدي. اليوم سنحاول فتح هذا الموضوع قليلاً.


دعنا نلقي نظرة على

رمز من المادة الأخيرة
<?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; 


ما هي مشكلته؟ حسنًا ، في الإنصاف ، تجدر الإشارة إلى أن ميزته الوحيدة هي أنه يعمل ، وهناك مشاكل هناك ، ولكن لا يزال.

في رأيي ، المشكلة الرئيسية هي أنه يجب علينا أولاً كتابة "رأس الملف المحلي" (LFH) مع crc32 وطول الملف ، ثم محتويات الملف نفسه.
ماذا يهدد هذا؟ أو نقوم بتحميل الملف بأكمله في الذاكرة ، والنظر في crc32 لذلك ، وكتابة LFH ، ثم تكون محتويات الملف اقتصادية من وجهة نظر I / O ، لكنها غير مقبولة مع الملفات الكبيرة. أو نقرأ الملف مرتين - أولاً لحساب البعثرة ، ثم لقراءة المحتويات والكتابة إلى الأرشيف - اقتصاديًا من وجهة نظر RAM ، ولكن ، على سبيل المثال ، يضاعف الحمل على محرك الأقراص ، وهو ليس بالضرورة SSD.

وإذا كان الملف يقع عن بعد وحجمه ، على سبيل المثال ، 1.5GB؟ حسنًا ، يجب عليك إما تحميل كل 1.5 جيجا بايت في الذاكرة ، أو الانتظار حتى يتم تنزيل كل هذه السعة 1.5 جيجا بايت وسنقوم بحساب التجزئة ، ثم ننزعها مرة أخرى لإعطاء المحتويات. إذا كنا نريد أن نقدم على الطاير ، على سبيل المثال ، قاعدة بيانات تفريغ ، والتي ، على سبيل المثال ، نقرأها من stdout ، فإن هذا غير مقبول عمومًا - لقد تغيرت البيانات في قاعدة البيانات ، وستتغير بيانات التفريغ ، وسيكون هناك تجزئة مختلفة تمامًا وسنحصل على أرشيف غير صالح. نعم ، الأمور سيئة ، بالطبع.


هيكل واصف البيانات لتدفق سجلات الأرشيف


لكن لا يتم إحباطك ، فإن مواصفات ZIP تسمح لنا بكتابة البيانات أولاً ، ثم التمسك ببنية واصف البيانات (DD) بعد البيانات ، التي تحتوي بالفعل على crc32 وطول البيانات المحزومة وطول البيانات دون ضغط. للقيام بذلك ، نحتاج فقط 3 مرات في اليوم على معدة فارغة في LFH حدد generalPurposeBitFlag تساوي 0x0008 ، وحدد crc32 و compressedSize و uncompressedSize 0 . بعد ذلك ، بعد البيانات ، نكتب بنية DD ، والتي ستبدو كما يلي:



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

وفي رأس ملف الدليل المركزي (CDFH) فقط generalPurposeBitFlag ، يجب أن تكون بقية البيانات حقيقية. لكن هذه ليست مشكلة ، حيث نكتب CDFH بعد جميع البيانات ، والتجزئة مع أطوال البيانات معروفة في أي حال.


هذا كل شيء ، بالطبع ، جيد. يبقى فقط للتنفيذ في PHP.
وستساعدنا مكتبة هاش القياسية. يمكننا إنشاء سياق تجزئة سيكون فيه كافياً لتعبئة القطع بالبيانات ، وفي النهاية الحصول على قيمة التجزئة. بالطبع ، سيكون هذا الحل أكثر تعقيدًا إلى حد ما من التجزئة ('crc32b' ، المحتوى $) ، ولكنه سيوفر لنا مجموعة من الموارد والوقت لا يمكن تخيلهما .

يبدو شيء مثل هذا:

 $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); 

إذا تم كل شيء بشكل صحيح ، فلن تختلف القيمة على الإطلاق عن hash_file ('crc32b' ، أو $ source) أو hash ('crc32b' ، file_get_content ($ source)) .

دعونا نحاول أن نلف كل هذا بطريقة أو بأخرى في وظيفة واحدة ، حتى نتمكن من قراءة الملف بطريقة مناسبة لنا ، وفي النهاية نحصل على التجزئة والطول. وسوف تساعدنا المولدات في هذا:

 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))]; } 

والآن يمكننا فقط

 $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; 

في رأيي ، الأمر بسيط ومريح للغاية. مع ملف 1 غيغابايت ، كان الحد الأقصى لاستهلاك الذاكرة 2 ميغابايت.

الآن دعونا نحاول تعديل الشفرة من المقالة السابقة حتى نتمكن من استخدام هذه الوظيفة.

النصي النهائي
 <?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; 


في الخرج ، يجب أن نحصل على أرشيف مضغوط باسم test.zip ، سيكون فيه ملف به البرنامج النصي أعلاه و 100 MB.bin ، بحجم حوالي 100 ميغابايت.

ضغط في الرمز البريدي المحفوظات


الآن لدينا كل شيء تقريبًا لضغط البيانات والقيام بذلك أيضًا أثناء الطيران.
تمامًا كما حصلنا على علامة تجزئة عن طريق إعطاء مجموعات صغيرة للوظائف ، يمكننا أيضًا ضغط البيانات بفضل مكتبة Zlib الرائعة ووظائف deflate_init و deflate_add الخاصة بها .


يبدو شيء مثل هذا:

 $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; } 

لقد قابلت خيارًا كهذا ، مقارنةً بالخيار السابق ، سيضيف بعض الأصفار في النهاية.
المفسد العنوان
 while (!feof($handle)) { yield deflate_add($deflateCtx, $chunk, ZLIB_SYNC_FLUSH); } yield deflate_add($deflateCtx, '', ZLIB_FINISH); 

ولكن بفك أقسم ، لذلك اضطررت للتخلص من هذا التبسيط.

دعنا نصلح القارئ الخاص بنا حتى يقوم بضغط بياناتنا على الفور ، وفي النهاية يعيدنا التجزئة ، وطول البيانات دون ضغط وطول البيانات مع الضغط:

 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)) ]; } 

وحاول على ملف 100 ميغابايت:

 $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; 

لا يزال استهلاك الذاكرة يدل على أننا لم نقم بتحميل الملف بأكمله في الذاكرة.

دعنا نجمع كل شيء معًا وأخيراً احصل على أرشيف نص حقيقي بالفعل.
على عكس الإصدار السابق ، سيتغير generalPurposeBitFlag - الآن تبلغ قيمته 0x0018 ، وكذلك compressionMethod - 8 (مما يعني Deflate ).

النصي النهائي
 <?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; 

نتيجةً لذلك ، حصلت على أرشيف بحجم 360183 بايت (تم ضغط ملفنا البالغ 100 ميجابايت بشكل جيد للغاية ، والذي على الأرجح مجرد مجموعة من وحدات البايت المتطابقة) ، وأظهر unzip عدم وجود أخطاء في الأرشيف.

استنتاج


إذا كان لدي ما يكفي من الطاقة والوقت لمقال آخر ، فسأحاول توضيح كيف ، والأهم من ذلك ، لماذا يمكن استخدام كل هذا.

إذا كنت مهتمًا بأي شيء آخر حول هذا الموضوع - اقترح في التعليقات ، سأحاول إعطاء إجابة لسؤالك. على الأرجح لن نتعامل مع التشفير ، لأن البرنامج النصي قد نما بالفعل ، وفي الواقع ، لا تستخدم هذه المحفوظات في كثير من الأحيان.



شكرا لاهتمامكم وتعليقاتكم.

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


All Articles