ランレングス符号化をPythonで実装してみた
今回は、ランレングス符号化をPythonで実装してみたので、その解説をしてみようと思いまs。
ランレングス符号化とは?
ランレングス符号化とはデータ圧縮方式の一つで、連続する同じデータをまとめて扱うことで圧縮をするというものです。
たとえばAABBBCCCC
というテキストがあった場合に、各文字とそれがいくつ連続しているかという情報に変換してみます。するとA2B3C4
のように変換でき、元のテキストの長さ9文字と比較して6文字に収めることができます。
ただし、ランレングス符号化では必ず圧縮後のデータ量が小さくなる保証はありません。たとえばABCDEF....XYZ
のようにアルファベットが全て並んでいるテキストを圧縮する場合、エンコーディング前は26文字なのに対し、エンコード後は2倍の52文字になってしまいます。
また、たとえば12
というテキストを符号化すると1121
のように符号化できますが、捉えようによっては1が121回連続したのかもしれないと判断できます。このような場合、たとえば11:21
のように何かしらのセパレータを導入して区切りを与えることはできますが、その分文字数が増えてしまう欠点もあるかと思います。
Pythonで実装してみた
それではPythonで実装してみましょう。まずは愚直にやってみます。
import click
@click.command()
@click.option("-t", "--text", type=str, default=None)
@click.option("-f", "--file", type=str, default=None)
def main(text, file):
if isinstance(text, str) and file is None:
target_text = text
elif isinstance(file, str) and text is None:
with open(file, "r") as f:
target_text = "".join([line.strip() for line in f.readlines() if len(line.strip()) > 0])
else:
raise Exception("Please specify input data")
encoded_data = []
for item in target_text:
if len(encoded_data) == 0:
encoded_data.append([item, 1])
continue
if item == encoded_data[-1][0]:
encoded_data[-1][1] += 1
else:
encoded_data.append([item, 1])
encoded_text = "".join([f"{item[0]}{item[1]}" for item in encoded_data])
print(encoded_text)
target_text_length = len(target_text)
encoded_text_length = len(encoded_text)
print(f"Original text's length: {target_text_length}")
print(f"Encoded text's length: {encoded_text_length}")
print(f"Compress rate: {encoded_text_length / target_text_length * 100: .3f}%")
if __name__ == "__main__":
main()
まずclickを使ってコマンドライン引数を受け取る準備をしています。-t
が指定されれば指定されたテキストを、-f
が指定されれば指定されたファイルを読み込みます。
次に、ランレングス符号化のロジックについてです。encoded_data
には連続して同じ文字が表示された回数を格納するようにしています。for文で順番に文字を見ていきますが、一番最初のループでは以下のようにしてリセットしています。
if len(encoded_data) == 0:
encoded_data.append([item, 1])
continue
2文字目以降はencoded_data
の最後のアイテムと比較し、同じ文字であればカウントを増やし、そうでない場合は新たにデータを末尾に追加しています。
if item == encoded_data[-1][0]:
encoded_data[-1][1] += 1
else:
encoded_data.append([item, 1])
それでは実際に動かしてみましょう。たとえば最初のサンプルのAABBBCCCC
を動かしてみると以下のようになりました。
❯ uv run main.py -t AABBBCCCC
A2B3C4
Original text's length: 9
Encoded text's length: 6
Compress rate: 66.667%
これは予想していた通りの符号化になっています。また、圧縮後は3文字減っており、圧縮後は元データの約67%まで圧縮できています。
それでは次に試しとして、私の前回の記事をtextファイルにして読み込ませてみます。
実行してみると以下の結果になりました。
❯ uv run main.py -f python_new_feature.txt
今1回1は1、1P1y1t1h1o1n131.11131で1導1入1さ1れ1る1新1機1能1に1つ1い1て1ま1と1め1て1み1ま1し1た1。1私1自1身1、1普1段1は131.112ま1た1は131.11121を1使1う1こ1と1が1多1く1、131.11131に1つ1い1て1は1ど1の1よ1う1な1機1能1が1追1加1さ1れ1る1か1認1識1し1て1い1な1か1っ1た1の1で1調1べ1て1み1ま1し1た1。1#2 1追1加1さ1れ1る1新1機1能1ま1ず1、1P1y1t1h1o1n131.11131で1導1入1さ1れ1る1新1機1能1は1こ1ち1ら1に1ま1と1ま1っ1て1い1ま1す1の1で1、1詳1し1く1は1参1照1く1だ1さ1い1。1h1t2p1s1:1/2d1o1c1s1.1p1y1t1h1o1n1.1o1r1g1/131/1w1h1a1t1s1n1e1w1/131.11131.1h1t1m1l1#1o1t1h1e1r1-1l1a1n1g1u1a1g1e1-1c1h1a1n1g1e1s1#3 1よ1り1よ1い1イ1ン1タ1ラ1ク1テ1ィ1ブ1イ1ン1タ1プ1リ1タ1P1y1P1y1プ1ロ1ジ1ェ1ク1ト1の1コ1ー1ド1を1ベ1ー1ス1と1... 以下省略
Original text's length: 3990
Encoded text's length: 7716
Compress rate: 193.383%
日本語だからというのもありますが、圧縮後の文字数が原文のおよそ2倍程度にまで膨れ上がってしまっています。これをみても分かる通り、全てのケースで圧縮できるかというとそうではないということです。
どのような場合にランレングス符号化が役立つか
最後に、具体的なユースケースについて考えてみます。
まず圧倒的に向いていないのは日本語の文章だと思います。日本語の文章で同じ文字が連続して書かれることって割合としてごく稀だと思います。なので日本語の文章を圧縮するときは別の方法が適していると思います。
逆に向いているデータの例としては画像データのような、連続したデータが同じ値を持つ可能性がある場合です。画像データは隣接する画素の値は似る傾向が強いため、ユースケースとなるのではないでしょうか。
まとめ
今回はPythonでランレングス符号化を実装してみました。愚直な実装になっているのでもっと効率的な方法はあるかと思いますが、どのようなロジックで実行される圧縮方式かは紹介できたかなと思います。
他にも様々な圧縮方式がありますので、機会がありましたら実装も併せて紹介できればと思います。
Discussion