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

  • 关注 字节的数值
  • 附加,\log 2的计算网址:https://tool.91maths.com/log/15.html

二、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()函数:然后重构哈夫曼树

编码测试

  • 先编译程序
1
g++ -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp

  • 现在我们想将./testfile/下的test_file1文件进行压缩,输入下面的命令即可,效果如下图所示:
    • 红框圈出来的部分:每一个字符的信息,以红框为例,[105868]代表该字符在文件中的偏移(seek),125代表该字符的ASCII值,01111101代表哈夫曼编码(值越大说明出现的次数越少,值越小说明出现的次数越多)
    • 下面箭头是打印的程序总的运行信息
1
./huffman ./testfile/test_file1

  • 程序运行之后会生成一个压缩文件(.huffman)和一个解压缩文件(.unhuffman)。通过下图可以看出test_file1压缩之后由104K变为了65K,解压之后又回到了104K,压缩比为0.625

  • 现在我们对视频进行压缩看看,因为视频文件大小比较大,所以在进行解压缩的时候大量的打印信息会导致程序运行很久才会结束,因此在对视频进行解压缩之前将FileCompress.hpp文件中的一处打印语句注释掉(如下图所示)

  • 注释掉之后重新编译,运行,然后查看解压缩信息,如下所示,可以看出视频在压缩之后只减少了1M,因此视频的压缩效率比较低
1
2
3
g++ -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp

./huffman ./testfile/1.flv

  • 现在我们使用Windows自带的.zip压缩工具来对比一下,可以看到其压缩效率比我们上面的哈夫曼解压缩的效率要高

  • 这种哈夫曼解压缩的效率比较低,在Zlib库文章中我们有调用Zlib库API编写的解压缩程序,因为是直接调用库函数,所以解压缩的效率比较高,可以参阅:https://blog.csdn.net/qq_41453285/article/details/106688596

五、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压缩算法

  • zlib压缩算法页是对deflate进行的封装
  • zlib=zlib头+deflate编码的实际内容+zlib尾
  • 他也是一个实现库(delphi中有zlib,zlibex),Zlib库参阅:https://blog.csdn.net/qq_41453285/article/details/106688596

七、Nginx中的gzip模块

  • Nginx中有压缩数据模块(gzip模块),可以用来提升网站速度。具体可以参阅:https://blog.csdn.net/qq_41453285/article/details/106338837