哈夫曼压缩算法-Python实现

2023-05-16

前面上传了A*算法的实现,就顺便把一起写的哈夫曼压缩也上传了吧




本文主要提供了Python版本的哈夫曼压缩算法实现,并在此基础上提供了命令行和基于Qt的GUI用户界面(User Interface)

完整效果

哈夫曼(Huffman Encoding)压缩算法-Python实现

哈夫曼编码作为计算机届非常底层的算法,不少领域都会出现该算法的身影,例如在MPEG图片压缩算法中等等。因此掌握哈夫曼算法以及相关的哈弗曼编码、哈弗曼树实现还是比较必要的。

0. 前言

我目前是一个大二的计算机系学生,最近数据结构课程的配套实验要求学生从十四个题中选择四个来做。刚好最近学习了PyQt的相关内容,于是本着学习的目的把我完成的成果记录下来,希望我的这篇博客能够帮助大家学习与理解。

数据结构算法实验

1. 编码

所有的文件,包括MP3音频、MP4视频、PDF文档、乃至于可执行文件(Windows上的.EXE、Linux中的.O等ELF格式文件),对于计算机来说都是二进制数据流,即由0101的比特构成的流(Stream)。

计算机底层以二进制形式存储文件

然而二进制流对于计算机而言是有意义的,对人而言却没有多大的意义了,因为人看不懂二进制的字节流。于是为了解决这个问题,就出现了编码。所谓编码即指将信息从一种格式转换为另外一种格式而不改变其内容,更详细的介绍,参考下面百度百科和维基百科的介绍。

From Wikipedia

编码是信息从一种形式或格式转换为另一种形式的过程;解码则是编码的逆过程。

维基百科上对编码的介绍

From BaiduBaike:

编码是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码简称编码。用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。编码在电子计算机、电视、遥控和通讯等方面广泛使用。编码是信息从一种形式或格式转换为另一种形式的过程。解码,是编码的逆过程。

百度百科上对编码的介绍

因此,编码其实指的就是转变信息的形式而不改变信息的内容。之所以要进行编码与解码,就是因为对于不同的主体而言,不同编码方式的信息的阅读难度和理解难度不同。例如上面的三国演义,对于我们人而言,文字形式的三国演义易于理解而二进制形式的三国演义更利于计算机理解。

换个角度,其实语种之间的转换也是一种编码的转换。例如英语对于中文母语的人就不好理解。而翻译也是把信息从一种语言转换为另一种语言,其形式在变,但本质内涵的信息却没有变。所以究其根本,编码的目的在于方便信息的理解。

英文版的三国演义

话说回来,要进行编码,就需要一张编码表。例如学C语言时候的ASCII表。我们通过编码表构建了两种不同编码之间的映射关系,因此单纯的ASCII字符的转换,就是数值的映射关系,从而通过数值-字符的转换,实现了编码与解码。其实从语言的角度来看,语言的编码不仅仅是单词的映射,还涉及语法的转换等等更加复杂的关系。

然而单纯的使用ASCII编码或者UTF8这种编码方式其实虽然很不错,计算机能够理解,但是他们的问题就是太浪费空间了。例如针对ASCII编码,每一个字符需要8个比特位(0-255)。因此哈夫曼编码除了方便计算机理解以外,更大的好处是能够节约空间。

说了这么久,总算是说到了哈夫曼编码,哈夫曼编码其实就是一种编码,和ASCII编码在本质上没有区别的。

2. 哈夫曼算法 / Huffman Coding

关于哈夫曼编码和对应的哈夫曼算法就不赘述了,在数据结构的课本上其实讲得非常好也非常的透彻。下面就放一下百度百科和维基百科的介绍

From Wikipedia

霍夫曼编码(英语:Huffman Coding),又译为哈夫曼编码、赫夫曼编码,是一种用于无损数据压缩的熵编码(权编码)算法。由美国计算机科学家大卫·霍夫曼(David Albert Huffman)在1952年发明。

维基百科上哈夫曼编码的介绍

From BaiduBaike

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。

百度百科上哈夫曼编码的介绍

3. 实现思路

针对不同的功能,整个程序的实现,主要分为三个层面:

  1. 核心层:负责哈夫曼算法的实现
  2. 数据存取层:负责任意形式数据的读取、保存
  3. 用户界面层:负责提供命令行用户界面以及图形用户接口,从而实现高级感(逼格)

A. 核心层

核心层的任务其实就是构建哈弗曼树、得到哈夫曼编码即可,主要算法其实在数据结构课本、B站视频、知乎上都有不少介绍。

然而一个重要的问题就是课本上和B站中的不少介绍视频都是针对字符(字母)进行哈夫曼编码的。我们由于要对任意格式的文件进行编码,因此就要从数据的本源:字节,进行哈夫曼编码。因此我们哈弗曼树的叶子节点实际上是字节。我们以8位二进制作为一个单元(Symbol)。然后针对这个Symbol进行编码。因此我们编码的对象就从26个字母变成了256种八位二进制数字。换而言之我们是对0~255这256个数字进行编码。

然后根据得到的哈夫曼编码将原字节数据一个字节(八位比特)进行编码、转换为哈夫曼编码即可。

B. 数据存取层

在数据存取层,我们假设现在已经对原数据(raw data)构建出了哈弗曼树并且得到了原数据的哈弗曼编码,而且根据这些哈夫曼编码得到了原数据的哈夫曼编码字节流。我们在数据存取层需要做的就是

  1. 保存转换后的新的哈夫曼编码的字节流
  2. 读取原始数据的二进制字节流
  3. 保存哈夫曼编码表

读取原始数据的二进制字节流其实问题不大,Python中以rb(read byte)模式打开文件即可。而哈夫曼编码表的保存则可以利用Python的Pickle模块将Python中的对象保存到磁盘/从磁盘中加载(我们以字典/映射的形式保存编码表)

因此数据存取层的关键就在于如何保存编码后的哈夫曼编码字节流?这个问题看似简单,其实有一个问题,就是(二进制)数据在计算机外存(固态、硬盘)中的保存都是整字节整字节的保存的,读取也是整字节整字节读取的。而我们对原文件的二进制流进行哈夫曼编码后实际上其长度是不定的,不一定是整字节的。

因此保存的时候虽然我们可以强行以整字节整字节的保存编码后的二进制流,但是在最后一位由于不一定是一个整字节,因此实际上存在问题。为了处理这个问题,我们设定一个特殊的字符EOF,由于8位二进制能表示256个数字(Symbol),而这些数字都有可能出现在原字节流中,因此我们是不能直接选取一个数字来表示文件结尾的。为此,在Python中,通过一个特定的类来实现文件结尾的表示,即我们自定义的EOF。在大文件(几MB的文件)中,一共有1024*1024个字节,因此从0~255这256个数字出现的次数一定是远远大于1的,而EOF的出现次数永远为1,因此必然会拥有最长的编码。

在我们转换完所有的原数据之后,我们把EOF添加到末尾去,并且如果最后不满足8位的话在最后补0。而在读取的时候,我们只需要获取最长长度的编码的长度,然后对最后一个字节做截取即可得到正确的完整的带有EOF的编码的编码后的原数据的哈夫曼编码字节流。

C. 用户界面层

在用户界面层,不关心具体的哈夫曼编码的实现、数据的保存方式等这些具体的细节。相比,用户界面层仅关注如何提供一个用户友好的用户界面。例如在终端(Terminal)提供选项,让用户制定压缩、解压的文件。还会检查用户指定的文件是否存在、查找用户需要解码的文件对应的的哈弗曼编码表的文件位置。

当然,GUI使用会让程序显得非常高级,因此GUI部分是必须的。而完成GUI程序,则借助Qt来完成,即使用Qt的Python Binding。

当然,为了便于写代码管理与开发,需要注意GUI的代码要与完成功能的代码在逻辑和形式上分离,因此采取前面所说的分层的思想。

因此将GUI作为用户界面层的一个组件(另一个组件为终端命令行接口),调用由数据存取层提供的接口函数完成功能。

在GUI内部,进行在GUI窗口上发生的事件的监听,以及轮转调度、回调函数调用等等功能。

4. 具体代码

本部分根据上面的讲解,对照代码进行详细的介绍。

项目结构如下

(base) jack@Alienware:~/projects/data_structure/11_huffman$ tree -L 2
.
├── ErrorHuffman.py
├── huff.py
├── resources
│   ├── 三国演绎.txt
│   ├── D_Star_illustration.pptx
│   ├── ICRA2022.pdf
│   ├── romance_of_three_kingdoms.rar
│   ├── Song.mp3
│   ├── str.txt
│   └── test.mp4
├── ui
│   └── main.ui
└── Ui_main.py

2 directories, 11 files

其中:

  • resource为项目资源文件夹,主要存放稍后用于验证压缩的资源,包括MP4、txt、MP3、rar、PDF等多种格式文件
  • ui为程序图形界面配置文件,负责设定界面大小、按钮位置等配置参数。而UI_main为具体的按钮等控件的实现代码
  • huff.py为最终的程序,内含所有完整代码;errHuff.py为先前写错的代码,问题出在了哈夫曼建树的时候得到的不是范式哈弗曼树,因此该程序遍历哈弗树得到哈夫曼编码的时候有问题,最后压缩出来的文件比源文件大了好几倍(反向压缩。。。应了刘某的反向抽烟XXX😂)

1. 导入库

主要导入的库为命令行参数获取库argparse、Python对象序列化库Pickle、路径处理库pathlib、堆管理库heapq、GUI库PyQt以及为了更好的显示得到的哈夫曼编码表的pandas库与命令行彩色输出的colorama库。

最后定义了文件的私有变量设定作者和日期,以及将可能存在源代码的文件加入文件搜索路径(类似于Linux环境变量LD_LIBRARY_PATH)

import sys
import pickle
import argparse
import itertools
import collections
from pathlib import Path
from heapq import heapify, heappush, heappop
from typing import Union, Any, Dict, List, Tuple
from PyQt5.QtCore import QDir

import pandas
import pandas as pd
from colorama import Fore, Style
from PyQt5.QtWidgets import *

__author__ = "Jack Wang"
__date__ = "2021/12/19"
sys.path.append(Path(__file__).resolve().parent.joinpath("./ui").__str__())

2. 定义EOF符号

EOF符号由于和0-255个数字一样将在构建树的进行比较,因此需要重载如等于(__eq__)、大于(__gt__)、小于(__lt__)等运算符。在Python中重载运算符以魔术方法(double underline)实现。最后由于在序列化以及字典中保存符号-哈夫曼编码键值对的时候,符号作为字典的键需要时可哈希的对象(Hashable),因此重写__hash__方法,以实现作为字典的键。

注意,上面说过了EOF需要保证编码是最长的,因此当合别人比大的时候总是False,比小的时候总是True,并且只会等于同样是EOF的对象

class _EOFSymbol:
    """
    Notes:
        _EOFSymbol 是内置的文件结尾的类,用于在编码的时候标志文件的结尾。
        由于在进行哈夫曼树的构建的时候需要选出来权最小的节点,因此需要重写比较的魔术方法。
        默认文档结尾是频率最少的字符,因此和任何数字比较
    """

    def __repr__(self):
        return "_EOF"

    def __lt__(self, other):
        return True

    def __gt__(self, other):
        return False

    def __eq__(self, other):
        return self.__class__ == other.__class__

    def __hash__(self):
        return hash(self.__class__)

_EOF = _EOFSymbol()

3. 哈夫曼编码

采用工厂模式,实现哈夫曼编码与解码的核心对象为_HuffmanCoder,与用于构建_HuffmanCoder的HuffmanFactory类。

_HuffmanCoder类负责:

  • 输出/打印哈夫曼编码表:get_code_table方法、print_code_table方法
  • 编码、解码字节流序列:_encode_streaming方法、_decode_streaming方法
  • 为用户界面层提供统一接口:encode方法、decode方法
  • 保存、加载哈夫曼编码表:save方法、load方法

其初始化参数负责接受已经获得的编码表和符号的拼接方式以及eof符号定义

此外save方法通过Python内建的字典将类的元数据保存到字典中,并利用Python的Pickle模块序列化到磁盘上。而load方法则通过类方法,在通过Pickle加载通过save方法保存到磁盘上的数据后显示调用类的初始化方法以实现_HuffmanCoder类的重构。

此外,在保存的时候需要确保保存的文件的路径存在,因此编写ensure_dir函数,而_guess_concat_function则是为支持多种类型的Symbol(字符串、二进制流(串))在编码、解码时候拼接所写的函数,即根据输入的类型来获得拼接方法的函数。

HuffmanCoderFactory类负责:

  • 获得哈夫曼编码表:from_frequencies方法
  • 创建_HuffmanCoder类:from_sequence方法、from_frequencies方法。

其中from_frequencies接受符号-频数键值对并构建哈弗曼树、获得哈夫曼编码表;from_sequence负责统计序列中的符号的频数,由于需要对任意的数据进行压缩,因此符号即指一个字节。

在进行构建的时候,利用优先队列,以出现频率作为优先级进行排列,每次pop都会获得优先队列顶的元素,即出现频率最小的元素。

而队列中的每个元素,则是一个元组,元祖的第一个元素为权值(频率),第二个元素为以该节点为祖宗的叶子节点的二进制编码(以数字形式表现)和对应的比特位数(所处的哈弗曼树的层数/深度)对。使用数字是由于位移操作方便构建哈弗曼编码而记录比特位数则是为了记录开头为0的哈夫曼编码的总位数。

具体流程为只要队列(森林)中的节点多于1个,则首先弹出权值较小的子树a与权值较大的子树b,将子树a与b的频率相加作为新的子树的频率,而后将子树a的所有的叶子节点的深度+1(比特位数),然后在原二进制编码前加上0(维持不变)。而对子树b,将其所有的叶子节点的二进制编码前加上1(将1左移原有比特位数(原有深度),然后加上当前二进制编码),最后将其深度(比特位数)加1。即使用递归的方式完成哈弗曼树的构建与哈夫曼编码的同步获取。

def _guess_concat_function(data):
    """
    Notes:
        To support multiple types of input, e.g., str, bytes, list, there must be a
        function that return the needed concat function
    Returns:
        pass
    """
    return {type(u""): u"".join, type(b""): bytes}.get(type(data), list)


def ensure_dir(path: Union[str, Path]) -> Path:
    path: Path = path if isinstance(path, Path) else Path(path)
    if not path.exists():
        path.mkdir(parents=True)
    assert path.is_dir()
    return path

class _HuffmanCoder:
    """
    Notes:
        _HuffmanCoder, which provide encode, decode, serialize, deserialize, save_code, load_code functionalities.
    Args:

    """

    def __init__(self, code_table: Dict[Any, Tuple[int, int]], concat=list, check=True, eof=_EOF):
        """
        Notes:
            initialize method of _HuffmanCoder with given code table
        Args:
            code_table (Dict[Any, Tuple[int, int]]): code table of all symbols in the encoding sequence, create automatically by
                HuffmanCoderFactory, items are Tuple[Symbol, [depth, bit]]
            concat: concat method of symbols, will be automatically determined in HuffmanCoderFactory
            check: valid code table's format
            eof: eof symbol, leave it alone
        """
        self._table = code_table
        self._concat = concat
        self._eof = eof
        if check:
            assert isinstance(self._table, dict), f"{Fore.RED}Code table need to be a dict!{Style.RESET_ALL}"
            assert all(
                isinstance(b, int) and b >= 1 and isinstance(v, int) and v >= 0 for (b, v) in self._table.values()
            ), f"{Fore.RED}Code table internal format Error!{Style.RESET_ALL}"

    def get_code_table(self) -> Dict[Any, Tuple[int, int]]:
        """
        Notes:
            get_code_table returns code table of input sequence
        Returns:
            self._table
        """
        return self._table

    def print_code_table(self, verbose=False) -> pandas.DataFrame:
        """
        Notes:
            print_code_table is used to print code table
        Args:
            verbose (bool): if True, then print the table in a pretty format
        Returns:
            pd.DataFrame
        """
        pd.options.display.max_rows = None
        columns = list(zip(*list(zip(*itertools.chain(
            [("Symbol", "Huff Code", "BitSize")], (
                (repr(symbol), bin(val)[2:].rjust(bits, "0"), str(bits))
                for symbol, (bits, val) in self._table.items()
            )
        )))))
        df = pd.DataFrame(columns[1:], columns=columns[0])
        if verbose:
            print(df)
        return df

    def _encode_streaming(self, raw_sequence):
        size = 0
        buffer = 0
        for s in raw_sequence:
            bits, values = self._table[s]
            buffer = (buffer << bits) + values
            size += bits
            while size >= 8:
                byte = buffer >> (size - 8)
                yield byte
                buffer = buffer - (byte << (size - 8))
                size -= 8
        if size > 0:
            bit, value = self._table[self._eof]
            buffer = (buffer << bit) + value
            size += bit
            if size > 8:
                byte = buffer >> (size - 8)
            else:
                byte = buffer << (8 - size)
            yield byte

    def _decode_streaming(self, encoded_sequence):
        # Reverse lookup table: map (bitsize, value) to symbols
        lookup = {(b, v): s for s, (b, v) in self._table.items()}

        buffer = 0
        size = 0
        for byte in encoded_sequence:
            for m in [128, 64, 32, 16, 8, 4, 2, 1]:
                buffer = (buffer << 1) + bool(byte & m)
                size += 1
                if (size, buffer) in lookup:
                    symbol = lookup[size, buffer]
                    if symbol == self._eof:
                        return
                    yield symbol
                    buffer = 0
                    size = 0

    def encode(self, raw_seq):
        """
        Notes:
            encode is used to encode the sequence that build the huffman tree
        Args:
            raw_seq: sequence that build the huffman tree
        Returns:
            bytes
        """
        return bytes(self._encode_streaming(raw_seq))

    def decode(self, encoded_seq, concat=None):
        return (concat or self._concat)(self._decode_streaming(encoded_seq))

    def save(self, path: Union[str, Path], metadata: Any = None):
        code_table = self.get_code_table()
        data = {
            "code_table": code_table,
            "type": type(self),
            "concat": self._concat,
        }
        if metadata:
            data['metadata'] = metadata
        path = Path(path)
        ensure_dir(path.parent)
        with path.open(mode='wb') as f:
            pickle.dump(data, file=f)

    @staticmethod
    def load(path: Union[str, Path]) -> '_HuffmanCoder':
        path = Path(path)
        with path.open(mode='rb') as f:
            data = pickle.load(f)
        cls = data['type']
        assert issubclass(cls, _HuffmanCoder)
        code_table = data['code_table']
        return cls(code_table, concat=data['concat'])


class HuffmanCoderFactory(_HuffmanCoder):
    """
    Notes:
        HuffmanCodecFactory is the class that create HuffmanCoder from different type of inputs
    Methods:
        from_frequencies: create HuffmanCoder from frequency table
    """

    @classmethod
    def from_frequencies(cls, frequencies: Dict[Any, int], concat=None, eof=_EOF):
        """
        Notes:
            from_frequencies creates huffman codec by symbol-frequency table/mapping.
        Args:
            frequencies (Dict[Any, int]): Symbols and its frequency, symbols can be str, bytes or int, etc.
            concat (Union[None]): concat method of symbols, will be determined by the function if argument is not
                specified
            eof (_EOFSymbol): leave it alone.
        Returns:
            __HuffmanCoder
        Examples:
            >>> huf_coder = HuffmanCoderFactory.from_frequencies({"a":29, "b":10, "c": 5})
            >>> type(huf_coder)
        """
        concat_function = concat if concat is not None else _guess_concat_function(next(iter(frequencies)))

        # build huffman tree node heap
        # each item: (frequency, [(symbol, (bitsize, value))], value equals which layer of the tree
        heap: List[Tuple[int, List[Tuple[Any, Tuple[int, int]]]]] = [(freq, [(symbol, (0, 0))]) for symbol, freq in
                                                                     frequencies.items()]
        # add eof
        if eof not in frequencies:
            heap.append((1, [(eof, (0, 0))]))

        heapify(heap)
        while len(heap) > 1:
            # get first 2 min as left and right child tree
            a: Tuple[int, List[Tuple[Any, Tuple[int, int]]]] = heappop(heap)
            b: Tuple[int, List[Tuple[Any, Tuple[int, int]]]] = heappop(heap)
            # merge child to form parent
            # parent frequencies adds together, left child add 0 ahead (do nothing) of previous bits
            # right add 1 ahead of previous bits
            merged = (
                a[0] + b[0],
                [(s, (n + 1, v)) for (s, (n, v)) in a[1]] + [(s, (n + 1, (1 << n) + v)) for (s, (n, v)) in b[1]]
            )
            heappush(heap, merged)

        # code table is root
        table = dict(heappop(heap)[1])
        return cls(table, concat=concat, check=False, eof=eof)

    @classmethod
    def from_sequence(cls, sequence: Union[List[Any], Tuple[Any], str, bytes]):
        """
        Notes:
            from_sequence will build a huffman tree from a sequence of symbol
        Args:
            sequence (Union[List, Tuple][Any]):
        Returns:
            __HuffmanCoder
        Examples:
            >>> seq = "a"*100 + "b"*29 + "c"*32
            >>> coder = HuffmanCoderFactory.from_sequence(seq)
        """
        frequencies = collections.Counter(sequence)
        return cls.from_frequencies(frequencies, concat=_guess_concat_function(sequence))

4. 哈夫曼编码层接口

为实现GUI与哈弗曼编码解耦,在前面哈夫曼编码的核心类的基础上,编写哈弗曼编码接口函数,完成编码、解码一步到位

其中:

  • encode负责读取文件的二进制流、压缩、输出压缩时的摘要信息
  • decode负责读取文件的二进制流、解压、输出解压时的摘要信息
def encode(path: Union[str, Path], verbose: bool=True):
    assert (path := Path(path)).exists(), f"{Fore.RED}{path} not exists!{Style.RESET_ALL}"
    with path.open(mode="rb") as f:
        content = f.read()
    coder = HuffmanCoderFactory.from_sequence(content)
    byte = coder.encode(content)
    if not (out_fd := path.resolve().parent / (path.stem + "_huf")).exists():
        out_fd.mkdir(parents=True)
    with (main_path := out_fd.joinpath(path.stem + "_" + path.suffix[1:] + ".huf")).open("wb") as f:
        f.write(byte)
    coder.save(coder_path := out_fd.joinpath(path.stem + "_coder.huf"))

    b1 = path.stat().st_size
    b2 = main_path.stat().st_size
    b3 = coder_path.stat().st_size
    summary1, summary2, summary3, summary4 = [""]*4
    if verbose:
        summary1 = f"Origin file: {path.stem}{path.suffix}, byte size: {b1}"
        summary2 = f"Compressed file: {main_path.stem}{main_path.suffix}, byte size: {b2}"
        summary3 = f"HuffCoder file: {coder_path.stem}{coder_path.suffix}, byte size: {b3}"
        summary4 = f"{Fore.GREEN}Compression rate {b2}/{b1}={Style.BRIGHT}{round(b2/b1, 4)*100}%{Style.NORMAL}" +\
                f", saved {Style.BRIGHT}{b1-b2}{Style.RESET_ALL} bytes"
        print(s := "Summary".center(max([len(summary1), len(summary2), len(summary3), len(summary4)]), "="))
        print("\n".join((summary1, summary2, summary3, summary4)))
        print("="*len(s))
    return b1, b2, b3, coder


def decode(huf_path: Union[Path, str], coder_path: Union[Path, str], verbose: bool=True):
    huf_path, coder_path = Path(huf_path), Path(coder_path)
    coder = _HuffmanCoder.load(coder_path)
    with huf_path.open(mode="rb") as f:
        content = f.read()
    decoded = coder.decode(content)
    with (dec_path := huf_path.parent.joinpath((s := huf_path.stem.split("_"))[0] + f"_decode.{s[1]}")).open(mode="wb") as f:
        f.write(decoded)
    
    b1 = huf_path.stat().st_size
    b2 = coder_path.stat().st_size
    b3 = dec_path.stat().st_size
    if verbose:
        summary1 = f"Origin file: {huf_path.stem}{huf_path.suffix}, byte size: {b1}"
        summary2 = f"Coder file: {coder_path.stem}{coder_path.suffix}, byte size: {b2}"
        summary3 = f"Decompressed file: {dec_path.stem}{dec_path.suffix}, byte size: {b3}"
        summary4 = f"{Fore.GREEN}Decompression rate {b3}/{b1}={Style.BRIGHT}{round(b3/b1, 4)*100}%{Style.NORMAL}" +\
                f", lost {Style.BRIGHT}{b3-b1}{Style.RESET_ALL} bytes"
        print(s := "Summary".center(max([len(summary1), len(summary2), len(summary3), len(summary4)]), "="))
        print("\n".join((summary1, summary2, summary3, summary4)))
        print("="*len(s))
    return b1, b2, b3, coder

5. 用户接口层

用户接口层负责提供命令行用户界面以及GUI用户界面

A. GUI用户界面

GUI用户界面由于会接管程序流的控制,因此以类的形式进行封装。

其中:

  • initUI方法负责窗口的UI设计、按钮等控件的信号对应的槽函数(信号处理函数)的绑定
  • fileDialog方法负责暂时接管程序流,以GUI的形式帮助用户进行文件的选择,以Dialog的形式暂时接管程序运行
  • compress方法作为压缩按钮的槽函数,负责文件的压缩,在进行文件路径检查(文件存在)之后,调用哈夫曼编码层提供的接口函数进行压缩,并将摘要信息输出到程序显示框内
  • decompress方法作为解压按钮的槽函数,负责文件的解压,在进行文件路径检查(压缩文件以及哈夫曼编码表文件均存在)之后,调用哈夫曼编码层提供的接口函数进行解压,并将摘要信息输出到程序显示框内
class QtGUIHuff(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.initUI()

    def initUI(self):
        from Ui_main import Ui_Form
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        self.setWindowTitle("哈夫曼压缩/解压缩程序")
        self.ui.compress.clicked.connect(self.compress)
        self.ui.decompress.clicked.connect(self.decompress)

    def fileDialog(self):
        dialog = QFileDialog()
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setFilter(QDir.Files)
        if dialog.exec():
            filename = dialog.selectedFiles()
            return Path(filename[0])
        else:
            return False

    def compress(self):
        f_path = self.fileDialog()
        self.ui.textEdit.setReadOnly(True)
        s = ["开始压缩"]
        self.ui.textEdit.setText("\n".join(s))
        result = encode(f_path)
        summary1 = f"Origin file: {f_path.stem}{f_path.suffix}, byte size: {result[0]}"
        summary2 = f"Compressed file:  byte size: {result[1]}"
        summary3 = f"HuffCoder file: , byte size: {result[2]}"
        summary4 = f"Compression rate {result[1]}/{result[0]}={round(result[1]/result[0], 4)*100}%" + \
                f", saved {result[0] - result[1]} bytes"
        self.ui.textEdit.setText("\n".join([*s, summary1, summary2, summary3, summary4, "压缩结束"]))
        self.ui.textEdit_2.setText(result[-1].print_code_table(verbose=False).__str__())

    def decompress(self):
        f_path = self.fileDialog()
        if f_path.suffix != ".huf":
            QMessageBox.warning(self, "警告", "请选择正确的压缩文件路径", QMessageBox.Yes, QMessageBox.Yes)
            return False
        
        if len(ps := list(Path(f_path).parent.glob("*.huf"))) == 2:
            print(ps)
            for p_idx in range(len(ps)):
                if "coder" in ps[p_idx].stem:
                    cp_idx = p_idx
        else:
            QMessageBox.warning(self, "警告", "缺少必要的解压缩文件", QMessageBox.Yes, QMessageBox.Yes)
            return False
        s = ["开始解压"]
        import time
        time.sleep(1)
        self.ui.textEdit.setText("\n".join(s))
        result = decode(huf_path:=ps[1-cp_idx], coder_path:=ps[cp_idx])
        summary1 = f"Origin file: {huf_path.stem}{huf_path.suffix}, byte size: {result[0]}"
        summary2 = f"Coder file: {coder_path.stem}{coder_path.suffix}, byte size: {result[1]}"
        summary3 = f"Decompressed file: byte size: {result[2]}"
        summary4 = f"Decompression rate {result[2]}/{result[0]}={round(result[2]/result[0], 4)*100}%" +\
                f", lost {result[2] - result[0]} bytes"
        self.ui.textEdit.setText("\n".join([*s, summary1, summary2, summary3, summary4, "解压结束"]))
        self.ui.textEdit_2.setText(result[-1].print_code_table(verbose=False).__str__())

B. Terminal用户界面

Terminal用户界面则是以argparse库中的ArgumentParser对象完成命令行参数的接受,稍后在主程序中配合完成用户界面。设定了-h参数输出帮助信息、-s对指定路径的文件进行压缩、-t对指定路径的文件进行解压、-g以GUI用户界面运行程序,-v将压缩/解压摘要输出到命令行窗口

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="哈夫曼压缩算法Python实现,可以对任意文件进行压缩,提供命令行以及GUI用户界面。作者:Jack Wang"
    )
    parser.add_argument("-s", "--src", dest="src_path", type=str, default=None, help="需要压缩的文件路径")
    parser.add_argument("-t", "--target", dest="target_path", type=str, default=None, help="需要解压文件所在的文件夹的路径")
    parser.add_argument("-v", "--verbose", dest="v", action="store_true", help="是否显示压缩的摘要信息")
    parser.add_argument("-g", "--gui", dest="gui", action="store_true", help="是否以GUI方式显示")
    return parser.parse_args()

6. 主程序

主程序很简单,根据接受的命令行参数调用不同的接口即可

在非图形界面的运行的时候,程序的主控制流以程序为主,而以GUI方式运行的时候,程序的控制流则在Qt框架下,用户级线程调度、事件监听、信号分发、槽函数(信号处理函数)唤醒等均由框架负责。

def main(args: argparse.Namespace):
    if not args.gui:
        assert 0 < int(args.src_path is not None) + int(args.target_path is not None), f"{Fore.RED}请选择压缩或者解压,使用-h查询用法{Style.RESET_ALL}"
    if not args.gui:
        print(f"{Fore.GREEN}命令行工作模式{Style.RESET_ALL}")
        if args.src_path is not None:
            print(f"开始压缩 {args.src_path}")
            if args.v:
                print(encode(Path(args.src_path))[-1].print_code_table(verbose=False))
            else:
                encode(Path(args.src_path), verbose=False)
            print("压缩完成")
        elif args.target_path is not None:
            if len(ps := list(Path(args.target_path).glob("*.huf"))) == 2:
                print(ps)
                for p_idx in range(len(ps)):
                    if "coder" in ps[p_idx].stem:
                        cp_idx = p_idx
            else:
                assert False, f"{Fore.RED}无效的路径,没有找到压缩文件{Style.RESET_ALL}"
            print(f"开始解压 {args.target_path}")
            if args.v:
                print(decode(huf_path=ps[1-cp_idx], coder_path=ps[cp_idx])[-1].print_code_table(verbose=False))
            else:
                decode(huf_path=ps[1-cp_idx], coder_path=ps[cp_idx], verbose=False)
            print("解压完成")
    else:
        app = QApplication(sys.argv)
        m = QtGUIHuff()
        m.show()
        sys.exit(app.exec_())




if __name__ == "__main__":
    main(parse_args())

5. 运行实例

A. GIF版演示

这里先放一个gif图片(比较大,44兆,不知道能不能加载出来),高清版本去下面的B站视频里看看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CYNjOkb4-1669723754698)(https://jack-1307599355.cos.ap-shanghai.myqcloud.com/img/哈夫曼算法压缩任意文件演示.gif)]

B. B站高清版

视频链接:https://www.bilibili.com/video/BV1cL41177zL/

6. 完整代码

完整代码如下

A. huff.py

import sys
import pickle
import argparse
import itertools
import collections
from pathlib import Path
from heapq import heapify, heappush, heappop
from typing import Union, Any, Dict, List, Tuple
from PyQt5.QtCore import QDir

import pandas
import pandas as pd
from colorama import Fore, Style
from PyQt5.QtWidgets import *

__author__ = "Jack Wang"
__date__ = "2021/12/19"
sys.path.append(Path(__file__).resolve().parent.joinpath("./ui").__str__())


class _EOFSymbol:
    """
    Notes:
        _EOFSymbol 是内置的文件结尾的类,用于在编码的时候标志文件的结尾。
        由于在进行哈夫曼树的构建的时候需要选出来权最小的节点,因此需要重写比较的魔术方法。
        默认文档结尾是频率最少的字符,因此和任何数字比较
    """

    def __repr__(self):
        return "_EOF"

    def __lt__(self, other):
        return True

    def __gt__(self, other):
        return False

    def __eq__(self, other):
        return self.__class__ == other.__class__

    def __hash__(self):
        return hash(self.__class__)


_EOF = _EOFSymbol()


def _guess_concat_function(data):
    """
    Notes:
        To support multiple types of input, e.g., str, bytes, list, there must be a
        function that return the needed concat function
    Returns:
        pass
    """
    return {type(u""): u"".join, type(b""): bytes}.get(type(data), list)


def ensure_dir(path: Union[str, Path]) -> Path:
    path: Path = path if isinstance(path, Path) else Path(path)
    if not path.exists():
        path.mkdir(parents=True)
    assert path.is_dir()
    return path


class _HuffmanCoder:
    """
    Notes:
        _HuffmanCoder, which provide encode, decode, serialize, deserialize, save_code, load_code functionalities.
    Args:

    """

    def __init__(self, code_table: Dict[Any, Tuple[int, int]], concat=list, check=True, eof=_EOF):
        """
        Notes:
            initialize method of _HuffmanCoder with given code table
        Args:
            code_table (Dict[Any, Tuple[int, int]]): code table of all symbols in the encoding sequence, create automatically by
                HuffmanCoderFactory, items are Tuple[Symbol, [depth, bit]]
            concat: concat method of symbols, will be automatically determined in HuffmanCoderFactory
            check: valid code table's format
            eof: eof symbol, leave it alone
        """
        self._table = code_table
        self._concat = concat
        self._eof = eof
        if check:
            assert isinstance(self._table, dict), f"{Fore.RED}Code table need to be a dict!{Style.RESET_ALL}"
            assert all(
                isinstance(b, int) and b >= 1 and isinstance(v, int) and v >= 0 for (b, v) in self._table.values()
            ), f"{Fore.RED}Code table internal format Error!{Style.RESET_ALL}"

    def get_code_table(self) -> Dict[Any, Tuple[int, int]]:
        """
        Notes:
            get_code_table returns code table of input sequence
        Returns:
            self._table
        """
        return self._table

    def print_code_table(self, verbose=False) -> pandas.DataFrame:
        """
        Notes:
            print_code_table is used to print code table
        Args:
            verbose (bool): if True, then print the table in a pretty format
        Returns:
            pd.DataFrame
        """
        pd.options.display.max_rows = None
        columns = list(zip(*list(zip(*itertools.chain(
            [("Symbol", "Huff Code", "BitSize")], (
                (repr(symbol), bin(val)[2:].rjust(bits, "0"), str(bits))
                for symbol, (bits, val) in self._table.items()
            )
        )))))
        df = pd.DataFrame(columns[1:], columns=columns[0])
        if verbose:
            print(df)
        return df

    def _encode_streaming(self, raw_sequence):
        size = 0
        buffer = 0
        for s in raw_sequence:
            bits, values = self._table[s]
            buffer = (buffer << bits) + values
            size += bits
            while size >= 8:
                byte = buffer >> (size - 8)
                yield byte
                buffer = buffer - (byte << (size - 8))
                size -= 8
        if size > 0:
            bit, value = self._table[self._eof]
            buffer = (buffer << bit) + value
            size += bit
            if size > 8:
                byte = buffer >> (size - 8)
            else:
                byte = buffer << (8 - size)
            yield byte

    def _decode_streaming(self, encoded_sequence):
        # Reverse lookup table: map (bitsize, value) to symbols
        lookup = {(b, v): s for s, (b, v) in self._table.items()}

        buffer = 0
        size = 0
        for byte in encoded_sequence:
            for m in [128, 64, 32, 16, 8, 4, 2, 1]:
                buffer = (buffer << 1) + bool(byte & m)
                size += 1
                if (size, buffer) in lookup:
                    symbol = lookup[size, buffer]
                    if symbol == self._eof:
                        return
                    yield symbol
                    buffer = 0
                    size = 0

    def encode(self, raw_seq):
        """
        Notes:
            encode is used to encode the sequence that build the huffman tree
        Args:
            raw_seq: sequence that build the huffman tree
        Returns:
            bytes
        """
        return bytes(self._encode_streaming(raw_seq))

    def decode(self, encoded_seq, concat=None):
        return (concat or self._concat)(self._decode_streaming(encoded_seq))

    def save(self, path: Union[str, Path], metadata: Any = None):
        code_table = self.get_code_table()
        data = {
            "code_table": code_table,
            "type": type(self),
            "concat": self._concat,
        }
        if metadata:
            data['metadata'] = metadata
        path = Path(path)
        ensure_dir(path.parent)
        with path.open(mode='wb') as f:
            pickle.dump(data, file=f)

    @staticmethod
    def load(path: Union[str, Path]) -> '_HuffmanCoder':
        path = Path(path)
        with path.open(mode='rb') as f:
            data = pickle.load(f)
        cls = data['type']
        assert issubclass(cls, _HuffmanCoder)
        code_table = data['code_table']
        return cls(code_table, concat=data['concat'])


class HuffmanCoderFactory(_HuffmanCoder):
    """
    Notes:
        HuffmanCodecFactory is the class that create HuffmanCoder from different type of inputs
    Methods:
        from_frequencies: create HuffmanCoder from frequency table
    """

    @classmethod
    def from_frequencies(cls, frequencies: Dict[Any, int], concat=None, eof=_EOF):
        """
        Notes:
            from_frequencies creates huffman codec by symbol-frequency table/mapping.
        Args:
            frequencies (Dict[Any, int]): Symbols and its frequency, symbols can be str, bytes or int, etc.
            concat (Union[None]): concat method of symbols, will be determined by the function if argument is not
                specified
            eof (_EOFSymbol): leave it alone.
        Returns:
            __HuffmanCoder
        Examples:
            >>> huf_coder = HuffmanCoderFactory.from_frequencies({"a":29, "b":10, "c": 5})
            >>> type(huf_coder)
        """
        concat_function = concat if concat is not None else _guess_concat_function(next(iter(frequencies)))

        # build huffman tree node heap
        # each item: (frequency, [(symbol, (bitsize, value))], value equals which layer of the tree
        heap: List[Tuple[int, List[Tuple[Any, Tuple[int, int]]]]] = [(freq, [(symbol, (0, 0))]) for symbol, freq in
                                                                     frequencies.items()]
        # add eof
        if eof not in frequencies:
            heap.append((1, [(eof, (0, 0))]))

        heapify(heap)
        while len(heap) > 1:
            # get first 2 min as left and right child tree
            a: Tuple[int, List[Tuple[Any, Tuple[int, int]]]] = heappop(heap)
            b: Tuple[int, List[Tuple[Any, Tuple[int, int]]]] = heappop(heap)
            # merge child to form parent
            # parent frequencies adds together, left child add 0 ahead (do nothing) of previous bits
            # right add 1 ahead of previous bits
            merged = (
                a[0] + b[0],
                [(s, (n + 1, v)) for (s, (n, v)) in a[1]] + [(s, (n + 1, (1 << n) + v)) for (s, (n, v)) in b[1]]
            )
            heappush(heap, merged)

        # code table is root
        table = dict(heappop(heap)[1])
        return cls(table, concat=concat, check=False, eof=eof)

    @classmethod
    def from_sequence(cls, sequence: Union[List[Any], Tuple[Any], str, bytes]):
        """
        Notes:
            from_sequence will build a huffman tree from a sequence of symbol
        Args:
            sequence (Union[List, Tuple][Any]):
        Returns:
            __HuffmanCoder
        Examples:
            >>> seq = "a"*100 + "b"*29 + "c"*32
            >>> coder = HuffmanCoderFactory.from_sequence(seq)
        """
        frequencies = collections.Counter(sequence)
        return cls.from_frequencies(frequencies, concat=_guess_concat_function(sequence))


def encode(path: Union[str, Path], verbose: bool=True):
    assert (path := Path(path)).exists(), f"{Fore.RED}{path} not exists!{Style.RESET_ALL}"
    with path.open(mode="rb") as f:
        content = f.read()
    coder = HuffmanCoderFactory.from_sequence(content)
    byte = coder.encode(content)
    if not (out_fd := path.resolve().parent / (path.stem + "_huf")).exists():
        out_fd.mkdir(parents=True)
    with (main_path := out_fd.joinpath(path.stem + "_" + path.suffix[1:] + ".huf")).open("wb") as f:
        f.write(byte)
    coder.save(coder_path := out_fd.joinpath(path.stem + "_coder.huf"))

    b1 = path.stat().st_size
    b2 = main_path.stat().st_size
    b3 = coder_path.stat().st_size
    summary1, summary2, summary3, summary4 = [""]*4
    if verbose:
        summary1 = f"Origin file: {path.stem}{path.suffix}, byte size: {b1}"
        summary2 = f"Compressed file: {main_path.stem}{main_path.suffix}, byte size: {b2}"
        summary3 = f"HuffCoder file: {coder_path.stem}{coder_path.suffix}, byte size: {b3}"
        summary4 = f"{Fore.GREEN}Compression rate {b2}/{b1}={Style.BRIGHT}{round(b2/b1, 4)*100}%{Style.NORMAL}" +\
                f", saved {Style.BRIGHT}{b1-b2}{Style.RESET_ALL} bytes"
        print(s := "Summary".center(max([len(summary1), len(summary2), len(summary3), len(summary4)]), "="))
        print("\n".join((summary1, summary2, summary3, summary4)))
        print("="*len(s))
    return b1, b2, b3, coder


def decode(huf_path: Union[Path, str], coder_path: Union[Path, str], verbose: bool=True):
    huf_path, coder_path = Path(huf_path), Path(coder_path)
    coder = _HuffmanCoder.load(coder_path)
    with huf_path.open(mode="rb") as f:
        content = f.read()
    decoded = coder.decode(content)
    with (dec_path := huf_path.parent.joinpath((s := huf_path.stem.split("_"))[0] + f"_decode.{s[1]}")).open(mode="wb") as f:
        f.write(decoded)
    
    b1 = huf_path.stat().st_size
    b2 = coder_path.stat().st_size
    b3 = dec_path.stat().st_size
    if verbose:
        summary1 = f"Origin file: {huf_path.stem}{huf_path.suffix}, byte size: {b1}"
        summary2 = f"Coder file: {coder_path.stem}{coder_path.suffix}, byte size: {b2}"
        summary3 = f"Decompressed file: {dec_path.stem}{dec_path.suffix}, byte size: {b3}"
        summary4 = f"{Fore.GREEN}Decompression rate {b3}/{b1}={Style.BRIGHT}{round(b3/b1, 4)*100}%{Style.NORMAL}" +\
                f", lost {Style.BRIGHT}{b3-b1}{Style.RESET_ALL} bytes"
        print(s := "Summary".center(max([len(summary1), len(summary2), len(summary3), len(summary4)]), "="))
        print("\n".join((summary1, summary2, summary3, summary4)))
        print("="*len(s))
    return b1, b2, b3, coder



def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="哈夫曼压缩算法Python实现,可以对任意文件进行压缩,提供命令行以及GUI用户界面。作者:Jack Wang"
    )
    parser.add_argument("-s", "--src", dest="src_path", type=str, default=None, help="需要压缩的文件路径")
    parser.add_argument("-t", "--target", dest="target_path", type=str, default=None, help="需要解压文件所在的文件夹的路径")
    parser.add_argument("-v", "--verbose", dest="v", action="store_true", help="是否显示压缩的摘要信息")
    parser.add_argument("-g", "--gui", dest="gui", action="store_true", help="是否以GUI方式显示")
    return parser.parse_args()


class QtGUIHuff(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.initUI()

    def initUI(self):
        from Ui_main import Ui_Form
        self.ui = Ui_Form()
        self.ui.setupUi(self)
        self.setWindowTitle("哈夫曼压缩/解压缩程序")
        self.ui.compress.clicked.connect(self.compress)
        self.ui.decompress.clicked.connect(self.decompress)

    def fileDialog(self):
        dialog = QFileDialog()
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setFilter(QDir.Files)
        if dialog.exec():
            filename = dialog.selectedFiles()
            return Path(filename[0])
        else:
            return False

    def compress(self):
        f_path = self.fileDialog()
        self.ui.textEdit.setReadOnly(True)
        s = ["开始压缩"]
        self.ui.textEdit.setText("\n".join(s))
        result = encode(f_path)
        summary1 = f"Origin file: {f_path.stem}{f_path.suffix}, byte size: {result[0]}"
        summary2 = f"Compressed file:  byte size: {result[1]}"
        summary3 = f"HuffCoder file: , byte size: {result[2]}"
        summary4 = f"Compression rate {result[1]}/{result[0]}={round(result[1]/result[0], 4)*100}%" + \
                f", saved {result[0] - result[1]} bytes"
        self.ui.textEdit.setText("\n".join([*s, summary1, summary2, summary3, summary4, "压缩结束"]))
        self.ui.textEdit_2.setText(result[-1].print_code_table(verbose=False).__str__())

    def decompress(self):
        f_path = self.fileDialog()
        if f_path.suffix != ".huf":
            QMessageBox.warning(self, "警告", "请选择正确的压缩文件路径", QMessageBox.Yes, QMessageBox.Yes)
            return False
        
        if len(ps := list(Path(f_path).parent.glob("*.huf"))) == 2:
            print(ps)
            for p_idx in range(len(ps)):
                if "coder" in ps[p_idx].stem:
                    cp_idx = p_idx
        else:
            QMessageBox.warning(self, "警告", "缺少必要的解压缩文件", QMessageBox.Yes, QMessageBox.Yes)
            return False
        s = ["开始解压"]
        import time
        time.sleep(1)
        self.ui.textEdit.setText("\n".join(s))
        result = decode(huf_path:=ps[1-cp_idx], coder_path:=ps[cp_idx])
        summary1 = f"Origin file: {huf_path.stem}{huf_path.suffix}, byte size: {result[0]}"
        summary2 = f"Coder file: {coder_path.stem}{coder_path.suffix}, byte size: {result[1]}"
        summary3 = f"Decompressed file: byte size: {result[2]}"
        summary4 = f"Decompression rate {result[2]}/{result[0]}={round(result[2]/result[0], 4)*100}%" +\
                f", lost {result[2] - result[0]} bytes"
        self.ui.textEdit.setText("\n".join([*s, summary1, summary2, summary3, summary4, "解压结束"]))
        self.ui.textEdit_2.setText(result[-1].print_code_table(verbose=False).__str__())



def main(args: argparse.Namespace):
    if not args.gui:
        assert 0 < int(args.src_path is not None) + int(args.target_path is not None), f"{Fore.RED}请选择压缩或者解压,使用-h查询用法{Style.RESET_ALL}"
    if not args.gui:
        print(f"{Fore.GREEN}命令行工作模式{Style.RESET_ALL}")
        if args.src_path is not None:
            print(f"开始压缩 {args.src_path}")
            if args.v:
                print(encode(Path(args.src_path))[-1].print_code_table(verbose=False))
            else:
                encode(Path(args.src_path), verbose=False)
            print("压缩完成")
        elif args.target_path is not None:
            if len(ps := list(Path(args.target_path).glob("*.huf"))) == 2:
                print(ps)
                for p_idx in range(len(ps)):
                    if "coder" in ps[p_idx].stem:
                        cp_idx = p_idx
            else:
                assert False, f"{Fore.RED}无效的路径,没有找到压缩文件{Style.RESET_ALL}"
            print(f"开始解压 {args.target_path}")
            if args.v:
                print(decode(huf_path=ps[1-cp_idx], coder_path=ps[cp_idx])[-1].print_code_table(verbose=False))
            else:
                decode(huf_path=ps[1-cp_idx], coder_path=ps[cp_idx], verbose=False)
            print("解压完成")
    else:
        app = QApplication(sys.argv)
        m = QtGUIHuff()
        m.show()
        sys.exit(app.exec_())




if __name__ == "__main__":
    main(parse_args())

2. Ui_main.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file '/media/jack/JackCode/project/data_structure/11_huffman/ui/main.ui'
#
# Created by: PyQt5 UI code generator 5.9.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(751, 374)
        self.layoutWidget = QtWidgets.QWidget(Form)
        self.layoutWidget.setGeometry(QtCore.QRect(50, 20, 641, 321))
        self.layoutWidget.setObjectName("layoutWidget")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget)
        self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.verticalLayout = QtWidgets.QVBoxLayout()
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.compress = QtWidgets.QPushButton(self.layoutWidget)
        self.compress.setObjectName("compress")
        self.horizontalLayout.addWidget(self.compress)
        self.decompress = QtWidgets.QPushButton(self.layoutWidget)
        self.decompress.setObjectName("decompress")
        self.horizontalLayout.addWidget(self.decompress)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.label = QtWidgets.QLabel(self.layoutWidget)
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.textEdit = QtWidgets.QTextEdit(self.layoutWidget)
        self.textEdit.setObjectName("textEdit")
        self.verticalLayout.addWidget(self.textEdit)
        self.horizontalLayout_2.addLayout(self.verticalLayout)
        self.verticalLayout_2 = QtWidgets.QVBoxLayout()
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.label_2 = QtWidgets.QLabel(self.layoutWidget)
        self.label_2.setAlignment(QtCore.Qt.AlignCenter)
        self.label_2.setObjectName("label_2")
        self.verticalLayout_2.addWidget(self.label_2)
        self.textEdit_2 = QtWidgets.QTextEdit(self.layoutWidget)
        self.textEdit_2.setObjectName("textEdit_2")
        self.verticalLayout_2.addWidget(self.textEdit_2)
        self.horizontalLayout_2.addLayout(self.verticalLayout_2)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.compress.setText(_translate("Form", "压缩"))
        self.decompress.setText(_translate("Form", "解压"))
        self.label.setText(_translate("Form", "Compress/Decompress Log"))
        self.label_2.setText(_translate("Form", "Huffman Tree"))

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

哈夫曼压缩算法-Python实现 的相关文章

  • 网络通讯学习(3)-----UDP通讯(仅了解)

    理论 UDP xff08 用户数据报协议 xff09 是一个无连接 xff0c 不可靠的数据传输 xff0c 其特点是简单 xff0c 快捷 相比与TCP xff0c UDP不需要建立连接 xff08 不需connect accept函数
  • WIFI模块不支持MQTT协议,可通过MCU实现

    1 话题原因 我们使用某款WIFI模块 xff0c 在物联网开发时 xff0c 平台端的开发者想要使用MQTT协议 xff0c 但是我们当前使用的模块不支持MQTT协议 xff08 好像ESP8266可以通过重新烧录固件的方式支持 xff0
  • (一) 路径规划算法---Astar与C++可视化在RVIZ的三维点云地图

    Astar与C 43 43 可视化在RVIZ的三维点云地图 文章目录 Astar与C 43 43 可视化在RVIZ的三维点云地图1 功能包介绍2 算法功能包的组成与介绍2 1文件系统组成2 2 头文件说明2 3 源文件说明 3 相关坐标系说
  • SpringSecurity整合OAuth2.0

    SpringSecurity整合OAuth2 一 概述与原理1 1 OAuth2 0 是什么 xff1f 1 2 OAuth2 0中角色解释1 3 OAuth2 0的4中授权模式1 3 1 授权码模式 xff08 重点 xff09 1 3
  • HAL_UART_IRQHandler(UART_HandleTypeDef *huart)里面的中断接收函数

    目录 前言1 UART Receive IT2 HAL UART Receive3 HAL UART Receive IT 前言 看了很长时间串口中断的HAL库 xff0c 最容易混淆的就是函数的名称 xff0c 主要集中在UART Rec
  • 位操作读写寄存器一个字节的多个位

    一 写寄存器多个位 方法一 span class token comment bitStart 目标字节的起始位 length 位长度 data 存放改变目标字节位的值 b 写入后的一个字节值 span u8 mask
  • STM32 电压采集上位机 C#

    工具箱中添加progressBar 添加一个事件函数 xff0c 用于串口接收数据 xff0c 并显示电压值 private void PortDataReceivedEvent object sender SerialDataReceiv
  • cmake使用教程(一)多目录下多个文件的构建

    1 采用 out of source 外部构建多个目录多个文件 这里的文件存储方式如下 xff1a 其中build是构建目录 xff0c 即构建的结果和中间产物都在该目录下 include是包含目录 src是子函数的目录或是依赖文件的目录
  • vue 实现遍历后端接口数据并展示在表格中

    用前端的vue遍历接口 首先就需要有后端的JSON数据 这里可以自己去写接口 可以伪造JSON数据 整理是伪造的JSON数据 34 userId 34 1 34 deptId 34 103 34 userName 34 34 admin 3
  • STM32的存储器映射中的指针操作

    例如 xff1a GPIOB的端口输出数据寄存器ODR的地址是0x4001 0C0C 并且ODR寄存器是32位的 那么我们可以用如下代码对寄存器进行操作 xff1a unsigned int 0x4001 0C0C 61 0xFFFFFFF
  • Mac 启动Redis报错 没有指定的conf文件

    报错如下 xff1a Warning no config file specified span class token punctuation span span class token keyword using span the de
  • java 优化双重for循环

    首先我们要有两个对象分别是 学生信息 和 学生住宿信息 span class token keyword class span span class token class name Student span span class toke
  • 微服务 - gateway网关配置

    server port 10010 网关端口 spring application name gateway 服务名称 cloud nacos server addr localhost 8848 nacos地址 gateway route
  • 如何在手机或平板上编写代码?

    下面给大家推荐一款免费的 在线协作式 基于浏览器的 IDE的在线编程网站 支持语言包括 Java C 43 43 C C JavaScript CSS PHP等50多种主流开发语言 地址 The collaborative browser
  • 羊了个羊, 低配版开源代码来啦~

    前几天朋友圈突然被一个小游戏 羊了个羊 刷屏了 xff0c 出于好奇我也打算小玩一把试试 xff0c 结果没想到上头了 游戏的玩法非常简单 xff0c 类似 消消乐 xff0c 从一堆方块中找到相同图案的 3 个方块并消除即可 但没想到 x
  • MySQL 使用索引和不使用索引的区别(附17W条数据SQL文件)

    MySQL 使用索引可以减少查询的时间 xff0c 而不使用索引的查询会更加耗时 xff0c 因为MySQL需要扫描整个表 此外 xff0c 使用索引可以提高查询的性能 xff0c 同时也可以提高查询的可读性和可维护性 换句话来说 使用索引
  • 如何使用AI来帮你写代码(Cursor使用教程)

    x1f4ac 产品介绍 cursor是一个新的Ide xff0c 它使用Ai来帮助您重构理解调试并使用Cursor编写代码我们的目标是使构建软件的过程更快 更愉快 我们从头开始构建了一个代码编辑器 xff0c 对我们的第一个功能进行了原型设
  • [Java多线程-基础] 如何定位线程中的死锁问题?

    x1f512 死锁代码 下面提供的代码演示了死锁的情况 程序创建了两个线程 xff0c 线程1和线程2 xff0c 它们都试图以不同的顺序获取两个不同的资源 xff0c resource1和resource2 线程1首先获取resource
  • [Java多线程-基础] 避免线程死锁问题(ReentrantLock的使用)

    ReentrantLock 的设计初衷是为了提供一种比 synchronized 更加灵活和可控的锁机制 与 synchronized 相比 xff0c ReentrantLock 提供了更多的功能 xff0c 如可重入性 公平锁和中断锁等
  • IDEA插件:智能代码生成器,附带注释和性能/安全检测功能

    x1f680 1 安装插件 在插件中搜索关键字 biot 点击安装 x1f680 2 代码生成 右侧的侧边栏点击biot后 在下方的输入框中输入你要问的内容 x1f680 3 biot AI 选中选区中的代码 点击鼠标右键让ai来帮你改代码

随机推荐

  • 安装Windows Server 2016 服务器 标准版

    注意事项 xff1a 安装带桌面版的 管理员密码设置 xff0c 要 注意大小写加数字 xff0c 不然会设置失败 安装文件下载 xff1a MSDN 我告诉你 PE U盘 微PE 服务器的驱动 xff0c 可以自己到对应服务器厂家的官网上
  • 第五节:基于Pytorch的相关可视化

    第五节 xff1a 基于Pytorch的相关可视化 在Pytorch发布后 xff0c 网络及训练过程的可视化工具也相应的被开发出来来帮助用户监督所建立的模型的结构和训练过程 本章将讲解HiddenLayer库 xff0c HiddenLa
  • 第六节:Pytorch实现全连接神经网络

    第六节 xff1a Pytorch实现全连接神经网络 前面的五节中 xff0c 我们讲解了使用PyTorch搭建一个神经网络中需要的需要各种技巧 xff0c 包括 xff1a 网络的搭建 选择不同的实践技巧 xff08 优化器选择 学习率下
  • 使用Visual Studio Code开发Arduino踩坑日记(持续更新)

    使用Visual Studio Code开发Arduino踩坑日记 持续更新 文章目录 使用Visual Studio Code开发Arduino踩坑日记 持续更新 1 在browse path中未找到包含文件问题描述问题分析解决思路解决过
  • 小白安装Ubuntu 18.04 LTS

    文章目录 小白安装Ubuntu 18 04 LTS作者 xff1a 王仕鸿日期 xff1a 2020 10 10 前言 xff08 可跳过 xff09 Ubuntu介绍操作系统介绍Ubuntu介绍 安装Ubuntu 18 04 LTS步骤一
  • 1_ROS基础

    ROS基础 本章讲解ROS中最基础的概念 不明白这些概念是没法学懂ROS的 学习了这些概念 后面我们将通过实操来在实践的过程中进一步体会 ROS是什么 ROS Robot Operating System 机器人操作系统 是一个提供一系列程
  • 2_ROS中的命令行工具

    ROS中的命令行工具 ROS中为我们提供了丰富的命令行工具 帮助我们进行代码的编写 调试 测试 框架的搭建 数据的显示等等 大图如下 所有的命令大致可以分为四类 分别是运行相关命令 编译相关命令 包制作管理相关命令 项目创建相关命令 下面进
  • 3_ROS创建工作空间和功能包

    3 ROS创建工作空间和功能包 前面我们讲解了ROS中的核心概念和使用ROS进行开发时候必须用到的命令行工具 下面我们就正式开始ROS中的开发 我们首先从创建工作空间和功能包开始 1 工作空间WorkSpace 工作空间是ROS中非常重要的
  • 4_Publieher的编程实现

    4 Publisher的编程实现 我们前面讲解了如何创建工作空间和功能包 但是我们都仅仅只创建了一个空的工作空间和功能包 什么都没有实现 我们想要进一步为功能包添加功能 就不可避免的需要添加Publisher和Subscriber 下面我们
  • 1.Latex介绍

    Latex介绍 本人鸿神 目前就读于XJTU 是一个即将开始科研的小白 既然做科研未来就无法避免发表论文 而发表论文就需要用到一系列的工具 Latex就是其中之一 谨以此文记录我的科研路 也希望Latex这一系列文章能够帮到各位 1 什么是
  • 2.Latex安装和TeXworks Editor基础

    二 Latex安装和TeXworks Editor使用教程 上一章我们讲解了什么是Latex和为什么我们要学习Latex 从这一章开始我们就要正式开始学习Latex 就像前面所讲的 Latex包含编译器和编辑器 我们需要在编辑器中编写夹杂代
  • 关于“ErrorFlash Download failed“Cortex-M3”的解决办法

    首先 xff0c 将仿真器连接电脑 xff0c 然后打开KEIL xff0c 点击FLash gt Erase xff0c 擦除Flash试一下 如果擦除不成功 xff0c 那么应该是的STM32的Flash被锁了 xff0c 要解锁一下
  • 3.Latex语法基础:命令与环境

    三 Latex语法基础 命令与环境 前面我们已经做好了开始编写Latex的一切准备工作 从这章开始 我们将开始讲解Latex语法 本章将讲解Latex语法的基础 命令与环境 1 命令与环境 命令 什么是命令 不同于其他编程语言 C C 43
  • Arduino多种传感器使用方法

    Arduino项目 智能窗户 前段时间参加了一个Arduino的比赛 具体内容就是用Arduino设计一个项目出来 我在的队伍的设计的项目就是智能窗户 智能窗户可以采集本地传感器采集到的环境参数 根据参数具有一套内部的逻辑判断 可以对温度
  • Paxos算法

    Paxos算法 Paxos算法是一系列共识算法中的一个 其目的就是为了解决共识 一致性问题 这个Github连接中详细的列出了多种共识算法 还有一些工程实践的例子 腾讯 Zookeeper Handpoo下的一个分布式框架 Handoop是
  • Python中的推导式

    推导式 Compression 推导式 Compression 是Python语言的一大特色 相比于其他语言而言 推导式使得Python能够编辑的进行循环 创建出特定的字典 列表等可迭代对象 使用推导式可以避免代码的冗长 简化代码风格 使得
  • A* 算法 Python实现

    A 算法 Python实现 最近考试周 没时间封装的很好 代码分两部分 定义和调用部分 定义部分 span class token keyword import span numpy span class token keyword as
  • Pytorch中使用TensorBoard

    本文记录了如何在Pytorch中使用Tensorboard xff08 主要是为了备忘 xff09 Pytorch中使用TensorBoard 虽然我本身就会用TensorBoard xff0c 但是因为TensorBoard只有在写训练代
  • A*算法-Python实现

    好久没有在CSDN上发文章了 xff0c 快一年了吧 这两天重新登录了一下 xff0c 不看不知道 xff0c 一看吓一跳 xff0c 没想到访问量快13万了 之前写博客的时候 xff0c 想着把一些有用的东西写下来 xff0c 一方面是当
  • 哈夫曼压缩算法-Python实现

    前面上传了A 算法的实现 xff0c 就顺便把一起写的哈夫曼压缩也上传了吧 本文主要提供了Python版本的哈夫曼压缩算法实现 xff0c 并在此基础上提供了命令行和基于Qt的GUI用户界面 xff08 User Interface xff