Gzip
الصق gzip أو zlib أو deflate خام مكتوبًا بترميز Base64 لتقرأ ما يحويه — والبايتات تحدد أيّها هو — أو اكتب نصًا لتضغطه بأيٍّ من الثلاثة، داخل متصفحك.
[
{
"name": "مريم",
"city": "القاهرة"
},
{
"name": "أحمد",
"city": "الرياض"
},
{
"name": "ليلى",
"city": "عمان"
},
{
"name": "يوسف",
"city": "الدار البيضاء"
},
{
"name": "سارة",
"city": "بيروت"
},
{
"name": "عمر",
"city": "دبي"
},
{
"name": "نور",
"city": "تونس"
},
{
"name": "خالد",
"city": "الكويت"
}
]قُرئ على أنه gzip.
- بايتات غير مضغوطة
- 477
- بايتات مضغوطة
- 189
- التغيّر
- -60.4%
- محارف Base64
- 252
ضغط واحد في ثلاثة أغلفة، وكلمة deflate
إنّ gzip وzlib وdeflate الخام ضغطٌ واحد، هو DEFLATE، مغلَّفٌ بثلاث طرق. يعرّف RFC 1951 ضغطَ DEFLATE نفسه: تُقطَّع البايتات إلى كتل، وتُكتب كل كتلة رموزًا يمثّل كلٌّ منها بايتًا، أو سلسلةً منسوخة من موضع سابق. ويضع غلاف gzip، المعرَّف في RFC 1952، في المقدمة ترويسةً لا تقل عن عشرة بايتات، وفي المؤخرة ثمانية بايتات: CRC-32 للبايتات الأصلية، وطولها؛ ويضع غلاف zlib، المعرَّف في RFC 1950، بايتين في المقدمة وAdler-32 في المؤخرة؛ أما deflate الخام فهو الضغط بلا غلاف على الإطلاق. وقد تكون البيانات المضغوطة في الداخل هي نفسها في الثلاثة، فالغلاف هو الفرق كله.
وعند كلمة deflate تتشابك الأسماء. ففي HTTP، يعني Content-Encoding: deflate غلافَ zlib، ويذكر RFC 9110 أن بعض الخوادم ترسل deflate الخام تحت هذا الاسم مع ذلك. وضاغط المتصفح نفسه يتبع HTTP: فهو يسمّي غلاف zlib باسم deflate، ويسمّي deflate الخام باسم deflate-raw. ويكتب deflate الخام كلٌّ من DeflateStream في .NET وgzdeflate في PHP، بينما يكتب غلافَ zlib كلٌّ من gzcompress في PHP وzlib.compress في Python. لذا قد تكون البايتات الموسومة بـ deflate أيًّا من الاثنين، ولا تستطيع أن تقول أيهما إلا البايتات نفسها.
لهذا تقرأ الصفحة الغلاف من البايتات عند فك الضغط، وتقول تحت ما خرج أيَّ غلاف قرأت: «قُرئ على أنه gzip» أو «قُرئ على أنه zlib» أو «قُرئ على أنه deflate خام». فالبايتات تحسم الأمر: يبدأ gzip دائمًا بـ 1f 8b، وأول بايتين في zlib، إذا قُرئا معًا عددًا واحدًا، كانا من مضاعفات 31، ولا يبدأ أي مرمِّز deflate الخام بأيٍّ من هاتين البدايتين. والملف الذي ينتهي اسمه بـ .gz ويحمل zlib يُقرأ على أنه zlib، مع تنبيه بأن الاسم والبايتات لا يتفقان، أما البايتات التي ليست أيًّا من الثلاثة فتُرفض، إذ لا يوجد فيها ما يُفك ضغطه. وعند الضغط تختار أنت الغلاف، وهو gzip ما لم تختر غيره.
من أين تأتي الحمولات، وما معنى H4sI وeJ
تصل البايتات المضغوطة إلى المطوّر في بضعة أشكال: جسمًا لاستجابة HTTP يذكر Content-Encoding فيها القيمةَ gzip أو deflate، أو ملفًا، أو نصًا مكتوبًا بـ Base64 أو بالست عشري. ويبدأ كل تدفق gzip بالبايتات الثلاثة نفسها، 1f 8b 08 — أي بايتا التوقيع، يليهما رقم طريقة الضغط فيه، وهي DEFLATE — والبايتات الثلاثة هي أربعة محارف من Base64 بالضبط، لذا يبدأ gzip المكتوب بـ Base64 دائمًا بـ H4sI. وهذا ما يسلّمه اشتراك CloudWatch Logs إلى دالّة Lambda أو إلى تدفق Kinesis في بيانات كل سجل، وهكذا يخزّن Helm الإصدار: بيانات JSON الخاصة به مضغوطةً بـ gzip، ثم مكتوبةً بـ Base64. وحين يحفظ Helm إصدارًا في Secret من Kubernetes، فإن قراءته مباشرةً من الـ Secret تعطي Base64 داخل Base64، لأن الـ Secret يحفظ بياناته بـ Base64 خاص به؛ وتفك أداة Base64 ترميز تلك الطبقة الخارجية فتعطي H4sI… الذي تقرؤه هذه الصفحة.
يحدّد بايتا الترويسة في zlib طريقةَ ضغطه، ونافذتَه، والمستوى الذي كُتب به، لذا يبدأ Base64 الخاص به، مع نافذة zlib الافتراضية، بإحدى أربع طرق: eJ عند المستوى الافتراضي، وهو ما تكتبه zlib.compress في Python ما لم يُطلب منها غير ذلك، وeA أو eF عند المستويات التي دونه، وeN عند التي فوقه. أما deflate الخام فلا توقيع له على الإطلاق، ويبدأ Base64 الخاص به كيفما اتفق أن تبدأ كتلته الأولى.
يحدّد «المضغوط على هيئة» كيف تقرأ الصفحة تلك البايتات وتكتبها نصًا: «Base64»، كما تُفتح الصفحة، أو «ست عشري». ويسري هذا الاختيار في الاتجاهين كليهما، ولا تغيّره الصفحة نيابةً عنك أبدًا، لأن كل رقم ست عشري هو محرف من Base64 أيضًا، فـ 1f8b0800 نص في كليهما، وبايتات مختلفة في كل منهما. ويُقرأ Base64 بالأبجدية القياسية أو بالأبجدية الآمنة لعناوين URL، بحشوه أو دونه، وعبر المسافات وفواصل الأسطر، ويُكتب بالحشو، أو بالأبجدية الآمنة لعناوين URL ودون حشو حين يكون خيار «آمن لعناوين URL» مفعّلًا. ويُكتب الست عشري أزواجًا تفصل بينها مسافات، ويُقرأ مفصولًا بمسافات أو متصلًا، أو مع 0x قبله، أو على هيئة تفريغ — و0x هو ما يكتب به T-SQL القيمة الثنائية، مثل gzip الذي تعيده COMPRESS() في SQL Server. وحين يفشل الست عشري المقروء على أنه Base64، أو يُرفض Base64 المقروء على أنه ست عشري عند محرف لا يوجد في الست عشري، ويُقرأ النص كله في الحالتين بالطريقة الأخرى، يقول ذلك تنبيه بجانب زر يبدّل؛ ولا يتبدّل شيء حتى تضغطه.
قراءة تدفق: الأعضاء، والبايتات التي تليه، والنهاية المبكرة
يجعل RFC 1952 ملف gzip سلسلةً من الأعضاء، أي من تدفقات gzip كاملة متتالية، لكل منها ترويستها وذيلها. والأمر cat a.gz b.gz يصنع ملفًا كهذا، وكذلك الإضافة إلى ملف قائم بالأمر gzip -c file >> archive.gz، وهو المثال الذي يضربه دليل GNU gzip نفسه؛ ويقرأ gunzip كل عضو ويصل ما تحمله الأعضاء. وهذه الصفحة تقرؤها بالطريقة نفسها: يُقرأ كل عضو ويُفحص، ويُعرض ما تحمله الأعضاء موصولًا، ويقول تنبيه كم عضوًا قُرئ. وليس كل قارئ لـ gzip يفعل ذلك. فالمعيار الذي يتبعه المتصفح نفسه في فك الضغط يسمح لتدفق gzip بعضو واحد، ويعدّ العضو الثاني خطأً، وتتوقف zlib.decompress في Python بعد العضو الأول دون أن تقول شيئًا.
أما البايتات التي تأتي بعد النهاية ولا تبدأ عضوًا فتُتخطّى بدل أن تُرفض، لأن كل ما قبلها كامل ومفحوص، ويقول تنبيه كم عددها، والإزاحة التي تبدأ عندها، وهل كلها أصفار. والأصفار هناك حشو — يصادفها دليل GNU gzip على الأشرطة المغناطيسية، مكتوبةً حتى نهاية كتلة — ويتخطاها gunzip بصمت؛ أما البايتات الأخرى فيتخطاها مع تحذير بأنه تجاهل بيانات زائدة في الآخر، بينما ترفض gzip.decompress في Python الملف. ويصح الأمر نفسه بعد Adler-32 في تدفق zlib، وبعد الكتلة الأخيرة في تدفق deflate خام، وإن كان deflate الخام لا يحمل مجموع تحقّق يُفحص.
أما التدفق الذي ينتهي قبل نهايته، كنص ملصوق قُطع، أو تنزيلٍ توقّف في منتصفه، فيُعرض بقدر ما يصل، مع تنبيه بأن مجموع التحقّق لم يُفحص. والتدفق التالف يُرفض، ولا يُعرض منه شيء. وهذا أشد من gunzip، الذي يكتب ما فك ضغطه قبل أن يبلغ مجموع التحقّق الذي يكشف أنه خاطئ؛ لكن البت المتغيّر قد يُفك بصمت مسافةً ما قبل أن يكشفه شيء، فما خرج قبل أن يظهر التلف قد يكون خاطئًا بالفعل. ولا يُعطى موضع أيضًا، لأن الموضع الذي يلاحظ فيه المفكِّك التلف ليس موضع التلف نفسه. أما تدفق zlib الذي يطلب قاموسًا مُعدًّا مسبقًا فيُرفض بجملة خاصة به: فالتدفق يحدّد هوية البايتات التي هُيّئ بها ضاغطه مسبقًا، لكنه لا يحملها، فلا يوجد ما يُقرأ به.
ويُكتب ما يخرج كما يحدّد «البايتات على هيئة»: «نص» بترميز UTF-8، أو «ست عشري». وكثيرًا ما يكون JSON، الذي يرتّبه منسّق JSON ويتحقق منه، مشيرًا إلى السطر والعمود حيث ينكسر. أما الصورة أو الأرشيف المضغوطان بـ gzip فليسا نصًا، لذا مع «نص» يظهر كل تسلسل من بايتاتهما ليس UTF-8 على هيئة U+FFFD، مع تنبيه يعدّ تلك التسلسلات بجانب زر يعرض البايتات بالست عشري؛ ويحفظ الزر «تنزيل» البايتات نفسها في الحالتين. ويعرض سطرٌ تحت النتيجة ما تخزّنه ترويسة العضو الأول: اسمًا، ووقتًا بتوقيت UTC، وتعليقًا، والاسم المخزَّن لا يسمّي أبدًا الملف الذي يحفظه الزر «تنزيل». والناتج الذي يتجاوز أقصى ما تفك الصفحة ضغطه دفعة واحدة يتوقف عنده، مع تنبيه يذكر ذلك الحجم، وحيث يذكر ذيل gzip طولًا يتجاوزه، تقول الصفحة ذلك والعمل جارٍ.
لماذا يكبر الإدخال الصغير، وما الذي يضيفه Base64
يربح الضغط بإيجاد التكرار، والنص القصير قليل التكرار، بينما يكلّف كل غلاف بايتات من عنده: تبلغ ترويسة gzip وذيله معًا 18 بايتًا حين لا تخزّن الترويسة شيئًا آخر، وتبلغ ترويسة zlib وذيله 6 بايتات، ولا شيء منهما لـ deflate الخام، وإن كان DEFLATE ينفق بضعة بتات على تحديد كتله. وهكذا فإن Hello, world!، وهو 13 بايتًا، يصير 33 بايتًا بـ gzip، و21 بـ zlib، و15 بـ deflate الخام. ويعدّ سطر الأحجام تحت النتيجة البايتات في كل جانب والتغيّر بينهما، +153.8% لذلك الـ gzip، ويقول تنبيه السبب حين تكبر النتيجة. أما النص الكثير التكرار، مثل JSON أو سجل، فيستردّ تلك الكلفة أضعافًا كثيرة: فالمثال الذي تُفتح عليه الصفحة، وهو JSON صغير، يخرج بأقل من نصف حجمه.
وحين تُكتب البايتات نصًا، تكلّف أكثر مرةً أخرى. فـ Base64 يأخذ أربعة محارف لكل ثلاثة بايتات، أي بزيادة الثلث، كما يشرح دليل أداة Base64، لذا فإن تلك البايتات الـ 33 من gzip هي 44 محرفًا؛ ويأخذ الست عشري رقمين لكل بايت، والصفحة تكتبها أزواجًا تفصل بينها مسافات، فتكون 98 محرفًا للبايتات الـ 33 نفسها. ويعدّ سطر الأحجام تلك المحارف أيضًا، ويحوّل محوّل أحجام البيانات أيًّا من أعداده إلى KB أو KiB.
الضغط: بلا مستوى، وليست بايتات gzip -c، وبلا Brotli
الضغط عمل المتصفح نفسه، عبر CompressionStream الذي يعرّفه معيار Compression، وذلك المعيار لا يقدّم مستوى للضغط، فلا تقدّمه هذه الصفحة أيضًا: ما تحصل عليه هو ما يفعله المتصفح افتراضيًا. قد يخرج gzip -9 أصغر قليلًا، ونادرًا ما يكون الفرق كبيرًا.
ولا تكون البايتات هي التي يكتبها gzip -c، وإن كان فك ضغط كليهما يعطي النص نفسه. فالترويستان تختلفان أولًا. يخزّن GNU gzip اسم الملف ووقته ما لم يُعطَ -n، ووقتًا صفريًا حين يقرأ من أنبوب؛ ويسجّل في أحد بايتات الترويسة ما إذا استُخدم -9 أو -1؛ وفي البايت العاشر، الذي يخصّصه RFC 1952 لنظام التشغيل، يكتب 03 على Linux، وهو رقم Unix. أما المتصفح فلا يتسلّم إلا البايتات، فلا يمكن أن يخرج مع النتيجة اسمُ ملفك ولا وقته: فـ Chromium لا يخزّن اسمًا، ويخزّن وقتًا صفريًا، وهو ما يختاره gzip -n، ويكتب في البايت العاشر 03 على Linux، بينما يكتب فيه Chrome على Windows القيمة 0a. ثم تختلف البيانات المضغوطة نفسها، لأن ضاغط GNU gzip ليس ضاغط المتصفح: ففي النص الطويل يختار الاثنان بايتات مختلفة عند المستوى نفسه. لذا فإن عدم التطابق مع gzip -c لا يعني شيئًا بحد ذاته؛ المهم أن فك ضغط كليهما يعطي البايتات نفسها.
لا يوجد Brotli هنا، في الوقت الحالي. فمعيار Compression يذكره، لكن ليس كل متصفح يكتبه بعد، والخيار الذي لا تستطيع الصفحة أن تقدّمه إلا حيث يتيحه المتصفح سيجعلها صفحةً مختلفة في كل متصفح. أما zstd فليس في ذلك المعيار أصلًا. وما تقرؤه هذه الصفحة وتكتبه هو DEFLATE بأغلفته الثلاثة، ولا شيء غيره.
ملف يدخل، وملف يخرج، ولا يُرفع شيء
يمكن للملف أن يأخذ مكان مربع النص في الاتجاهين: اختر ملفًا، أو أسقطه على المربع. وتُقرأ بايتاته كما هي، فلا يسري عليها «المضغوط على هيئة» ولا «البايتات على هيئة»، وليس للملف هنا حدٌّ كالذي لمربع النص، وإن كان الملف الكبير جدًا يستدعي تحذيرًا من أن المتصفح قد لا يتسع له. وعند فك الضغط، يسمّي الزر «تنزيل» النتيجة كما يسمّيها gunzip: اسم الملف دون .gz، مع تحويل .tgz إلى .tar، ودون .zz أو .deflate كذلك، وهما الامتدادان اللذان تكتبهما هذه الصفحة للغلافين الآخرين؛ أما أي اسم آخر، وكل ما يُلصق، فيخرج باسم bytes.bin. وعند الضغط، يضيف .gz أو .zz أو .deflate إلى اسم الملف — وبالامتداد .zz يسمّي pigz ملفات zlib — أو إلى الكلمة text لما كتبته.
ينقل الزر «استخدمه كإدخال» النتيجة إلى المربع ويعكس الاتجاه، فتكفي الجولة ذهابًا وإيابًا نقرةٌ واحدة، أما النتيجة التي جاءت من ملف، أو التي هي أكبر من أن يتسع لها المربع، فتنتقل ملفًا. وأيًّا كان الاتجاه، يُقرأ الملف في هذا التبويب، ويجري العمل في Web Worker تشغّله الصفحة على جهازك أنت: فلا تُرفع حمولة ولا ملف لإنجاز ذلك.
الأمر نفسه مع gunzip وPython
في سطر الأوامر، يقرأ gunzip وzcat ملفات gzip كما تقرؤها هذه الصفحة، بكل أعضائها، بعد أن يعيد base64 -d الحمولة إلى بايتات، لكن أيًّا منهما لا يقرأ zlib ولا deflate الخام، إذ يرفضهما كلاهما على أنهما ليسا gzip؛ ويضغط gzip -k ملفًا ويُبقي الأصل بجانبه. وفي Python، تقرأ gzip.decompress غلاف gzip وكل عضو فيه، وتقرأ zlib.decompress أيًّا من الأغلفة الثلاثة، وتعرف أيها من wbits الخاص بها:
# حمولة: فك ترميزها ثم فك ضغطها
printf 'H4sIAAAAAAAAA/NIzcnJ11Eozy/KSVEEAObG5usNAAAA' | base64 -d | gunzip # Hello, world!
# ملف: ضغطه مع إبقاء الأصل ثم طباعته من جديد
printf 'Hello, world!' > hello.txt
gzip -k hello.txt
zcat hello.txt.gz # Hello, world!
# عضوان: الثاني مُضاف بعد الأول ويُقرآن من جديد كأنهما واحد
printf 'Hello, ' | gzip > two.gz
printf 'world!' | gzip >> two.gz
zcat two.gz # Hello, world!
# الشيء نفسه في سكربت: كل الأعضاء ثم غلاف واحد في كل مرة
import base64, gzip, zlib
two = open('two.gz', 'rb').read()
zl = base64.b64decode('eJzzSM3JyddRKM8vyklRBAAgXgSK')
raw = base64.b64decode('80jNycnXUSjPL8pJUQQA')
gzip.decompress(two) # b'Hello, world!'
zlib.decompress(two, wbits=31) # b'Hello, '
zlib.decompress(zl, wbits=15) # b'Hello, world!'
zlib.decompress(raw, wbits=-15) # b'Hello, world!'
zlib.decompress(zl, wbits=47) # b'Hello, world!'القيمة 31 لـ wbits تطلب غلاف gzip، أما القيمة 15، وهي الافتراضية، فتطلب غلاف zlib، وأما -15 فتطلب deflate الخام — والـ 15 في كل منها هي أكبر نافذة — بينما تأخذ القيمة 47 غلافَ gzip أو غلافَ zlib، أيهما بدأت به البايتات. لكن zlib.decompress تقرأ عضوًا واحدًا فقط، وتُسقط الباقي دون أن تقول شيئًا، ولهذا تعيد b'Hello, ' وحده من العضوين أعلاه؛ أما gzip.decompress فتقرؤها كلها. وPython أشد من هذه الصفحة في موضعين أيضًا: ترفض gzip.decompress البايتات التي بعد النهاية إن لم تكن أصفارًا، وترفض الدالّتان كلتاهما التدفق الذي ينتهي مبكرًا، بينما تعرض الصفحة ما خرج.
الأسئلة الشائعة
- لماذا نتيجتي المضغوطة أكبر مما كتبته؟
- لأن كل غلاف يكلّف بايتات من عنده، والنص القصير لا يحمل إلا القليل مما يزيله الضغط: فغلاف gzip يضيف 18 بايتًا، وغلاف zlib يضيف 6، وDEFLATE يحدّد كتله فوق ذلك، فتكبر الكلمات القليلة حيث يصغر JSON الطويل، والصفحة تقول ذلك في تنبيه. وdeflate الخام هو الأصغر بين الثلاثة. وأي نتيجة تُكتب بـ Base64 تطول بالثلث مرةً أخرى.
- لماذا لا يطابق الناتج ما يكتبه gzip -c؟
- لا شيء يَعِد بأنه سيطابقه. فترويسة gzip قد تحمل اسمًا، ووقتًا، وعلامةً على المستوى، وبايتًا لنظام التشغيل، يملؤها GNU gzip والمتصفح بطريقتين مختلفتين، والاثنان ضاغطان مختلفان، فحتى البيانات التي بين الترويسة والذيل قد تختلف في نص أطول. وتدفقا gzip لنص واحد صحيحان كلاهما إذا كان فك ضغط كل منهما يعطيه، لذا قارن ما يعطيه فك ضغطهما: فالزر «استخدمه كإدخال» يعيد نتيجة هذه الصفحة إلى أصلها مباشرةً.
- ماذا يعني H4sI في بداية حمولة؟
- يعني أن الحمولة gzip مكتوب بـ Base64: فكل تدفق gzip يبدأ بالبايتات 1f 8b 08، وهذه البايتات الثلاثة هي H4sI في Base64. الصقها هنا كما هي. والحمولة التي تبدأ بـ eJ هي على الأرجح zlib بمستواه الافتراضي، والتي لا تبدأ بأيٍّ منهما قد تكون deflate خامًا، أو غير مضغوطة أصلًا، وهو ما تخبرك به الصفحة.
- هل gzip تشفير؟
- لا. فالضغط لا يأخذ مفتاحًا، لذا يستطيع أي شخص يملك البايتات أن يفك ضغطها، هنا أو بـ gunzip، فيعود كل بايت. وكلمة المرور داخل حمولة مضغوطة بـ gzip مكشوفة تمامًا ككلمة مرور مكتوبة كاملةً.
- هل تُرفَع حمولتي أو ملفي إلى أي مكان؟
- لا. فالحمولة التي تلصقها والملف الذي تختاره أو تُسقطه يُقرآن كلاهما داخل هذا التبويب، حيث يتولى Web Worker تشغّله الصفحة الضغطَ وفكَّ الضغط، ويبني الزر «تنزيل» ملفه من النتيجة هناك مباشرةً. ولا يُرسَل أيٌّ منهما إلى هذا الموقع ولا إلى أي جهة أخرى.
أدوات ذات صلة
- منسّق JSON
يتحقق من JSON ويجمّله، مع تحديد أماكن الأخطاء بوضوح.
- محوّل أحجام البيانات
حوّل بين KB وMB وGB وبين KiB وMiB وGiB.
- Base64
يرمّز ويفكّ ترميز Base64 — بدعم UTF-8 كامل.
- Base32
يرمّز ويفكّ ترميز Base32 وbase32hex — نص بترميز UTF-8 أو بايتات بالست عشري.