Gzip
הדביקו gzip, zlib או deflate גולמי ב־Base64 כדי לקרוא את מה שהם מכילים — הבייטים אומרים איזה מהם — או הקלידו טקסט כדי לדחוס אותו לאחד משלושתם, בדפדפן שלך.
[
{
"name": "נועה",
"city": "תל אביב"
},
{
"name": "איתי",
"city": "חיפה"
},
{
"name": "מאיה",
"city": "ירושלים"
},
{
"name": "יונתן",
"city": "באר שבע"
},
{
"name": "תמר",
"city": "אילת"
},
{
"name": "אורי",
"city": "נצרת"
},
{
"name": "שירה",
"city": "אשדוד"
},
{
"name": "דניאל",
"city": "טבריה"
}
]נקרא כ־gzip.
- בייטים לא דחוסים
- 472
- בייטים דחוסים
- 179
- שינוי
- -62.1%
- תווי Base64
- 240
דחיסה אחת בשלוש עטיפות, והמילה 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. DeflateStream של .NET ו־gzdeflate של PHP כותבים deflate גולמי, ואילו gzcompress של PHP ו־zlib.compress של Python כותבים את העטיפה של zlib. לכן בייטים שמתויגים 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 שומר release: ה־JSON שלו, דחוס ב־gzip ואחר כך כתוב ב־Base64. כש־Helm שומר release ב־Secret של Kubernetes, קריאה ישירה מה־Secret נותנת Base64 פעמיים, כי Secret מחזיק את הנתונים שלו ב־Base64 משלו; הכלי Base64 מפענח את השכבה החיצונית הזאת ל־H4sI… שהעמוד הזה קורא.
שני בייטי הכותרת של zlib מציינים את השיטה שלו, את החלון שלו ואת הרמה שבה נכתב, ולכן עם חלון ברירת המחדל של zlib, ה־Base64 שלו מתחיל באחת מארבע דרכים: 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 גולמי אינו נושא checksum שאפשר לבדוק.
זרם שנגמר לפני סופו, כמו הדבקה שנקטעה או הורדה שנעצרה באמצע, מוצג עד כמה שהוא מגיע, עם הערה שה־checksum לא נבדק. זרם פגום נדחה, ושום חלק ממנו אינו מוצג. זה מחמיר יותר מ־gunzip, שכותב את מה שכבר ביטל את דחיסתו לפני שהוא מגיע ל־checksum שמגלה שזה שגוי; אבל ביט שהשתנה יכול להתפענח בשקט לאורך קטע מסוים לפני ששום דבר מסגיר אותו, כך שמה שיצא לפני שהנזק התגלה עלול כבר להיות שגוי. וגם שום מיקום אינו ניתן, כי המקום שבו מפענח מבחין בנזק אינו המקום שבו הנזק נמצא. וזרם 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 או בייטים בהקסדצימלי.