游程编码是一种比较简单的压缩算法,其基本思想是将重复且连续出现屡次的字符使用(连续出现次数,某个字符)来描述。html
好比一个字符串:python
AAAAABBBBCCC
使用游程编码能够将其描述为:git
5A4B3C
5A表示这个地方有5个连续的A,同理4B表示有4个连续的B,3C表示有3个连续的C,其它状况以此类推。算法
原字符串须要12个字符才能描述,而使用游程编码压缩以后只须要6个字符就能够表示,还原回去的时候只须要将字符重复n次便可,这是个原理很是简单的算法。数据结构
再来分析一下这个编码的适用场景,前面说过了这个算法的基本思想是将重复且连续出现的字符进行压缩,使用更简短的方式来描述,这种方式是基于柯氏复杂度的,那么什么是柯氏复杂度呢,好比有这么三个字符串,它们的长度都是100,其中第一个是100个A,第二个是99个A和一个B,第三个是100个彻底随机的字符,咱们想办法用尽量短的语言来描述原字符串,描述第一个字符串时能够说“这是100个A”,描述第二个字符串能够说“这是99个A,而后是一个B”,可是描述第三个字符串时应该怎么说呢?比较容易想到的是“这是100个随机字符”,看上去彷佛没有问题,可是这里一个比较重要的点是描述信息须要符合这么一个条件,当单独给出对字符串的描述时可以根据描述恢复出原字符串的内容,100个A和先99个A而后是一个B能够,可是100个随机字符太笼统了显然不行,这个描述信息的长度就称之为柯氏复杂度,在这个例子中第一个柯氏复杂度最小,第二第三依次次之。dom
那么在不一样状况下这个编码的效果如何呢,假如采用定长1个字节来描述连续出现次数,而且一个字符占用1个字节,那么描述(连续出现次数,某个字符)须要的空间是2个字节,只要这个连续出现次数大于2就可以节省空间,好比AAA占用3个字节,编码为(3,A)占用两个字节,可以节省一个字节的空间,能够看出连续出现的次数越多压缩效果越好,节省的空间越大,对一个字符编码可以节省的空间等于=连续出现次数-2,因而就很容易推出连续出现次数等于2时占用空间不变,好比AA占用两个字节,编码为(2,A)仍然占用两个字节,白白浪费了对其编码的资源却没有达到节省空间的效果,还有更惨的状况,就是连续出现次数老是为1,这个时候会越压越大,好比A占用一个字节,编码为(1,A)占用两个字节,比原来多了一个字节,这种状况就很悲剧,一个1M的文件可能一下给压缩成了2M(真是效果奇佳啊),这是可以出现的最糟糕的状况,至关于在文件的每个字节前面都插入了一个多余的字节0X01(这个字节表示连续出现次数为1),这种状况说明不适合使用游程编码,事实上,绝大多数数据的特征都属于第三种状况,不适合使用游程编码。编码
本节将尝试使用游程编码对文本进行压缩。spa
先来写一种最简单的实现,只能压缩英文字母,读取一个字符串,而后一直数出现了多少个连续字符,当重复被打断时就将上一个的重复字符和重复次数记一下,恢复时反之:
htm
#! /usr/bin/python3 # -*- coding: utf-8 -*- import random import string def rle_compress(s): """ 对字符串使用RLE算法压缩,s中不能出现数字 :param s: :return: """ result = '' last = s[0] count = 1 for _ in s[1:]: if last == _: count += 1 else: result += str(count) + last last = _ count = 1 result += str(count) + last return result def rle_decompress(s): result = '' count = '' for _ in s: if _.isdigit(): count += _ else: result += _ * int(count) count = '' return result def random_rle_friendly_string(length): """ 生成对RLE算法友好的字符串以演示压缩效果 :param length: :return: """ result = '' while length > 0: current_length = random.randint(1, length) current_char = random.choice(string.ascii_letters) result += (current_char * current_length) length -= current_length return result if __name__ == '__main__': raw_string = random_rle_friendly_string(128) rle_compressed = rle_compress(raw_string) rle_decompress = rle_decompress(rle_compressed) print(' raw string: %s' % raw_string) print(' rle compress: %s' % rle_compressed) print('rle decompress: %s' % rle_decompress)
执行效果:blog
能够看到,128个字符的原始数据被压缩成9个字符,大约是原来的7%。
可是上面的不支持数字是个硬伤,为何不支持数字呢,由于没办法区分一个数字到底是表示连续出现次数的数字仍是重复字符,这个是编码中常常出现的问题,一个颇有效的方式是使数据结构化。
啥是结构化呢,看这个“100M22c6t”,表示连续出现次数的数字100占用三个字符,22占用两个字符,6占用1个字符,不是定长的啊,这个时候能够规定个人整个字符串能够从最开始按两个字符进行分组,每一组的第一个字符表示连续出现次数,第二个字符表示连续出现的字符,这样我按照位置区分数据的类型,就可以存储任意字符了:
#! /usr/bin/python3 # -*- coding: utf-8 -*- import random import string def rle_compress(s): """ 对字符串使用RLE算法压缩,s能够出现任意字符,包括数字,由于采用了定长表示重复次数 :param s: :return: """ result = '' last = s[0] count = 1 for _ in s[1:]: # 采用一个字符表示重复次数,因此count最大是9 if last == _ and count < 9: count += 1 else: result += str(count) + last last = _ count = 1 result += str(count) + last return result def rle_decompress(s): result = '' for _ in range(len(s)): if _ % 2 == 0: result += int(s[_]) * s[_ + 1] return result def random_rle_friendly_string(length): """ 生成对RLE算法友好的字符串以演示压缩效果 :param length: :return: """ char_list_to_be_choice = string.digits + string.ascii_letters result = '' while length > 0: current_length = random.randint(1, length) current_char = random.choice(char_list_to_be_choice) result += (current_char * current_length) length -= current_length return result if __name__ == '__main__': # raw_string = random_rle_friendly_string(128) raw_string = "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMcccccccccccccccccccccctttttt" rle_compressed = rle_compress(raw_string) rle_decompress = rle_decompress(rle_compressed) print(' raw string: %s' % raw_string) print(' rle compress: %s' % rle_compressed) print('rle decompress: %s' % rle_decompress)
执行效果:
采用定长表示连续出现次数,可是由于一个字符最大可以表示9,因此一样的数据压缩以后比原来大了一些。
前面的例子使用游程编码压缩文本,看起来很好理解,那么游程编码能不能用来压缩二进制文件呢。
二进制文件在内存中的表示是字节,因此须要想办法可以压缩字节,压缩字节和压缩字符实际上是同样同样的,仍是结构化的存储,每两个字节一组,每组的第一个字节表示连续出现次数,第二个字节表示连续出现的字节。
#! /usr/bin/python3 # -*- coding: utf-8 -*- def compress_file_use_rle(src_path, dest_path): with open(dest_path, 'wb') as dest: with open(src_path, 'rb') as src: last = None count = 0 t = src.read(1) while t: if last is None: last = t count = 1 else: # 一个字节可以存储的最大长度是255 if t == last and count < 255: count += 1 else: dest.write(int.to_bytes(count, 1, byteorder='big')) dest.write(last) last = t count = 1 t = src.read(1) dest.write(int.to_bytes(count, 1, byteorder='big')) dest.write(last) def decompress_file_use_rle(src_path, dest_path): with open(dest_path, 'wb') as dest: with open(src_path, 'rb') as src: count = src.read(1) byte = src.read(1) while count and byte: dest.write(int.from_bytes(count, byteorder='big') * byte) count = src.read(1) byte = src.read(1) if __name__ == '__main__': img_name = 'test-bmp-24' suffix = 'bmp' raw_img_path = 'data/%s.%s' % (img_name, suffix) rle_compressed_img_save_path = 'data/%s-rle-compressed.%s' % (img_name, suffix) rle_decompress_img_save_path = 'data/%s-rle-decompress.%s' % (img_name, suffix) compress_file_use_rle(raw_img_path, rle_compressed_img_save_path) decompress_file_use_rle(rle_compressed_img_save_path, rle_decompress_img_save_path)
对文件进行压缩比较适合的状况是文件内的二进制有大量的连续重复,一个经典的例子就是具备大面积色块的BMP图像,BMP由于没有压缩,因此看到的是什么样子存储的时候二进制就是什么样子,来作一个简单的实验,Win+R,输入mspaint打开Windows自带的画图程序,随便涂抹一副具备大面积色块的图片:
保存时选择256色的BMP:
为何必定要是BMP呢,由于这种算法不压缩,存储二进制的规律与看到的一致,那为何是256色呢,由于上面写的程序只计算后面一个字节的重复次数,而一个字节可以表示最大256色,若是表示一个像素超过了一个字节,那么上面的代码将极可能越压越大起不到任何做用。
而后对比一下结果:
压缩效果很惊人,原来370K的文件压缩后只有7K,是原来的1.9%,至关于98.1%的数据被压掉了,而且是无损压缩。
那么试一下对于颜色比较丰富的256色图图片呢,随便搞一张信息量比较大的贴到画图中而后保存为256色BMP:
将其转为256色以后会损失一些颜色,而后看下效果:
效果并不太理想,因此仍是具备大面积色块的256色如下的BMP更合适,固然存储图片时通常也没有使用BMP格式的,太奢侈了,这里只是强行举了一个例子。
另外值得一提的是,由于是边读取边压缩边写入,因此这种也能够读取输入流中的数据写到输出流,不受文件大小的限制能够无限压缩下去。
游程编码适合的场景是数据自己具备大量连续重复出现的内容。
.