Linux(程序設計):28---數據流壓縮原理(Deflate壓縮算法、gzip、zlib)

一、壓縮原理

  • 壓縮原理其實很簡單,就是找出那些重複出現的字符串,然後用更短的符號代替, 從而達到縮短字符串的目的。比如,有一篇文章大量使用"中華人民共和國"這個詞語, 我們用"中國"代替,就縮短了 5 個字符,如果用"華"代替,就縮短了6個字符。事實上, 只要保證對應關係,可以用任意字符代替那些重複出現的字符串
  • 本質上,所謂"壓縮"就是找出文件內容的概率分佈,將那些出現概率高的部分代替成更短的形式。所以:
    • 內容越是重複的文件,就可以壓縮地越小。比如,"ABABABABABABAB"可以壓縮成"7AB"
    • 相應地,如果內容毫無重複,就很難壓縮。極端情況就是,遇到那些均勻分佈的隨機字符串,往往連一個字符都壓縮不了。比如,任意排列的10個阿拉伯數字 (5271839406),就是無法壓縮的;再比如,無理數(比如π)也很難壓縮

壓縮極限

  • 概念:當每一個字符都不重複的時候,就不能再去壓縮了,也就是不能無限的壓縮
  • 香農極限:

  • 下面是一個例子。假定有兩個文件都包含1024個符號,在ASCII碼的情況下,它們的長度是相等的,都是1KB。甲文件的內容 50%是a,30%b,20%是c,文本里面只有abc,則平均每個符號要佔用1.49個二進制位

  • 比如每個字節的數值概率是0~255,均勻分佈每個數值出現的概率 1/256,如果一 段文字的字節數值是平均分佈,則Pn = 1/256,計算出極限爲8

二、Deflate壓縮算法

  • deflate壓縮算法用來很多地方:
    • 例如其是zip壓縮文件的默認算法、在zip文件中,在7z, xz 等其他的壓縮文件中都用
    • gzip壓縮算法、zlib壓縮算法等都是對defalte壓縮算法的封裝(下面會介紹)
    • gzip、zlib等壓縮程序都是無損壓縮,因此對於文本的壓縮效果比較好,對視頻、圖片等壓縮效果不是很好(視頻一般都是採用有損壓縮算法),所以對於視頻、圖片這種已經是二進制形式的文件可以不需要壓縮,因爲效果也不是很明顯
  • 實際上deflate只是一種壓縮數據流的算法。 任何需要流式壓縮的地方都可以用
  • Deflate壓縮算法=LZ77算法+哈夫曼編碼
  • deflate算法下的壓縮器有三種壓縮模型:
    • 不壓縮數據,對於已經壓縮過的數據,這是一個明智的選擇。 這樣的數據會會稍稍增加,但是會小於在其上再應用一種壓縮算法
    • 壓縮,先用LZ77壓縮,然後用huffman編碼。 在這個模型中壓縮的樹是Deflate規範規定定義的, 所以不需要額外的空間來存儲這個樹
    • 壓縮,先用LZ77壓縮,然後用huffman編碼。 壓縮樹是由壓縮器生成的,並與數據 一起存儲
  • 數據被分割成不同的塊,每個塊使用單一的壓縮模式。 如過壓縮器要在這三種壓縮模式中相互切換,必須先結束當前的塊,重新開始一個新的塊

信息熵

  • 數據爲何是可以壓縮的,因爲數據都會表現出一定的特性,稱爲熵。絕大多數的數據所表現出來的容量往往大於其熵所建議的最佳容量。比如所有的數據都會有一定的冗餘性,我們可以把冗餘的數據採用更少的位對頻繁出現的字符進行標記,也可以基於數 據的一些特性基於字典編碼,代替重複多餘的短語

三、LZ77算法原理

  • Ziv和Lempel於1977年發表題爲“順序數據壓縮的一個通用算法(A Universal Algorithm for Sequential Data Compression )。LZ77 壓縮算法採用字典的方式進行壓縮, 是一個簡單但十分高效的數據壓縮算法。其方式就是把數據中一些可以組織成短語(最長字符)的字符加入字典,然後再有相同字符出現採用標記來代替字典中的短語,如此通過標記代替多數重複出現的方式以進行壓縮
  • 關鍵詞術語:
    • 前向緩衝區:每次讀取數據的時候,先把一部分數據預載入前向緩衝區。爲移入滑動窗口做準備,大小可以自己設定
    • 滑動窗口:一旦數據通過緩衝區,那麼它將移動到滑動窗口中,並變成字典的一部分。滑動窗口的大小也可以自己設定的
    • 短語字典:從字符序列 S1...Sn,組成n個短語。比如字符(A,B,D),可以組合的短語爲 {(A),(A,B),(A,B,D),(B),(B,D),(D)},如果這些字符在滑動窗口裏面,就可以記爲當前的短語字典,因爲滑動窗口不斷的向前滑動,所以短語字典也是不斷的變化
  • 優缺點:
    • 大多數情況下LZ77壓縮算法的壓縮比相當高,當然了也和你選擇滑動窗口大小, 以及前向緩衝區大小,以及數據熵有關係
    • 缺點:其壓縮過程是比較耗時的,因爲要花費很多時間尋找滑動窗口中的短語匹配
    • 優點:不過解壓過程會很快,因爲每個標記都明確告知在哪個位置可以讀取了

算法的主要邏輯

  • LZ77 的主要算法邏輯就是,先通過前向緩衝區預讀數據,然後再向滑動窗口移入(滑動窗口有一定的長度),不斷的尋找能與字典中短語匹配的最長短語,然後通過標記符標記
  • 我們還以字符ABD爲例子,看如下圖:

  • 目前從前向緩衝區中可以和滑動窗口中可以匹配的最長短語就是(A,B),然後向前移動的時候再次遇到(A,B)的時候採用標記符代替

LZ77壓縮原理

  • 當壓縮數據的時候,前向緩衝區與滑動窗口之間在做短語匹配的是後會存在2種情況:
    • (1)找不到匹配時:將未匹配的符號編碼成符號標記(多數都是字符本身)
    • (2)找到匹配時:將其最長的匹配編碼成短語標記
  • 短語標記包含三部分信息:
    • (1)滑動窗口中的偏移量(從匹配開始的地方計算)
    • (2)匹配中的符號個數
    • (3)匹配結束後的前向緩衝區中的第一個符號
  • 一旦把 n 個符號編碼並生成相應的標記,就將這 n 個符號從滑動窗口的一端移出, 並用前向緩衝區中同樣數量的符號來代替它們,如此,滑動窗口中始終有最新的短語

演示案例

  • 初始化:如下所示,滑動窗口的初始化大小爲8,向前緩衝區的大小爲4

  • 壓縮A:滑動窗口中沒有數據,所以沒有匹配到短語,將字符A標記爲A

  • 壓縮B:滑動窗口中有 A,沒有從緩衝區中字符(BABC)中匹配到短語,依然把B標記爲B

  • 壓縮ABC:緩衝區字符(ABCB)在滑動窗口的位移6位置找到AB,成功匹配到短語AB,將AB編碼爲(6,2,C)
    • 6:重複的字符串的起始位置。此處爲滑動窗口索引[6]處
    • 2:重複的字符串長度爲2,也就是AB
    • C:重複的字符串的下一個字符是C

  • 壓縮BABA:緩衝區字符(BABA)在滑動窗口位移4的位置匹配到短語 BAB,將 BAB 編碼爲(4,3,A)
    • 4:重複的字符串的起始位置。此處爲滑動窗口索引[4]處
    • 3:重複的字符串長度爲3,也就是BAB
    • A:重複的字符串的下一個字符是A

  • 壓縮BCA:緩衝區字符(BCAD)在滑動窗口位移 2 的位置匹配到短語BC,將BC編碼爲 (2,2,A)
    • 2:重複的字符串的起始位置。此處爲滑動窗口索引[2]處
    • 2:重複的字符串長度爲3,也就是BC
    • A:重複的字符串的下一個字符是A

  • 最後壓縮D:緩衝區字符 D,在滑動窗口中沒有找到匹配短語,標記爲D

  • 緩衝區中沒有數據進入了,結束

LZ77解壓原理

  • 解壓類似於壓縮的逆向過程,通過解碼標記和保持滑動窗口中的符號來更新解壓數據
  • 當解碼字符標記:將標記編碼成字符拷貝到滑動窗口中
  • 解碼短語標記:在滑動窗口中查找相應偏移量,同時找到指定長短的短語進行替換

演示案例

  • 以上面最終壓縮的樣子爲例,起始如下所示

  • 解壓A:標記爲A,直接將A解壓

  • 解壓B:標記爲B,直接將B解壓

  • 解壓(6,2,C):標記爲(6,2,C),通過在滑動窗口中找到索引[6]處,然後找到2個字符(AB),最後加上一個C,所以最終解壓出來的就是ABC

  • 解壓(4,3,A):標記爲(4,3,C),通過在滑動窗口中找到索引[4]處,然後找到3個字符(BAB),最後加上一個A,所以最終解壓出來的就是BABA

  • 解壓(2,2,A):標記爲(2,2,A),通過在滑動窗口中找到索引[2]處,然後找到2個字符(BC),最後加上一個A,所以最終解壓出來的就是BCA

  • 解壓D:標記爲D,直接將D解壓

四、Huffman算法原理

  • 哈夫曼設計了一個貪心算法來構造最優前綴碼,被稱爲哈夫曼編碼(Huffman code), 其正確性證明依賴於貪心選擇性質和最優子結構。哈夫曼編碼可以很有效的壓縮數據,具體壓縮率依賴於數據本身的特性
  • 這裏我們先介紹幾個概念:
    • 碼字:每個字符可以用一個唯一的二進制串表示,這個二進制串稱爲這個字符的碼字
    • 碼字長度:這個二進制串的長度稱爲這個碼字的碼字長度
    • 定長編碼:碼字長度固定就是定長編碼。
    • 變長編碼:碼字長度不同則爲變長編碼。
  • 變長編碼可以達到比定長編碼好得多的壓縮率,其思想是賦予高頻字符(出現頻率高的字符)短(碼字長度較短)碼字,賦予低頻字符長碼字。例如,我們 用 ASCII 字符編輯一個文本文檔,不論字符在整個文檔中出現的頻率,每個字符都要佔 用一個字節。如果我們使用變長編碼的方式,每個字符因在整個文檔中的出現頻率不同導致碼字長度不同,有的可能佔用一個字節,而有的可能只佔用一比特,這個時候,整 文檔佔用空間就會比較小了。當然,如果這個文本文檔相當大,導致每個字符的出現頻率基本相同,那麼此時所謂變長編碼在壓縮方面的優勢就基本不存在了(這點要十分明確,這是爲什麼壓縮要分塊的原因之一,源碼分析會詳細講解)

構造過程

  • 哈夫曼編碼會自底向上構造出一棵對應最優編碼的二叉樹,我們使用下面這個例子來說明哈夫曼樹的構造過程
  • 首先,我們已知在某個文本中有如下字符及其出現頻率:

  • 構造過程如下圖所示:
    • 在一開始,每個字符都已經按照出現頻率大小排好順序
    • 在後續的步驟中,每次都將頻率最低的兩棵樹合併,然後用合併後的結果再次排序(注意,排序不是目的,目的是找到這時出現頻率最低的兩項,以便下次合併。gzip 源碼中並沒有專門去“排序”,而是使用專門的數據結構把頻率最低的兩項找到即可)
    • 葉子節點用矩形表示,每個葉子節點包含一個字符及其頻率。中間節點用圓圈表示,包含其孩子節點的頻率之和。中間節點指向左孩子的邊標記爲 0, 指向右孩子的邊標記爲 1。一個字符的碼字對應從根到其葉節點的路徑上的邊的標籤序列
    • 圖1爲初始集合,有六個節點,每個節點對應一個字符;圖2到圖5爲中間步驟, 圖6爲最終哈夫曼樹。此時每個字符的編碼都是前綴碼

哈夫曼編碼編碼實現

  • 利用庫中的優先級隊列實現哈夫曼樹,最後基於哈夫曼樹最終實現文件壓縮
  • 代碼結構爲:
    • 1.統計文件中字符出現的次數,利用優先級隊列構建Haffman樹,生成Huffman編碼。構造過程可以使用 priority_queue 輔助,每次pq.top()都可以取出權值(頻數)最小 的節點。每取出兩個最小權值的節點,就new出一個新的節點,左右孩子分別指向它 們。然後把這個新節點 push 進優先隊列。
    • 2.壓縮:利用 Haffman 編碼對文件進行壓縮,即在壓縮文件中按順序存入每個字符 的 Haffman 編碼。 碼錶(實際存儲是對應數值的概率,然後調用程序生成碼錶) + 編碼
    • 3.將文件中出現的字符以及它們出現的次數寫入配置文件中,以便後續壓縮使用
    • 4.解壓縮:利用配置文件重構 Haffman 樹,對文件進行減壓縮
  • 源碼鏈接爲:https://github.com/dongyusheng/csdn-code/tree/master/HuffmanDecompression
  • 目錄結構爲:FileCompress.hpp、HuffmanTree.hpp、main.cpp三個文件是哈夫曼編碼的主要代碼,testfile/目錄下是一些測試的代碼(用來壓縮和解壓縮的)

代碼講解

  • Compress()函數:會構造一棵哈夫曼樹,然後讀取文件,將文件中的每個字符進行編碼形成一個二進制值,然後打印出來

  • Compress()函數:碼錶的相關信息,info._ch打印的是每個字符,info._count是這個字符出現的頻率,就是上面“構造過程”對應的第一張圖。然後會將這個碼錶的信息寫入壓縮文件中

  • Compress()函數:壓縮完成之後會打印相關信息,其中“huffman code table  size”是碼錶的大小,就是

  • Uncompress()函數:讀取碼錶(寫入字符的信息)

  • Uncompress()函數:然後重構哈夫曼樹

編碼測試

  • 先編譯程序
g++ -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp

  • 現在我們想將./testfile/下的test_file1文件進行壓縮,輸入下面的命令即可,效果如下圖所示:
    • 紅框圈出來的部分:每一個字符的信息,以紅框爲例,[105868]代表該字符在文件中的偏移(seek),125代表該字符的ASCII值,01111101代表哈夫曼編碼(值越大說明出現的次數越少,值越小說明出現的次數越多)
    • 下面箭頭是打印的程序總的運行信息
./huffman ./testfile/test_file1

  • 程序運行之後會生成一個壓縮文件(.huffman)和一個解壓縮文件(.unhuffman)。通過下圖可以看出test_file1壓縮之後由104K變爲了65K,解壓之後又回到了104K,壓縮比爲0.625

  • 現在我們對視頻進行壓縮看看,因爲視頻文件大小比較大,所以在進行解壓縮的時候大量的打印信息會導致程序運行很久纔會結束,因此在對視頻進行解壓縮之前將FileCompress.hpp文件中的一處打印語句註釋掉(如下圖所示)

  • 註釋掉之後重新編譯,運行,然後查看解壓縮信息,如下所示,可以看出視頻在壓縮之後只減少了1M,因此視頻的壓縮效率比較低
g++ -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp

./huffman ./testfile/1.flv

 

  • 現在我們使用Windows自帶的.zip壓縮工具來對比一下,可以看到其壓縮效率比我們上面的哈夫曼解壓縮的效率要高

五、gzip壓縮算法

  • gzip壓縮算法是對deflate進行的封裝。gzip本身只是一種文件格式,其內部通常採用Deflate數據格式,而Deflate採用LZ77壓縮算法來壓縮數據
  • gzip=gzip頭+deflate 編碼的實際內容+gzip尾
  • gzip文件由1到多個“塊”組成,實際上通常只有1塊。每個塊包含頭、數據和尾3部分。塊的概貌如下:

頭部分

  • ID1 與 ID2:各 1 字節。固定值,ID1 = 31 (0x1F),ID2 = 139(0x8B),指示 GZIP 格式
  • CM:1 字節。壓縮方法。目前只有一種:CM = 8,指示 DEFLATE 方法
  • FLG:1 字節。標誌:
    • bit 0 FTEXT - 指示文本數據
    • bit 1 FHCRC - 指示存在 CRC16 頭校驗字段
    • bit 2 FEXTRA - 指示存在可選項字段
    • bit 3 FNAME - 指示存在原文件名字段
    • bit 4 FCOMMENT - 指示存在註釋字段 bit 5-7 保留
  • MTIME:4 字節。更改時間。UINX 格式
  • XFL:1 字節。附加的標誌。當 CM = 8 時, XFL = 2 - 最大壓縮但最慢的算法;XFL = 4 - 最快但最小壓縮的算法
  • OS:1 字節。操 作系統,確切地說應該是文件系統。有下列定義:
    • 0 - FAT 文件系統 (MS-DOS, OS/2, NT/Win32)
    • 1 - Amiga
    • 2 - VMS/OpenVMS
    • 3 - Unix
    • 4 - VM/CMS
    • 5 - Atari TOS
    • 6 - HPFS 文件系統 (OS/2, NT)
    • 7 - Macintosh
    • 8 - Z-System
    • 9 - CP/M
    • 10 - TOPS-20
    • 11 - NTFS 文件系統 (NT)
    • 12 - QDOS
    • 13 - Acorn RISCOS
    • 255 - 未知

額外的頭字段

  • 存在額外的可選項時,SI1 與 SI2 指示可選項 ID,XLEN 指示可選項字節數。如 SI1 = 0x41 ('A'),SI2 = 0x70 ('P'),表示可選項是 Apollo 文件格式的額外數據
  • (若 FLG.FEXTRA = 1)

  • (若 FLG.FNAME = 1)

  • (若 FLG.FCOMMENT = 1)

  • (若 FLG.FHCRC = 1)

數據部分

  • BFINAL:1 比特。0 - 還有後續子塊;1 - 該子塊是最後一塊
  • BTYPE:2 比特。00 - 不壓縮;01 - 靜態 Huffman 編碼壓縮;10 - 動態 Huffman 編碼壓縮;11 - 保留
  • 各種情形的處理過程,請參考後面列出的 RFC 文檔

尾部分

  • CRC32:4 字節。原始(未壓縮)數據的 32 位校驗和
  • ISIZE:4 字節。原始(未壓縮)數 據的長度的低 32 位。
  • GZIP 中字節排列順序是 LSB 方式,即 Little-Endian,與 ZLIB 中的相反

六、zlib壓縮算法

七、Nginx中的gzip模塊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章