> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# كيف يعمل ضغط البيانات Compression؟ من Huffman إلى Zstd
- URL: https://arabroot.io/articles/كيف-يعمل-ضغط-البيانات-compression/
- Published: 2026-10-10T06:15:00.000Z
- Updated: 2026-10-09T14:42:32.000Z
- Description: لماذا تصغر ملفات السجلات إلى العشر ولا تصغر الصور أبداً، وكيف تختار أداة الضغط المناسبة بين السرعة والحجم.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, DevOps وCI/CD, التشغيل والنسخ الاحتياطي, الاستضافة الذاتية

ملف سجلات حجمه 20 MB يصبح بعد `gzip` أقل من 2.4 MB، أي أصغر من ثماني مرات، بينما مجلد فيه صور JPEG بنفس الأداة لا يصغر إلا 2%، وملف مشفر قد يكبر بعد ضغطه بدل أن يصغر. وإذا كنت تدير سيرفرات فأنت تتعامل مع الضغط Compression كل يوم دون أن تفكر فيه: في تدوير السجلات Log Rotation، وفي النسخ الاحتياطية، وفي كل صفحة يرسلها ال Reverse Proxy مضغوطة إلى المتصفح، وفي صور PNG وملفات ZIP. والسؤال الذي يجيب عنه هذا المقال هو: لماذا يصغر ملف ولا يصغر آخر، وكيف تعمل الخوارزميات من الداخل، وكيف تختار الأداة والمستوى المناسبين بالأرقام وليس بالانطباع.

وهذا المقال من مرحلة [أساسيات علوم الحاسب Computer Science Fundamentals](https://arabroot.io/start/#stage-foundations)، ويفترض أنك تعرف البت والبايت من مقال [الأنظمة العددية للمبرمجين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A7%D9%86%D8%B8%D9%85%D8%A9-%D8%A7%D9%84%D8%B9%D8%AF%D8%AF%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AB%D9%86%D8%A7%D8%A6%D9%8A-%D9%84%D9%84%D9%85%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D9%86/)، وأن تعرف الفرق بين الترميز والتشفير من مقال [الترميز Encoding](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/). وسوف نناقش في هذا المقال ما يلي:

- لماذا يعمل الضغط أصلاً: التكرار Redundancy والإنتروبيا Entropy كما عرفها Shannon في 1948، مع حساب كامل باليد.
- الضغط دون فقد Lossless والضغط مع الفقد Lossy، ثم الطرق الأساسية: ترميز التكرار RLE وترميز Huffman والطرق المبنية على القاموس LZ77 وLZ78 وLZW.
- DEFLATE الذي يجمع LZ77 مع Huffman، ثم الأدوات الحديثة: Brotli وZstandard وLZMA وLZ4، والمقايضة بين السرعة والحجم.
- لماذا لا تضغط البيانات المضغوطة والمشفرة، والضغط في HTTP وهجوم BREACH باختصار.
- تجربة عملية: مقارنة ست أدوات بعدة مستويات على ثلاثة أنواع من البيانات، وبرنامج Huffman صغير بلغة Python.

## لماذا يمكن ضغط البيانات أصلاً؟

الضغط Compression هو أن تكتب نفس المعلومات ببتات أقل، وهذا ممكن فقط لأن أغلب البيانات التي نتعامل معها فيها تكرار Redundancy: كلمة `GET` تتكرر في كل سطر من سجلات Nginx، والمفتاح `"customer"` يتكرر في كل سجل JSON، والحرف «ا» أكثر تكراراً في النص العربي من «ظ»، ومع ذلك نحفظ كل حرف منها بنفس عدد البايتات. ولتقريب الفكرة من الحياة، فأنت عندما تكتب لزميلك «نفس الإعداد السابق مع تغيير المنفذ إلى 8080» بدل أن تنسخ له الإعداد كاملاً، فأنت تضغط الرسالة لأنكما تشتركان في ما قبلها.

ولكن هناك حد لا يمكن تجاوزه، وقد وضعه Claude Shannon في بحثه [«A Mathematical Theory of Communication»](https://ieeexplore.ieee.org/document/6773024?ref=arabroot.io) المنشور في 1948، حيث عرف الإنتروبيا Entropy، وهي متوسط كمية المعلومات في كل رمز، وتحسب بالبتات هكذا:

```text
H = - Σ p(x) × log2 p(x)
```

حيث `p(x)` هي نسبة تكرار الرمز `x`. والمعنى العملي أن الإنتروبيا هي أقل عدد من البتات تحتاجه لكل رمز في المتوسط إذا كنت تضغط كل رمز وحده بناء على تكراره فقط، فلا توجد أي خوارزمية من هذا النوع تستطيع أن تنزل تحتها.

لنأخذ مثالاً الكلمة `ABRACADABRA`، وهي 11 حرفاً: A خمس مرات، وB مرتان، وR مرتان، وC مرة، وD مرة. الآن نحسب:

- الحرف A نسبته 5/11 = 0.4545، و`log2(0.4545) = -1.1375`، فمساهمته 0.4545 × 1.1375 = 0.5170 بت.
- الحرفان B وR نسبة كل منهما 2/11 = 0.1818، و`log2(0.1818) = -2.4594`، فمساهمة كل منهما 0.4472، ومجموعهما 0.8944 بت.
- الحرفان C وD نسبة كل منهما 1/11 = 0.0909، و`log2(0.0909) = -3.4594`، فمساهمة كل منهما 0.3145، ومجموعهما 0.6290 بت.
- المجموع: H = 0.5170 + 0.8944 + 0.6290 = **2.040 بت لكل حرف**.

أي أن هذه الكلمة تحتاج نظرياً 2.04 × 11 = 22.4 بت، أي 23 بتاً على الأقل، بينما هي مخزنة بـ ASCII أو UTF-8 في 88 بتاً، فالفرق كله تكرار يمكن حذفه. ولاحظ أن الحروف العشوائية تماماً، مثل مخرج مولد أرقام عشوائية أو بيانات مشفرة، تكون إنتروبيتها قريبة من 8 بتات لكل بايت، أي أنه لا يوجد فيها أي شيء يحذف، وهذا سوف نراه بالأرقام في التجربة.

💡

الإنتروبيا التي حسبناها تنظر إلى كل حرف وحده، والطرق التي تستغل تكرار الكلمات والجمل كاملة، مثل LZ77، تستطيع أن تنزل تحت هذا الرقم في النصوص الحقيقية، لأن تكرار `ABRA` كاملة معلومة لا يراها حساب الحروف المنفردة. ولذلك تجمع الأدوات الحديثة بين الطريقتين.

## الضغط دون فقد والضغط مع الفقد

الضغط دون فقد Lossless يعيد لك نفس البايتات بالضبط بعد فك الضغط، وهو الوحيد المقبول لملفات النصوص والكود والقواعد والنسخ الاحتياطية، فبايت واحد مختلف في ملف إعداد يعني ملفاً معطوباً، وكل الأدوات في هذا المقال من هذا النوع. أما الضغط مع الفقد Lossy فيحذف عن قصد تفاصيل لا يلاحظها الإنسان، وهو مقبول فقط للصور والصوت والفيديو، وقد شرحنا كيف يفعل ذلك JPEG وAVIF في [مقال صيغ الصور](https://arabroot.io/articles/%D8%B5%D9%8A%D8%BA-%D8%A7%D9%84%D8%B5%D9%88%D8%B1-%D9%88%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%A8%D9%8A%D9%86%D9%87%D8%A7/)، وكيف يفعله الفيديو في [مقال مفاهيم الفيديو](https://arabroot.io/articles/%D9%85%D9%81%D8%A7%D9%87%D9%8A%D9%85-%D8%A7%D9%84%D9%81%D9%8A%D8%AF%D9%8A%D9%88-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#codec). ولاحظ أن أغلب صيغ الضغط مع الفقد تنتهي بمرحلة ضغط دون فقد، فـ JPEG مثلاً يحذف التفاصيل أولاً ثم يرمز ما بقي بـ Huffman الذي سوف نشرحه الآن.

## ترميز التكرار المتتالي Run-Length Encoding

أبسط طريقة على الإطلاق: بدل أن تكتب `AAAAAAAAAABBB` تكتب `10A3B`، أي الرمز وعدد مرات تكراره المتتالي. وهي ممتازة للبيانات التي فيها مساحات متصلة من نفس القيمة، مثل الصور القديمة بالأبيض والأسود أو مساحة فارغة من الأصفار، ولكنها تفشل تماماً مع النص العادي، والسبب أن الحروف في النص نادراً ما تتكرر متتالية، فكلمة `ABRACADABRA` تصبح `1A1B1R1A1C…`، أي ضعف حجمها. ولذلك لا تستخدم RLE وحدها في الأدوات العامة، وإنما كمرحلة داخل خوارزميات أكبر، فأداة bzip2 مثلاً تبدأ بها، وهذا ما يفسر النتيجة الغريبة التي سوف تراها في التجربة عندما ضغطنا 10 MB من الأصفار.

## ترميز Huffman: الرمز الأكثر تكراراً يأخذ أقصر كود

في 1952 نشر David Huffman طريقته في بحث [«A Method for the Construction of Minimum-Redundancy Codes»](https://ieeexplore.ieee.org/document/4051119?ref=arabroot.io)، والفكرة تشبه شفرة Morse: الحرف الأكثر استخداماً يأخذ أقصر رمز، والنادر يأخذ رمزاً أطول، فيقل المتوسط. والجزء الذكي هو طريقة بناء الرموز بحيث لا يكون أي رمز بداية لرمز آخر، وهذا ما يسمى Prefix Code، فيستطيع القارئ أن يفك البتات المتتالية دون أي فاصل بين الحروف.

والآن نبني الشجرة باليد للكلمة نفسها `ABRACADABRA`:

1. نعد تكرار كل حرف: A=5 وB=2 وR=2 وC=1 وD=1، ونعتبر كل حرف عقدة وحدها.
2. نأخذ أقل عقدتين تكراراً، C وD، ونجمعهما تحت عقدة جديدة تكرارها 2.
3. أقل عقدتين الآن B وR (لكل منهما 2)، نجمعهما تحت عقدة تكرارها 4\. ولاحظ أن العقدة الجديدة من C وD تكرارها 2 أيضاً، وعند التساوي يجوز أي اختيار، والطول الكلي لن يتغير.
4. نجمع العقدة 2 مع العقدة 4 تحت عقدة تكرارها 6.
5. بقيت عقدتان: A=5 والعقدة 6، فنجمعهما تحت العقدة العليا وتكرارها 11، وانتهت الشجرة.
6. نقرأ الرموز: كل انتقال إلى اليسار 0 وإلى اليمين 1، فيكون A=0، وC=100، وD=101، وB=110، وR=111.

الصورة التالية تبين الشجرة الناتجة:

![شجرة Huffman: العقدة العليا 11 يتفرع منها A بتكرار 5 برمز 0 وعقدة 6، والعقدة 6 يتفرع منها عقدة 2 لـ C وD برمزي 100 و101 وعقدة 4 لـ B وR برمزي 110 و111، وعلى اليمين الخطوات: العد ثم الجمع ثم قراءة المسار، والنتيجة 23 بتاً بدل 88](https://arabroot.io/content/images/2026/10/compression-basics-01-huffman-tree.webp)

كل حرف رمزه هو المسار من أعلى الشجرة إليه، فالأكثر تكراراً أقرب إلى الأعلى

والحساب: A خمس مرات ببت واحد = 5، وB وR مرتين لكل منهما بثلاثة بتات = 12، وC وD مرة لكل منهما بثلاثة بتات = 6، فالمجموع **23 بتاً** بدل 88، وهذا يساوي الحد الذي حسبناه من الإنتروبيا تقريباً، 2.09 بت لكل حرف مقابل 2.04\. وقد يتساءل البعض: وكيف يعرف من يفك الضغط هذه الرموز؟ والإجابة أن جدول الرموز نفسه يجب أن يحفظ مع البيانات، ولذلك فإن Huffman لا يفيد مع النصوص القصيرة جداً، ويبدأ أثره الحقيقي عندما يكون الملف أكبر بكثير من الجدول. وفي [التجربة العملية](#lab-huffman) سوف نكتب برنامجاً صغيراً يبني هذه الشجرة ويطبقها على كلمة عربية أيضاً.

## طرق القاموس: LZ77 وLZ78 وLZW

Huffman يرى الحروف وحدها، ولكن التكرار الحقيقي في ملفاتنا هو تكرار كلمات وأسطر كاملة. وهنا جاءت فكرة Abraham Lempel وJacob Ziv في بحثهما [في 1977](https://ieeexplore.ieee.org/document/1055714?ref=arabroot.io)، والتي تسمى LZ77: يقرأ الضاغط البيانات من البداية، ويحتفظ بنافذة Window من آخر ما كتبه، وكلما وجد أن النص القادم موجود في النافذة كتب بدله إشارة قصيرة إليه: «انسخ 4 بايتات من مكان يبعد 7 بايتات للخلف»، أي الزوج (المسافة Distance، الطول Length). والصورة التالية تبين ذلك:

![الكلمة ABRACADABRA في 11 خانة: أول سبعة أحرف تكتب كما هي وتصبح نافذة البحث، وآخر أربعة ABRA موجودة قبل 7 مواقع فتصبح إشارة واحدة مسافة 7 وطول 4، وتحتها سطرا سجلات حيث معظم السطر الثاني منسوخ من الأول ما عدا رقم في عنوان IP ورقم الطلب](https://arabroot.io/content/images/2026/10/compression-basics-02-lz77-window.webp)

كل ما سبق ظهوره في النافذة يكتب كإشارة قصيرة بدل تكراره

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

وبعد عام نشر الباحثان نفسهما [LZ78](https://ieeexplore.ieee.org/document/1055934?ref=arabroot.io)، والذي يبني قاموساً صريحاً من العبارات التي رآها ويرسل رقم العبارة في القاموس بدل المسافة، ثم طوره Terry Welch في [1984](https://ieeexplore.ieee.org/document/1659158?ref=arabroot.io) إلى LZW، وهو الذي استخدمته صور GIF والأداة القديمة `compress` في Unix. أما فرع LZ77 فهو الذي تقوم عليه أغلب الأدوات التي تستخدمها اليوم: gzip وZIP وPNG وZstandard وBrotli وLZMA وLZ4.

## DEFLATE: الخوارزمية التي في كل مكان

DEFLATE، الموصوفة في [RFC 1951](https://datatracker.ietf.org/doc/html/rfc1951?ref=arabroot.io)، هي بالضبط ما شرحناه في القسمين السابقين بالترتيب: LZ77 بنافذة حتى 32 KB وتطابق من 3 إلى 258 بايت، ثم Huffman على ناتجه، أي على الحروف التي لم تجد تطابقاً وعلى أرقام الأطوال والمسافات. وهذه الخوارزمية نفسها تجدها في ثلاثة أغلفة مختلفة، والصورة التالية تبين ذلك:

![في الأعلى ملف سجلات يمر بمرحلة LZ77 فتصبح التكرارات أزواج مسافة وطول، ثم مرحلة Huffman فتصبح بتات، وفي الأسفل ثلاثة أغلفة: gzip برأس 1f 8b 08 ثم البيانات ثم CRC-32 والحجم، وzlib ببايتين ثم البيانات ثم Adler-32، ومدخل ZIP برأس PK 03 04 ثم البيانات](https://arabroot.io/content/images/2026/10/compression-basics-03-deflate.webp)

نفس بيانات DEFLATE داخل gzip وzlib وZIP، والفرق في الرأس وطريقة التحقق من السلامة

- **gzip** حسب [RFC 1952](https://datatracker.ietf.org/doc/html/rfc1952?ref=arabroot.io): رأس من 10 بايتات يبدأ بـ `1f 8b`، ثم البيانات، ثم قيمة التحقق CRC-32 وحجم الملف الأصلي. وقد رأينا هذه البايتات الثمانية الأخيرة وكيف يكتشف `gzip -t` فسادها في [تجربة تحقيق Dirty Pipe](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-max-kellermann-%D8%AB%D8%BA%D8%B1%D8%A9-dirty-pipe/#lab-gzip).
- **zlib** حسب [RFC 1950](https://datatracker.ietf.org/doc/html/rfc1950?ref=arabroot.io): رأس من بايتين وقيمة تحقق Adler-32، وهو الغلاف داخل صور PNG وفي الترميز `deflate` في HTTP.
- **ZIP**: كل ملف داخل الأرشيف مضغوط بـ DEFLATE وحده، مع رأس يحمل اسمه وقيمة CRC-32 الخاصة به، وقد شرحنا هيكل ZIP بالبايت في [مقال ماذا يوجد داخل ملف ZIP](https://arabroot.io/articles/%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D9%88%D8%AC%D8%AF-%D8%AF%D8%A7%D8%AE%D9%84-%D9%85%D9%84%D9%81-zip/).

## الأدوات الحديثة: Brotli وZstandard وLZMA وLZ4

DEFLATE صمم في التسعينات بنافذة 32 KB لأن الذاكرة كانت غالية، والأدوات الحديثة تستخدم نفس الأفكار بنوافذ أكبر بكثير وترميز أذكى من Huffman، ولكل منها هدف مختلف:

- **Brotli** من Google، ومواصفته [RFC 7932](https://datatracker.ietf.org/doc/html/rfc7932?ref=arabroot.io) في يوليو 2016\. أهم ما يميزه قاموس ثابت مدمج في المواصفة حجمه 122,784 بايت، فيه كلمات وعبارات شائعة في صفحات الويب وHTML وJavaScript، فيستطيع أن يضغط حتى ملفاً صغيراً جيداً لأن «القاموس» موجود مسبقاً عند الطرفين. ولذلك صمم للويب: يضغط ببطء مرة واحدة، ويفك بسرعة عند كل زائر.
- **Zstandard** أو zstd، من Yann Collet في Facebook، ومواصفته [RFC 8878](https://datatracker.ietf.org/doc/html/rfc8878?ref=arabroot.io) في فبراير 2021، و[الكود في GitHub](https://github.com/facebook/zstd?ref=arabroot.io). ميزته أنه يغطي كل المدى بأداة واحدة: المستوى 1 أسرع من gzip بكثير، والمستوى 19 يقترب من xz في الحجم، وفك الضغط سريع في كل المستويات، ولهذا أصبح الخيار المعتاد للنسخ الاحتياطية والحزم وأنظمة الملفات.
- **LZMA** من Igor Pavlov مؤلف 7-Zip، وتشرحه [صفحة LZMA SDK](https://www.7-zip.org/sdk.html?ref=arabroot.io)، وتستخدمه صيغة 7z وأداة [xz](https://tukaani.org/xz/?ref=arabroot.io) في Linux. يعطي ملفات صغيرة جداً على حساب ضغط بطيء واستهلاك ذاكرة أكبر، ولذلك تستخدمه التوزيعات لحزمها، فتضغط مرة ويفك الملايين.
- **LZ4** من Yann Collet أيضاً، و[الكود في GitHub](https://github.com/lz4/lz4?ref=arabroot.io)، وهو في الطرف الآخر: LZ77 دون أي ترميز Huffman، فالملف أكبر ولكن السرعة مئات الميغابايتات في الثانية، ولذلك تجده حيث يكون المعالج أغلى من القرص: ضغط الذاكرة وأنظمة الملفات وقواعد البيانات.
- **bzip2** من Julian Seward، و[موقعه الرسمي](https://sourceware.org/bzip2/?ref=arabroot.io)، وهو من عائلة مختلفة تماماً تعيد ترتيب البيانات بتحويل Burrows-Wheeler حتى تتجاور الحروف المتشابهة ثم تضغطها، وهو بطيء في الاتجاهين، وسوف ترى في التجربة أنه فاز في نسبة الضغط على بياناتنا المولدة.

والقاعدة العامة أن كل أداة تقدم مقايضة Trade-off بين ثلاثة أشياء: نسبة الضغط، وسرعة الضغط، وسرعة فك الضغط. ولا توجد أداة تفوز في الثلاثة، فاختر بحسب ما يتكرر أكثر: إذا كنت تضغط مرة وتفك كثيراً، مثل ملفات موقعك الثابتة أو حزم البرامج، فاختر مستوى عالياً، وإذا كنت تضغط باستمرار، مثل السجلات أو البث بين سيرفرين، فاختر مستوى سريعاً.

## لماذا لا تضغط الملفات المضغوطة والمشفرة؟

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

![صفان: في الأول سجلات 5,000,000 بايت تضغط بـ zstd إلى 461,723 ثم تشفر إلى 461,739 بايت أي 9.2% من الأصل، وفي الثاني نفس السجلات تشفر أولاً إلى 5,000,016 بايت ثم تضغط فتصبح 5,000,146 بايت أي أكبر من الأصل](https://arabroot.io/content/images/2026/10/compression-basics-05-order.webp)

نفس البيانات ونفس الأداتين، والفرق في الترتيب فقط

ولهذا السبب تضغط أدوات النسخ الاحتياطي الحديثة البيانات قبل تشفيرها داخلياً، وقد قارنا بعضها في [مقال restic أم Borg أم Kopia](https://arabroot.io/articles/restic-%D8%A3%D9%85-borg-%D8%A3%D9%85-kopia/)، وشرحنا التشفير نفسه في [مقال التشفير للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD/#symmetric).

## الضغط في HTTP: gzip وBrotli وZstandard

كل صفحة HTML وملف CSS وJavaScript يرسله سيرفرك يمكن أن يضغط في الطريق، والمتصفح يعلن ما يفهمه في ترويسة [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding?ref=arabroot.io)، والسيرفر يختار منها ويعلن اختياره في ترويسة [Content-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding?ref=arabroot.io)، وهذه الآلية معرفة في [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110?ref=arabroot.io). والقيم الشائعة اليوم `gzip` و`br` أي Brotli، و`zstd` الذي سجلته RFC 8878، ثم حددت [RFC 9659](https://datatracker.ietf.org/doc/html/rfc9659?ref=arabroot.io) في سبتمبر 2024 أن نافذته في HTTP يجب ألا تتجاوز 8 MB حتى لا تختلف المتصفحات في ما تقبله. أما دعم المتصفحات بحسب [caniuse](https://caniuse.com/zstd?ref=arabroot.io) في أكتوبر 2026: zstd مدعوم في Chrome وEdge منذ الإصدار 123، وFirefox منذ 126، وSafari منذ 26.0 يفكه ولكنه لا يطلبه في كل الأنظمة، وغير مدعوم في Samsung Internet، بينما [Brotli](https://caniuse.com/brotli?ref=arabroot.io) مدعوم في كل المتصفحات منذ سنوات.

ونرى ذلك على مواقع حقيقية، حيث نطلب نفس الصفحة ثلاث مرات بقيم مختلفة لـ Accept-Encoding:

```bash
for u in https://www.facebook.com/ https://www.wikipedia.org/; do
  for e in "gzip" "br, gzip" "zstd, br, gzip"; do
    printf "%-28s %-16s " "$u" "$e"
    curl -s -o /dev/null -D - -H "Accept-Encoding: $e" "$u" | grep -i "^content-encoding" | tr -d "\r"
  done
done
```

والمخرج سوف يكون كما يلي:

```text
https://www.facebook.com/    gzip             content-encoding: gzip
https://www.facebook.com/    br, gzip         content-encoding: br
https://www.facebook.com/    zstd, br, gzip   content-encoding: zstd
https://www.wikipedia.org/   gzip             content-encoding: gzip
https://www.wikipedia.org/   br, gzip         content-encoding: gzip
https://www.wikipedia.org/   zstd, br, gzip   content-encoding: gzip
```

لاحظ أن Facebook اختار أفضل ما عرضناه في كل مرة، بينما بقي Wikipedia على gzip في الحالات الثلاث، فالقرار في النهاية للسيرفر، وgzip هو الحد الأدنى الذي يفهمه الجميع. وعلى سيرفرك تفعل هذا في ال Reverse Proxy، والقاعدة العملية: اضغط النصوص فقط، أي HTML وCSS وJavaScript وJSON وSVG، ولا تضغط الصور والفيديو والملفات المضغوطة أصلاً، والسبب ما رأيته في القسم السابق، فهي تستهلك المعالج دون أي توفير. وللملفات الثابتة التي لا تتغير، مثل حزم JavaScript بعد البناء، اضغطها مسبقاً بأعلى مستوى في Brotli أو zstd مرة واحدة واجعل السيرفر يرسل النسخة الجاهزة، بدل أن يضغطها عند كل طلب بمستوى منخفض.

وهناك تفصيل أمني يجب أن تعرفه، فقد عرض باحثون في 2013 هجوماً اسمه BREACH، مسجلاً باسم [CVE-2013-3587](https://www.cve.org/CVERecord?id=CVE-2013-3587&ref=arabroot.io)، والفكرة أن الصفحة المضغوطة يتغير طولها بحسب التكرار فيها، فإذا كانت الصفحة تعرض في نفس الرد سراً، مثل توكن CSRF، ونصاً يتحكم فيه المهاجم، مثل كلمة بحث من الرابط، فإن المهاجم الذي يرى أطوال الردود المشفرة على الشبكة يستطيع أن يخمن السر حرفاً حرفاً: التخمين الصحيح يتكرر مع السر فيصغر الرد ببايت. والتشفير بـ TLS لا يمنع ذلك لأنه لا يخفي الطول. والحماية العملية ألا تضع الأسرار في صفحة تعكس مدخلات المستخدم، أو تغير قيمة التوكن في كل رد بقناع عشوائي Masking كما تفعل أطر العمل الحديثة، أو توقف الضغط في الردود التي تحمل أسراراً.

## التجربة العملية

الآن نقيس كل ما سبق بأنفسنا. سوف نشغل Container واحداً فيه كل الأدوات، ونولد ثلاث مجموعات بيانات: سجلات وصول بصيغة Nginx، وملف JSON يشبه تصدير طلبات من قاعدة بيانات، ومجموعة صور JPEG، ثم نقيس لكل أداة نسبة الضغط وسرعة الضغط وفك الضغط، ثم نكتب برنامج Huffman صغيراً.

💡

أرقام السرعة خاصة بالجهاز الذي قسنا عليه، وهو معالج Intel Core i7-7700K، وبخيط واحد Thread لكل أداة، وأفضل نتيجة من ثلاث محاولات. على جهازك سوف تختلف الأرقام، ولكن الترتيب بين الأدوات وشكل المقايضة يبقيان كما هما في الغالب. والبيانات مولدة بأرقام عشوائية، فالسجلات الحقيقية تضغط عادة أفضل منها.

### تجهيز ال Container والبيانات

نشغل Container من Python 3.13 بإصدار مثبت، ونعطيه مجلداً في الذاكرة `/bench` حتى لا تؤثر سرعة القرص على القياس، ثم نثبت الأدوات من مستودع Debian 13:

```bash
docker run -d --name lab-compress-tools --tmpfs /bench:size=2g python:3.13.16-slim sleep infinity
docker exec lab-compress-tools bash -c "apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
  gzip bzip2 xz-utils zstd brotli lz4 zip unzip 7zip libarchive-tools xxd openssl curl"
docker exec lab-compress-tools bash -c "gzip --version|head -1; xz --version|head -1; zstd --version; brotli --version; lz4 --version"
```

والمخرج سوف يكون كما يلي:

```text
gzip 1.13
xz (XZ Utils) 5.8.1
*** Zstandard CLI (64-bit) v1.5.7, by Yann Collet ***
brotli 1.1.0
*** lz4 v1.10.0 64-bit multithread, by Yann Collet ***
```

والبرنامج التالي يولد ملفين نصيين كل منهما 20 MB، ويبدأ المولد العشوائي بنفس الرقم في كل مرة حتى تحصل على نفس البايتات إذا أعدت التجربة:

```python
# gen.py: generates the two text datasets of the benchmark (seeded, so every run gives the same bytes)
import random, json, datetime
random.seed(2026)
paths = ['/', '/login', '/api/v1/orders', '/api/v1/orders/{}', '/api/v1/users/{}', '/static/app.{}.js',
         '/static/style.{}.css', '/images/p/{}.webp', '/health', '/search?q={}']
agents = ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0 Safari/537.36',
          'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1',
          'Mozilla/5.0 (X11; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0', 'curl/8.14.1', 'kube-probe/1.34']
t = datetime.datetime(2026, 10, 1, tzinfo=datetime.timezone.utc)
with open('logs.txt', 'w') as f:
    while f.tell() < 20_000_000:
        t += datetime.timedelta(milliseconds=random.randint(1, 400))
        ip = f'10.{random.randint(0,3)}.{random.randint(0,255)}.{random.randint(1,254)}'
        p = random.choice(paths).format(random.randint(1, 99999))
        st = random.choices([200, 304, 404, 500], [90, 6, 3, 1])[0]
        f.write(f'{ip} - - [{t:%d/%b/%Y:%H:%M:%S} +0000] "GET {p} HTTP/1.1" {st} {random.randint(200, 90000)} "-" "{random.choice(agents)}" rt={random.random():.3f}\n')
cities = ['Riyadh', 'Jeddah', 'Cairo', 'Amman', 'Dubai', 'Rabat', 'Doha', 'Muscat']
recs = []
size = 0
i = 0
while size < 20_000_000:
    i += 1
    r = {'id': i, 'customer': {'name': f'customer-{random.randint(1, 50000)}', 'city': random.choice(cities),
         'email': f'user{random.randint(1, 50000)}@example.com'}, 'status': random.choice(['paid', 'shipped', 'cancelled', 'refunded']),
         'total': round(random.uniform(5, 2500), 2), 'currency': 'SAR', 'created_at': f'2026-{random.randint(1,9):02d}-{random.randint(1,28):02d}T{random.randint(0,23):02d}:{random.randint(0,59):02d}:00Z',
         'items': [{'sku': f'SKU-{random.randint(1000, 9999)}', 'qty': random.randint(1, 5), 'price': round(random.uniform(1, 500), 2)} for _ in range(random.randint(1, 4))]}
    s = json.dumps(r, ensure_ascii=False)
    recs.append(s); size += len(s) + 2
with open('dump.json', 'w') as f:
    f.write('[\n' + ',\n'.join(recs) + '\n]\n')
```

أما مجموعة الصور فهي 24 جزءاً بمقاس 1600 × 1067 قصصناها من صورة NASA التي استخدمناها في [تجربة مقال صيغ الصور](https://arabroot.io/articles/%D8%B5%D9%8A%D8%BA-%D8%A7%D9%84%D8%B5%D9%88%D8%B1-%D9%88%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%A8%D9%8A%D9%86%D9%87%D8%A7/#lab-setup)، وحفظناها JPEG بالجودة 85 في ملف tar واحد، ثم نسخنا كل شيء إلى ال Container:

```bash
docker exec lab-img-tools bash -c "cd /lab && mkdir photos && for i in \$(seq 0 23); do x=\$(( (i % 4) * 900 )); y=\$(( (i / 4) * 1000 )); \
  vips crop nasa-orig.jpg photos/p\$i.jpg[Q=85,strip] \$x \$y 1600 1067; done && tar -cf photos.tar photos"
docker cp lab-img-tools:/lab/photos.tar photos.tar
docker exec lab-compress-tools mkdir -p /data
docker cp gen.py lab-compress-tools:/data/gen.py
docker cp photos.tar lab-compress-tools:/data/photos.tar
docker exec -w /bench lab-compress-tools bash -c "cp /data/* . && python3 gen.py && ls -l"
```

```text
total 45040
-rw-r--r-- 1 root root 20000156 Oct  9 13:39 dump.json
-rwxr-xr-x 1 root root     2252 Oct  9 13:39 gen.py
-rw-r--r-- 1 root root 20000233 Oct  9 13:39 logs.txt
-rwxr-xr-x 1 root root  6113280 Oct  9 13:39 photos.tar
```

ولاحظ أننا نسخنا الملفات إلى `/data` أولاً ثم إلى `/bench` من داخل ال Container، والسبب أن `docker cp` لا يكتب داخل مجلد tmpfs، فهو يكتب في طبقة ملفات ال Container وليس في المجلد المحمل في الذاكرة.

### القياس: ست أدوات على ثلاثة أنواع من البيانات

البرنامج التالي يشغل كل أداة ثلاث مرات ويأخذ أسرع مرة، للضغط ثم لفك الضغط، ويتأكد أن الملف بعد فك الضغط مطابق للأصل بايتاً ببايت:

```python
# bench.py: ratio, compression and decompression speed for each tool on each dataset (best of 3 runs, one thread)
import subprocess, time, os, sys
TOOLS = [  # name, compress command, decompress command (stdin -> stdout)
    ('gzip -6',    ['gzip', '-6', '-c'],          ['gzip', '-d', '-c']),
    ('gzip -9',    ['gzip', '-9', '-c'],          ['gzip', '-d', '-c']),
    ('bzip2 -9',   ['bzip2', '-9', '-c'],         ['bzip2', '-d', '-c']),
    ('xz -6',      ['xz', '-6', '-T1', '-c'],     ['xz', '-d', '-T1', '-c']),
    ('zstd -1',    ['zstd', '-1', '-T1', '-q', '-c'],  ['zstd', '-d', '-q', '-c']),
    ('zstd -3',    ['zstd', '-3', '-T1', '-q', '-c'],  ['zstd', '-d', '-q', '-c']),
    ('zstd -19',   ['zstd', '-19', '-T1', '-q', '-c'], ['zstd', '-d', '-q', '-c']),
    ('brotli -11', ['brotli', '-q', '11', '-c'],  ['brotli', '-d', '-c']),
    ('lz4 -1',     ['lz4', '-1', '-q', '-c'],     ['lz4', '-d', '-q', '-c']),
]
def run(cmd, src, dst):
    best = 1e9
    for _ in range(3):
        with open(src, 'rb') as i, open(dst, 'wb') as o:
            t = time.perf_counter(); subprocess.run(cmd, stdin=i, stdout=o, check=True); best = min(best, time.perf_counter() - t)
    return best
for ds in sys.argv[1:]:
    n = os.path.getsize(ds); MB = n / 1e6
    print(f'\n{ds}: {n:,} bytes')
    print(f'{"tool":<11} {"size":>11} {"ratio":>6} {"comp MB/s":>10} {"decomp MB/s":>12}')
    for name, c, d in TOOLS:
        tc = run(c, ds, '/bench/out.z'); z = os.path.getsize('/bench/out.z')
        td = run(d, '/bench/out.z', '/bench/back')
        assert open('/bench/back', 'rb').read() == open(ds, 'rb').read()
        print(f'{name:<11} {z:>11,} {n / z:>6.2f} {MB / tc:>10.1f} {MB / td:>12.1f}')
    os.remove('/bench/out.z'); os.remove('/bench/back')
```

```bash
docker cp bench.py lab-compress-tools:/data/bench.py
docker exec -w /bench lab-compress-tools bash -c "cp /data/bench.py . && python3 bench.py logs.txt dump.json photos.tar"
```

والمخرج سوف يكون كما يلي:

```text
logs.txt: 20,000,233 bytes
tool               size  ratio  comp MB/s  decomp MB/s
gzip -6       2,319,711   8.62       31.5        159.6
gzip -9       2,191,675   9.13       15.7        129.3
bzip2 -9      1,443,200  13.86        4.0          7.2
xz -6         1,795,892  11.14        1.1         69.0
zstd -1       2,582,598   7.74      125.9        227.8
zstd -3       2,542,730   7.87       91.9        229.4
zstd -19      1,810,383  11.05        0.6        191.6
brotli -11    1,780,248  11.23        0.3        234.4
lz4 -1        4,024,072   4.97      483.9        601.1

dump.json: 20,000,156 bytes
tool               size  ratio  comp MB/s  decomp MB/s
gzip -6       2,819,390   7.09       45.1        139.9
gzip -9       2,698,387   7.41        8.8        198.4
bzip2 -9      1,680,342  11.90        3.9         13.0
xz -6         2,030,564   9.85        0.8        109.7
zstd -1       2,876,011   6.95      220.0        493.4
zstd -3       2,965,561   6.74      122.1        351.5
zstd -19      2,134,797   9.37        0.5        237.7
brotli -11    2,079,243   9.62        0.2        101.3
lz4 -1        5,008,197   3.99      409.5        385.2

photos.tar: 6,113,280 bytes
tool               size  ratio  comp MB/s  decomp MB/s
gzip -6       5,994,861   1.02       14.5         82.7
gzip -9       5,994,790   1.02       14.6         70.3
bzip2 -9      5,757,521   1.06        3.8          3.6
xz -6         5,915,220   1.03        1.2         17.8
zstd -1       6,020,316   1.02       69.5        292.0
zstd -3       6,007,324   1.02       61.6        239.3
zstd -19      5,933,685   1.03        1.6        157.8
brotli -11    5,905,833   1.04        0.2         58.5
lz4 -1        6,112,819   1.00      411.1        351.6
```

والصورة التالية ترسم نتائج السجلات، حيث الأعلى ملف أصغر والأيمن ضغط أسرع:

![رسم نقاط لنسبة الضغط مقابل سرعة الضغط بمقياس لوغاريتمي على 20 MB من السجلات: bzip2 في الأعلى بنسبة 13.9 وسرعة 4 MB/s، وbrotli وzstd -19 وxz حول 11 بسرعات أقل من 1.1 MB/s، وgzip حول 9 بسرعة 16 إلى 32 MB/s، وzstd -1 و-3 حول 7.8 بسرعة 92 إلى 126 MB/s، وlz4 في الأسفل يمين بنسبة 5 وسرعة 484 MB/s](https://arabroot.io/content/images/2026/10/compression-basics-04-speed-ratio.webp)

كلما اتجهت إلى اليمين خسرت من نسبة الضغط، ولا توجد أداة في الزاوية العليا اليمنى

في المخرج أعلاه لاحظ التالي:

- **المقايضة واضحة:** LZ4 يضغط السجلات بسرعة 484 MB/s ولكن بنسبة 5 فقط، وBrotli بالمستوى 11 يصل إلى 11.2 ولكن بسرعة 0.3 MB/s، أي أبطأ بـ 1600 مرة تقريباً، فلا يصلح إلا لما يضغط مرة واحدة.
- **zstd -1 أفضل من gzip -6 للعمل اليومي:** نسبة أقل قليلاً، 7.7 مقابل 8.6، مقابل ضغط أسرع بأربع مرات وفك أسرع بمرة ونصف تقريباً، ولهذا السبب أصبح الخيار المعتاد لتدوير السجلات والنسخ المستمرة.
- **فك الضغط سريع في كل المستويات:** zstd -19 ضغط ببطء شديد ولكنه فك بسرعة 192 MB/s، قريباً من zstd -1، وهذا ما قلناه عن LZ77: البحث مكلف والنسخ رخيص. أما bzip2 فبطيء في الاتجاهين، 7.2 MB/s لفك السجلات.
- **bzip2 فاز في النسبة على بياناتنا:** 13.9 مقابل 11.1 لـ xz، وهذا ليس قاعدة، فبياناتنا مولدة بأرقام عشوائية كثيرة مثل عناوين IP والأحجام وأرقام الطلبات، وطريقة Burrows-Wheeler تتعامل مع هذا النوع جيداً، بينما في ملفات حقيقية فيها تكرار أطول يتقدم عادة xz وzstd -19\. ولهذا السبب قس على بياناتك أنت قبل أن تقرر.
- **zstd -3 كان أكبر من zstd -1 على ملف JSON**، 2.97 MB مقابل 2.88 MB، أي أن المستوى الأعلى لا يضمن ملفاً أصغر في كل الحالات، وإنما في المتوسط.
- **الصور لم تصغر:** أفضل نتيجة 6% مع bzip2، وأغلب الأدوات بين 0% و4%، وLZ4 خرج بنفس الحجم تقريباً، فصور JPEG مضغوطة أصلاً، وضغطها مرة أخرى يستهلك الوقت دون فائدة.

### البيانات المضغوطة والمشفرة والعشوائية

الآن نرى بالأرقام ما ذكرناه نظرياً: نأخذ أول 5 MB من السجلات، ونشفرها بـ AES-256، ونولد 5 MB عشوائية، ثم نجرب الضغط مرتين، والتشفير قبل الضغط وبعده:

```bash
cd /bench
head -c 5000000 logs.txt > sample.log
openssl enc -aes-256-ctr -pbkdf2 -pass pass:lab-only -in sample.log -out sample.log.enc
head -c 5000000 /dev/urandom > random.bin
gzip -9 -k -c sample.log > sample.log.gz
gzip -9 -c sample.log.gz > sample.log.gz.gz
zstd -q -19 -c sample.log.enc > sample.log.enc.zst
gzip -9 -c random.bin > random.bin.gz
gzip -9 -c sample.log.enc > sample.log.enc.gz
zstd -q -19 -c sample.log > sample.log.zst
openssl enc -aes-256-ctr -pbkdf2 -pass pass:lab-only -in sample.log.zst -out sample.log.zst.enc
ls -l sample.log sample.log.gz sample.log.gz.gz sample.log.enc sample.log.enc.gz sample.log.enc.zst random.bin random.bin.gz sample.log.zst sample.log.zst.enc | awk '{printf "%12s  %s\n", $5, $9}'
```

والمخرج سوف يكون كما يلي:

```text
     5000000  random.bin
     5000794  random.bin.gz
     5000000  sample.log
     5000016  sample.log.enc
     5000814  sample.log.enc.gz
     5000146  sample.log.enc.zst
      547602  sample.log.gz
      547719  sample.log.gz.gz
      461723  sample.log.zst
      461739  sample.log.zst.enc
```

في المخرج أعلاه لاحظ التالي:

- البيانات العشوائية كبرت بعد gzip بـ 794 بايت، والسبب أن الضاغط لا يجد شيئاً يحذفه، فيخزنها كما هي ويضيف رؤوس الكتل والغلاف.
- السجلات المشفرة تصرفت تماماً مثل العشوائية، وكبرت مع gzip وzstd، وهذا يعني أن التشفير يعمل كما يجب، فلو صغرت لكان ذلك علامة على أن التشفير يترك أنماطاً.
- ضغط ملف gzip مرة ثانية زاده 117 بايتاً، فلا يوجد ضغط فوق الضغط.
- الترتيب الصحيح، الضغط ثم التشفير، أعطى 461,739 بايت، أي 9.2% من الأصل، والتشفير أضاف 16 بايتاً فقط، وهي الملح Salt الذي يضيفه `openssl enc` مع علامته.

وأخيراً حالة معاكسة تماماً، 10 MB من الأصفار، وهي أقصى تكرار ممكن:

```bash
head -c 10000000 /dev/zero > zeros.bin
for t in "gzip -9" "zstd -q -19" "xz -6 -T1" "bzip2 -9"; do printf "%-11s %s\n" "$t" $($t -c zeros.bin | wc -c); done
```

```text
gzip -9     9747
zstd -q -19 330
xz -6 -T1   1584
bzip2 -9    49
```

لاحظ أن gzip توقف عند نحو 1:1000، والسبب في مواصفة DEFLATE نفسها، فأطول تطابق في LZ77 هناك 258 بايتاً، فيحتاج 10 MB من الأصفار إلى نحو 39 ألف إشارة على الأقل. أما bzip2 فضغطها إلى 49 بايتاً فقط، لأن أول مرحلة فيه هي ترميز التكرار المتتالي RLE الذي شرحناه، وهي مصممة بالضبط لهذه الحالة. وهذه الأرقام هي أساس فكرة «القنبلة المضغوطة» Zip Bomb التي نشرحها في [مقال ملفات ZIP](https://arabroot.io/articles/%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D9%88%D8%AC%D8%AF-%D8%AF%D8%A7%D8%AE%D9%84-%D9%85%D9%84%D9%81-zip/).

### برنامج Huffman صغير بلغة Python

البرنامج التالي يحسب الإنتروبيا، ويبني شجرة Huffman بنفس الخطوات التي فعلناها باليد، باستخدام طابور أولوية Priority Queue من الوحدة `heapq`، وقد شرحنا هذا الهيكل في [مقال هياكل البيانات](https://arabroot.io/articles/%D9%87%D9%8A%D8%A7%D9%83%D9%84-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#heap):

```python
# huffman.py: entropy and a tiny Huffman encoder, to see how many bits the code saves
import heapq, math, sys
from collections import Counter

def entropy(text):
    n = len(text)
    return -sum(c / n * math.log2(c / n) for c in Counter(text).values())

def huffman(text):
    # each heap entry: (frequency, tie-breaker, {symbol: code so far})
    heap = [(f, i, {s: ''}) for i, (s, f) in enumerate(sorted(Counter(text).items(), key=lambda x: (-x[1], x[0])))]
    heapq.heapify(heap)
    tie = len(heap)
    while len(heap) > 1:
        f1, _, a = heapq.heappop(heap)          # the two least frequent groups
        f2, _, b = heapq.heappop(heap)
        merged = {s: '0' + c for s, c in a.items()} | {s: '1' + c for s, c in b.items()}
        heapq.heappush(heap, (f1 + f2, tie, merged)); tie += 1
    return heap[0][2]

for text in sys.argv[1:]:
    codes = huffman(text)
    bits = ''.join(codes[ch] for ch in text)
    utf8 = len(text.encode('utf-8')) * 8
    print(f'text: {text!r}  ({len(text)} characters, {utf8} bits in UTF-8)')
    for s, c in sorted(codes.items(), key=lambda x: (len(x[1]), x[1])):
        print(f'  {s!r:6} x{text.count(s):<3} -> {c}')
    print(f'  encoded: {bits}')
    print(f'  Huffman: {len(bits)} bits = {len(bits) / len(text):.2f} bits per character')
    print(f'  entropy: {entropy(text):.3f} bits per character, minimum {math.ceil(entropy(text) * len(text))} bits')
    print(f'  saving vs UTF-8: {100 * (1 - len(bits) / utf8):.1f}% (the code table itself is not counted)\n')
```

```bash
docker cp huffman.py lab-compress-tools:/data/huffman.py
docker exec -w /bench lab-compress-tools bash -c 'cp /data/huffman.py . && python3 huffman.py ABRACADABRA "سلسلة سلاسل"'
```

والمخرج سوف يكون كما يلي:

```text
text: 'ABRACADABRA'  (11 characters, 88 bits in UTF-8)
  'A'    x5   -> 0
  'C'    x1   -> 100
  'D'    x1   -> 101
  'B'    x2   -> 110
  'R'    x2   -> 111
  encoded: 01101110100010101101110
  Huffman: 23 bits = 2.09 bits per character
  entropy: 2.040 bits per character, minimum 23 bits
  saving vs UTF-8: 73.9% (the code table itself is not counted)

text: 'سلسلة سلاسل'  (11 characters, 168 bits in UTF-8)
  'ل'    x4   -> 0
  'س'    x4   -> 11
  'ة'    x1   -> 100
  ' '    x1   -> 1010
  'ا'    x1   -> 1011
  encoded: 11011010010101101011110
  Huffman: 23 bits = 2.09 bits per character
  entropy: 2.005 bits per character, minimum 23 bits
  saving vs UTF-8: 86.3% (the code table itself is not counted)
```

في المخرج أعلاه لاحظ التالي:

- الرموز التي طبعها البرنامج للكلمة الإنجليزية هي نفسها التي حسبناها باليد، والنتيجة 23 بتاً تساوي الحد الأدنى الذي تعطيه الإنتروبيا مقرباً.
- العبارة العربية فيها 11 حرفاً أيضاً، ولكنها في UTF-8 تأخذ 168 بتاً لأن كل حرف عربي بايتان، كما شرحنا في [مقال الترميز](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#charsets)، ولذلك كان التوفير أكبر، 86% مقابل 74%.
- الحرفان «ل» و«س» لهما نفس التكرار، ومع ذلك أخذ أحدهما بتاً واحداً والآخر بتين، وهذا طبيعي لأن الشجرة تبنى من أقل تكرارين في كل خطوة، والطول الكلي هو الأقل الممكن مهما اختلف توزيع الرموز.
- التوفير لا يحسب حجم جدول الرموز، وفي الملفات الحقيقية يحفظ DEFLATE الجدول بطريقة مختصرة في رأس كل كتلة، ولهذا يظهر أثر Huffman في الملفات الكبيرة وليس في كلمة واحدة.

### حذف التجربة

```bash
docker rm -f lab-compress-tools
```

## أي أداة تستخدم ومتى؟

| الحالة                                    | الاختيار                                   | والسبب                                 |
| ----------------------------------------- | ------------------------------------------ | -------------------------------------- |
| تدوير السجلات ونقل البيانات بين السيرفرات | zstd بالمستوى 1 إلى 3                      | أسرع من gzip بكثير بنسبة قريبة منه     |
| النسخ الاحتياطية                          | zstd بمستوى متوسط، أو ما تختاره أداة النسخ | ضغط سريع، وفك سريع وقت الاستعادة       |
| ملف ترسله لشخص لا تعرف ما عنده            | gzip أو ZIP                                | موجود في كل نظام دون تثبيت             |
| حزمة تضغط مرة وتنزل ملايين المرات         | xz أو zstd -19                             | الوقت يدفع مرة، والتوفير يتكرر         |
| ملفات الموقع الثابتة                      | Brotli بالمستوى 11 مسبقاً، وgzip كبديل     | أصغر ملف للزائر، والضغط مرة عند البناء |
| صفحات تتولد عند كل طلب                    | Brotli أو zstd بمستوى منخفض، أو gzip       | الضغط يجب ألا يؤخر الرد                |
| ضغط في الذاكرة أو في قاعدة البيانات       | LZ4 أو zstd -1                             | المعالج أغلى من الحجم هنا              |
| صور وفيديو وملفات مشفرة                   | لا تضغطها                                  | لن تصغر، وقد تكبر                      |

## أخطاء شائعة

1. **ضغط الملفات المشفرة:** لن تصغر، فاضغط قبل التشفير دائماً.
2. **ضغط الصور والفيديو مرة أخرى:** وقت معالج ضائع، فاستثنها من الضغط في ال Reverse Proxy ومن ضغط النسخ الاحتياطية إذا كانت أداتك تسمح.
3. **أعلى مستوى دائماً:** zstd -19 أبطأ من zstd -1 بأكثر من 200 مرة في تجربتنا مقابل ملف أصغر بنحو 30%، وهذا يستحق لحزمة توزعها، ولا يستحق لسجلات تدورها كل ساعة.
4. **الاعتماد على أرقام غيرك:** رأيت كيف فاز bzip2 على بياناتنا المولدة، فقس على عينة من بياناتك أنت قبل أن تقرر.
5. **Brotli بالمستوى 11 عند كل طلب:** 0.3 MB/s في تجربتنا، فاستخدمه للملفات الجاهزة مسبقاً فقط.
6. **أسرار في صفحة مضغوطة تعكس مدخلات المستخدم:** هذا هو شرط هجوم BREACH، فافصل الأسرار أو اجعلها تتغير في كل رد.

## تدريب عملي

- شغل `bench.py` على نسخة من سجلات سيرفرك الحقيقية، وقارن ترتيب الأدوات بما رأيته هنا. هل بقي bzip2 الأول؟
- غير `huffman.py` حتى يحسب حجم جدول الرموز ويضيفه إلى الناتج، ثم جربه على جملة قصيرة وعلى فقرة كاملة، وابحث عن الطول الذي يبدأ عنده التوفير الحقيقي.
- افحص موقعك بأمر `curl` الذي في قسم HTTP، وانظر هل يرسل Brotli أو zstd، وهل يضغط الصور دون فائدة.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الضغط يحذف التكرار، والإنتروبيا التي عرفها Shannon في 1948 هي الحد الأدنى لعدد البتات لكل رمز، وكلمة ABRACADABRA تحتاج 2.04 بت لكل حرف بدل 8.
- Huffman يعطي الرمز المتكرر كوداً قصيراً، وLZ77 يستبدل النص المكرر بإشارة (مسافة، طول)، وDEFLATE يجمعهما، وهو داخل gzip وzlib وPNG وZIP.
- الأدوات الحديثة مقايضة بين النسبة وسرعة الضغط وسرعة الفك: LZ4 للسرعة، وzstd لكل المدى، وxz وBrotli للحجم الأصغر عندما يضغط الملف مرة واحدة.
- البيانات المضغوطة والمشفرة والعشوائية لا تضغط، فاضغط ثم شفر، ولا تضغط الصور والفيديو.
- في HTTP يختار السيرفر من Accept-Encoding، وzstd مدعوم اليوم في Chrome وEdge وFirefox، وBrotli في كل المتصفحات، وBREACH يذكرك بأن طول الرد المضغوط يكشف معلومات.

والمقال التالي في هذه المرحلة يفتح ملفات الأرشيف نفسها: كيف يبنى ZIP بالبايت، ولماذا يختلف عنه 7z وRAR، وما الخطر في فك ملف مضغوط من مستخدم:

[![](https://arabroot.io/content/images/2026/10/zip-rar-inside-cover-cover.webp)دليلماذا يوجد داخل ملف ZIP، وما الذي يخفيه؟كيف يرتب ملف ZIP محتوياته من الداخل، ولماذا يستطيع ملف مضغوط صغير أن يملأ قرصك أو يكتب خارج المجلد الذي فككته فيه.](https://arabroot.io/articles/%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D9%88%D8%AC%D8%AF-%D8%AF%D8%A7%D8%AE%D9%84-%D9%85%D9%84%D9%81-zip/)[![](https://arabroot.io/content/images/2026/10/image-formats-cover-cover.webp)دليلصيغ الصور: كيف تختار الصيغة المناسبة لموقعككيف تحفظ كل صيغة صور البكسلات والألوان، ولماذا يصغر الملف نفسه إلى الثلث أو أقل عندما تختار الصيغة والمقاس المناسبين.](https://arabroot.io/articles/%D8%B5%D9%8A%D8%BA-%D8%A7%D9%84%D8%B5%D9%88%D8%B1-%D9%88%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D8%A8%D9%8A%D9%86%D9%87%D8%A7/)[![](https://arabroot.io/content/images/2026/10/encodings-cover-cover.webp)دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/)[![](https://arabroot.io/content/images/2026/10/dirty-pipe-investigation-cover-cover.webp)دليلكيف اكتشف Max Kellermann ثغرة Dirty Pipeكيف قادت ملفات gzip تالفة مهندساً إلى ثغرة في نواة Linux تسمح بالكتابة في ملفات للقراءة فقط، وكيف تتبعها بأدواته.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%A7%D9%83%D8%AA%D8%B4%D9%81-max-kellermann-%D8%AB%D8%BA%D8%B1%D8%A9-dirty-pipe/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على gzip 1.13 وbzip2 1.0.8 وXZ Utils 5.8.1 وzstd 1.5.7 وbrotli 1.1.0 وlz4 1.10.0 وPython 3.13.16 في Debian 13.7، على معالج Intel Core i7-7700K.