遊程編碼是一種比較簡單的壓縮算法,其基本思想是將重複且連續出現屢次的字符使用(連續出現次數,某個字符)來描述。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格式的,太奢侈了,這裏只是強行舉了一個例子。
另外值得一提的是,由於是邊讀取邊壓縮邊寫入,因此這種也能夠讀取輸入流中的數據寫到輸出流,不受文件大小的限制能夠無限壓縮下去。
遊程編碼適合的場景是數據自己具備大量連續重複出現的內容。
.