一、什么是游程编码
游程编码是一种比较简单的压缩算法,其基本思想是将重复且连续出现多次的字符使用(连续出现次数,某个字符)来描述。
比如一个字符串:
AAAAABBBBCCC
使用游程编码可以将其描述为:
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个随机字符太笼统了显然不行,这个描述信息的长度就称之为柯氏复杂度,在这个例子中第一个柯氏复杂度最小,第二第三依次次之。
那么在不同情况下这个编码的效果如何呢,假如采用定长1个字节来描述连续出现次数,并且一个字符占用1个字节,那么描述(连续出现次数,某个字符)需要的空间是2个字节,只要这个连续出现次数大于2就能够节省空间,比如AAA占用3个字节,编码为(3,A)占用两个字节,能够节省一个字节的空间,可以看出连续出现的次数越多压缩效果越好,节省的空间越大,对一个字符编码能够节省的空间等于=连续出现次数-2,于是就很容易推出连续出现次数等于2时占用空间不变,比如AA占用两个字节,编码为(2,A)仍然占用两个字节,白白浪费了对其编码的资源却没有达到节省空间的效果,还有更惨的情况,就是连续出现次数总是为1,这个时候会越压越大,比如A占用一个字节,编码为(1,A)占用两个字节,比原来多了一个字节,这种情况就很悲剧,一个1M的文件可能一下给压缩成了2M(真是效果奇佳啊),这是能够出现的最糟糕的情况,相当于在文件的每一个字节前面都插入了一个多余的字节0X01(这个字节表示连续出现次数为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:]:
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)
执行效果:
可以看到,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,所以同样的数据压缩之后比原来大了一些。
注:这里表示出现次数为了简单采用的是十进制0~9,也可以将其扩展为十六进制0~F,甚至扩展到无符号0~255,但可能会出现一些不可读字符。
三、 压缩字节(二进制文件)
前面的例子使用游程编码压缩文本,看起来很好理解,那么游程编码能不能用来压缩二进制文件呢。
二进制文件在内存中的表示是字节,所以需要想办法能够压缩字节,压缩字节和压缩字符其实是一样一样的,还是结构化的存储,每两个字节一组,每组的第一个字节表示连续出现次数,第二个字节表示连续出现的字节。
#! /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格式的,太奢侈了,这里只是强行举了一个例子。
另外值得一提的是,因为是边读取边压缩边写入,所以这种也可以读取输入流中的数据写到输出流,不受文件大小的限制可以无限压缩下去。
四、总结
游程编码适合的场景是数据本身具有大量连续重复出现的内容。
.