一、 题目
赫夫曼编译码器
二、 实验目的
- 掌握赫夫曼编码原理。
- 熟练掌握赫夫曼树的生成方法。
- 理解数据编码压缩和译码输出编码的实现。
三、需求分析
- 初始化(Initialization)。从终端读入字符集大小n,以及n个字符和n个权值,建立赫夫曼树,并将它存于文件hfmTree中。
- 编码(Encoding)。利用已建好的赫夫曼树(如不在内存,则从文件hfmTree中读入),对文件ToBeTran中的正文进行编码,然后将结果存入文件CodeFile中。
- 译码(Decoding)。利用已建好的赫夫曼树将文件CodeFile中的代码进行译码,结果存入文件Textfile中。
- 打印代码文件(Print)。将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。
- 打印赫夫曼树(Tree printing)。将已在内存中的赫夫曼树以直观的方式(比如树)显示在终端上,同时将此字符形式的赫夫曼树写入文件TreePrint 中。
- 网络通信(Network)。通过赫夫曼编码的形式作为加密方式,进行网络通信。
四、概要设计
五、程序说明
- 使用源文件运行
① 先安装PyQt5,graphviz 这两个库以及从https://graphviz.gitlab.io/download/安装dot
② 再打开该目录下的命令行,输入命令:python 哈夫曼编译码器.py
注:python版本要大于3.8 - 使用打包好的exe文件
双击 哈夫曼编译码器.exe 即可
但这两种方式中,都须将对应文件与ui文件夹置于同一目录下
六、详细设计
(一)初始化(Initialization)
初始化有两种形式一种是直接根据原文生成,另一种是根据通过直接导入字符,而在这两种方法的基础上,还可对列表内的字符集进行增删改查,而对于现存的字符集还可进行保存文件这一操作,最终,在关闭该窗口时,程序会根据现有的字符集来进行建树
- 根据原文生成
def add(self):
# 加入一空行
self.tableWidget.insertRow(self.tableWidget.rowCount())
def generateCharacterSetFromRawtext(self):
# 根据原文生成字符集
self.tableWidget.clearContents()
self.tableWidget.setRowCount(0)
def getFrequency(text: str) -> dict:
# 字频(统计)
cnt = {}
for i in text:
if i not in cnt:
cnt[i] = 1
else:
cnt[i] += 1
return cnt
CharacterSet = getFrequency(rawTextEdit.toPlainText())
for i, j in CharacterSet.items():
self.add()
item1 = QTableWidgetItem(i)
item2 = QTableWidgetItem(str(j))
self.tableWidget.setItem(self.tableWidget.rowCount()-1, 0, item1)
self.tableWidget.setItem(self.tableWidget.rowCount()-1, 1, item2)
- 直接导入字符
def importWordFrequency(self):
# 导入字频
filePath, ok = QFileDialog.getOpenFileName(self, '选择文件')
if ok:
self.tableWidget.clearContents()
self.tableWidget.setRowCount(0)
with open(filePath, 'r', encoding='utf-8') as file:
try:
frequency = file.read()
except UnicodeDecodeError:
QMessageBox.critical(
self, "错误", "请确保打开的是UTF-8编码的文本文件", QMessageBox.OK)
return
global CharacterSet
CharacterSet = {}
textlines = re.findall(r'([\s\S])\t(\S+)(\n|$)', frequency)
if len(textlines) == 0:
QMessageBox.critical(self, "错误", "字符集生成失败", QMessageBox.Ok)
return
for i, j, _ in textlines:
try:
CharacterSet[i] = float(j)
except ValueError:
QMessageBox.critical(
self, "错误", "字符集生成失败", QMessageBox.Ok)
self.tableWidget.clearContents()
self.tableWidget.setRowCount(0)
CharacterSet = {}
return
self.add()
item1 = QTableWidgetItem(i)
item2 = QTableWidgetItem(j)
self.tableWidget.setItem(
self.tableWidget.rowCount()-1, 0, item1)
self.tableWidget.setItem(
self.tableWidget.rowCount()-1, 1, item2)
- 额外的增删改查
def add(self):
# 加入一空行
self.tableWidget.insertRow(self.tableWidget.rowCount())
def find(self):
# 对于字符或字频或字符与字频进行查找
a: str = self.wordFrequencyEdit.text()
b: str = self.frequencyEdit.text()
i: int = 0
if a and b:
while i < self.tableWidget.rowCount():
if self.tableWidget.item(i, 0).text() == a and self.tableWidget.item(i, 1).text() == b:
self.resultLabel.setText(str(i+1))
break
i += 1
elif not a and b:
while i < self.tableWidget.rowCount():
if self.tableWidget.item(i, 1).text() == b:
self.resultLabel.setText(str(i+1))
break
i += 1
elif a and not b:
while i < self.tableWidget.rowCount():
if self.tableWidget.item(i, 0) and self.tableWidget.item(i, 0).text() == a:
self.resultLabel.setText(str(i+1))
break
i += 1
if i == self.tableWidget.rowCount():
self.resultLabel.setText("未找到")
- 保存字频
def saveWordFrequency(self):
# 保存文件
filePath, ok = QFileDialog.getSaveFileName(self, '选择文件')
if ok:
with open(filePath, 'w', encoding='utf-8') as file:
for i in range(self.tableWidget.rowCount()):
m = '\t'.join([self.tableWidget.item(
i, 0).text(), self.tableWidget.item(i, 1).text()])
file.write(m+'\n')
- 建树
def generateCharacterSetFromRawtext(self):
# 根据原文生成字符集
self.tableWidget.clearContents()
self.tableWidget.setRowCount(0)
def getFrequency(text: str) -> dict:
# 字频(统计)
cnt = {}
for i in text:
if i not in cnt:
cnt[i] = 1
else:
cnt[i] += 1
return cnt
CharacterSet = getFrequency(rawTextEdit.toPlainText())
for i, j in CharacterSet.items():
self.add()
item1 = QTableWidgetItem(i)
item2 = QTableWidgetItem(str(j))
self.tableWidget.setItem(self.tableWidget.rowCount()-1, 0, item1)
self.tableWidget.setItem(self.tableWidget.rowCount()-1, 1, item2)
def closeEvent(self, event):
# 关闭窗体
if self.tableWidget.rowCount() == 0:
return
global CharacterSet
CharacterSet = {}
# 将表格中的字符集存入变量CharacterSet中
for i in range(self.tableWidget.rowCount()):
if self.tableWidget.item(i, 0) and self.tableWidget.item(i, 1):
try:
CharacterSet[self.tableWidget.item(i, 0).text()] = float(
self.tableWidget.item(i, 1).text())
except:
pass
global HFTree
# 将树依据现有的字符集进行更新
if CharacterSet != {}:
HFTree = HuffmanTree(CharacterSet)
global showSVGWidget
if showSVGWidget:
HFTree.printTree('tmp')
showSVGWidget.update()
paintTreeWindow.printInform()
(二)编码(Encoding)
编码则是根据内存中现有的树来对原文进行编码,而原文的读取方式有两种,一种是手动输入,一种是读取文件,而原文也可进行保存
- 编码
def encode(self, text: str) -> str:
# 对text中的文本进行编码
p, q = '', '' # p是每个字符的编码,q是整篇文章的编码
for i in text:
for j in self.nodes:
if i == j.name:
while j.parent:
if j.parent.lchild == j:
p += '0'
elif j.parent.rchild == j:
p += '1'
j = j.parent
q += p[::-1]
p = ''
break
else:
# 若当前字符并不在字符集中,则返回空的密文
return None
return q
def encoding(self):
if not HFTree:
QMessageBox.critical(self, "错误", "当前无建好的树", QMessageBox.Ok)
elif rawTextEdit.toPlainText() == '':
QMessageBox.critical(self, "错误", "请输入原文", QMessageBox.Ok)
else:
t = HFTree.encode(rawTextEdit.toPlainText())
if not t:
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
self.encodedTextEdit.setText(t)
- 文件读入
def encodeFileReadin(self):
filePath, ok = QFileDialog.getOpenFileName(self, '选择文件')
if ok:
with open(filePath, 'r', encoding='utf-8') as file:
try:
text = file.read()
except UnicodeDecodeError:
QMessageBox.critical(
self, "错误", "请确保打开的是UTF-8编码的文本文件", QMessageBox.Ok)
return
self.rawTextEdit.setText(text)
- ③ 保存原文
def saveRawTextContent(self):
filePath, ok = QFileDialog.getSaveFileName(self, '选择文件')
if ok:
with open(filePath, 'w', encoding='utf-8') as file:
file.write(self.rawTextEdit.toPlainText())
(三)译码(Decoding)
译码则是根据内存中现有的树来对密文进行译码,而密文的读取方式有两种,一种是手动输入,一种是读取文件,而密文也可进行保存
- 译码
def decode(self, text: str) -> str:
# 在树中对text中的01串进行解码
root: TreeNode = self.rootnode
result = ""
for i in text:
if i == '0':
root = root.lchild
elif i == '1':
root = root.rchild
elif i == '\n': # 紧凑格式中的'\n'需忽略
continue
else:
return None
if root.name:
result += root.name
root = self.rootnode
if root != self.rootnode:
return None
else:
return result
def decoding(self):
if not HFTree:
QMessageBox.critical(self, "错误", "当前无建好的树", QMessageBox.Ok)
elif self.encodedTextEdit.toPlainText() == '':
QMessageBox.critical(self, "错误", "请输入密文", QMessageBox.Ok)
else:
t = HFTree.decode(self.encodedTextEdit.toPlainText())
if not t:
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
self.rawTextEdit.setText(t)
- 文件读入
def decodeFileReadin(self):
filePath, ok = QFileDialog.getOpenFileName(self, '选择文件')
if ok:
with open(filePath, 'r', encoding='utf-8') as file:
try:
encodedTextEdit = file.read()
except UnicodeDecodeError:
QMessageBox.critical(
self, "错误", "请确保打开的是UTF-8编码的文本文件", QMessageBox.Ok)
return
if not checkDecodedText(encodedTextEdit):
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
self.encodedTextEdit.setText(encodedTextEdit)
③ 保存密文
def saveEncodedTextContent(self):
if not checkDecodedText(self.encodedTextEdit.toPlainText()):
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
filePath, ok = QFileDialog.getSaveFileName(self, '选择文件')
if ok:
with open(filePath, 'w', encoding='utf-8') as file:
file.write(self.encodedTextEdit.toPlainText())
(四)打印代码文件(Print)
此处要求即为以紧凑格式输出,且要存储文件
- 紧凑格式
def compactFormPrint(self):
Text = self.encodedTextEdit.toPlainText()
text = ''
m = 50
for i in Text.replace('\n', ''):
text += i
m -= 1
if m == 0:
text += '\n'
m = 50
self.encodedTextEdit.setPlainText(text)
- 存储
def saveEncodedTextContent(self):
if not checkDecodedText(self.encodedTextEdit.toPlainText()):
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
filePath, ok = QFileDialog.getSaveFileName(self, '选择文件')
if ok:
with open(filePath, 'w', encoding='utf-8') as file:
file.write(self.encodedTextEdit.toPlainText())
(五)打印赫夫曼树(Tree printing)
打印赫夫曼树中,包括生成树的信息、对控件中的图像进行操作、树信息的显示以及树的导入以及存储,还有查看字符集的相关操作
- 生成树的图片
def printTree(self, filename=None):
# 生成树的图片
dot = Digraph(comment="生成的树")
dot.attr('node', fontname="STXinwei", shape='circle', fontsize="20")
for i, j in enumerate(self.nodes):
if j.name == '' or not j.name:
dot.node(str(i), '')
elif j.name == ' ':
dot.node(str(i), '[ ]') # 空格显示为'[ ]'
elif j.name == '\n':
dot.node(str(i), '\\\\n') # 换行符显示为'\n' 转义 此处的还会被调用,因此需要四个斜杠
elif j.name == '\t':
dot.node(str(i), '\\\\t') # 制表符显示为'\t'
else:
dot.node(str(i), j.name)
dot.attr('graph', rankdir='LR')
for i in self.nodes[::-1]:
if not (i.rchild or i.lchild):
break
if i.lchild:
dot.edge(str(self.nodes.index(i)), str(
self.nodes.index(i.lchild)), '0', constraint='true')
if i.rchild:
dot.edge(str(self.nodes.index(i)), str(
self.nodes.index(i.rchild)), '1', constraint='true')
dot.render(filename, view=False, format='svg')
- 树图像的放大缩小
class ShowSVGWidget(QWidget):
# 自定义控件,显示svg图片
leftClick: bool
svgrender: QSvgRenderer
defaultSize: QSizeF
point: QPoint
scale = 1
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
# 构造一张空白的svg图像
self.svgrender = QSvgRenderer(
b'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 0 0" width="512pt" height="512pt"></svg>')
# 获取图片默认大小
self.defaultSize = QSizeF(self.svgrender.defaultSize())
self.point = QPoint(0, 0)
self.scale = 1
def update(self):
# 更新图片
self.svgrender = QSvgRenderer("tmp.svg")
self.defaultSize = QSizeF(self.svgrender.defaultSize())
self.point = QPoint(0, 0)
self.scale = 1
self.repaint()
def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
# 绘画事件(回调函数)
painter = QPainter() # 画笔
painter.begin(self)
self.svgrender.render(painter, QRectF(
self.point, self.defaultSize*self.scale)) # svg渲染器来进行绘画,(画笔,QRectF(位置,大小))(F表示float)
painter.end()
def mouseMoveEvent(self, a0: QtGui.QMouseEvent) -> None:
# 鼠标移动事件(回调函数)
if self.leftClick:
self.endPos = a0.pos()-self.startPos
self.point += self.endPos
self.startPos = a0.pos()
self.repaint()
def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None:
# 鼠标点击事件(回调函数)
if a0.button() == Qt.LeftButton:
self.leftClick = True
self.startPos = a0.pos()
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent) -> None:
# 鼠标释放事件(回调函数)
if a0.button() == Qt.LeftButton:
self.leftClick = False
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
# 根据光标所在位置进行图像缩放
oldScale = self.scale
if a0.angleDelta().y() > 0:
# 放大
if self.scale <= 5.0:
self.scale *= 1.1
elif a0.angleDelta().y() < 0:
# 缩小
if self.scale >= 0.2:
self.scale *= 0.9
self.point = a0.pos()-(self.scale/oldScale*(a0.pos()-self.point))
self.repaint()
- 树信息的显示
def printInform(self):
# 更新树的信息
self.treeHeightlabel.setText(str(self.TreeDepth(HFTree)))
self.nodeCountlabel.setText(str(len(HFTree.characterset)*2-1))
self.leafCountlabel.setText(str(len(HFTree.characterset)))
- 树文件的读取
def importtree(self):
# 将树的信息导入到图片中
filePath, ok = QFileDialog.getOpenFileName(self, '选择文件')
if ok:
with open(filePath, 'r', encoding='utf-8') as file:
try:
text = file.read()
except UnicodeDecodeError:
QMessageBox.critical(
self, "错误", "请确保打开的是UTF-8编码的文本文件", QMessageBox.Ok)
return
global CharacterSet
CharacterSet = self.CharacterSet
textlines = re.findall(r'([\s\S])\t(\S+)\t\S+(\n|$)', text)
# 导入后重置字符集信息,并更新内存中的树
for i, j, _ in textlines:
CharacterSet[i] = float(j)
global HFTree
if CharacterSet != {}:
HFTree = HuffmanTree(CharacterSet)
global showSVGWidget
HFTree.printTree('tmp')
showSVGWidget.update()
self.printInform() # 将树的信息写在面板上
- 树的存储
def savetree(self):
# 保存树的信息
filePath, ok = QFileDialog.getSaveFileName(self, '选择文件')
if ok:
with open(filePath, 'w', encoding='utf-8') as file:
for i, j in HFTree.characterset.items():
m = '\t'.join([i, str(j), HFTree.encode(i)])
file.write(m+'\n')
(六)网络通信(Network)
网络通信包括服务端监听接口以及等待连接、客户端建立连接、树和密文的传输、等待接收以及断开连接
- 服务端监听端口
def buildServerConnection(self):
# 服务端监听端口
try:
# 获取端口号
port = int(self.lineEdit.text())
except ValueError:
QMessageBox.critical(self, "错误", "当前无已输入的端口号", QMessageBox.Ok)
return
# 建立一个套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 设置监听端口并监听
s.bind(("0.0.0.0", port))
s.listen()
except OSError:
QMessageBox.critical(self, "错误", "端口已被占用", QMessageBox.Ok)
return
# 等待客户端连接
self.stateLabel.setText("等待连接")
# 开启一个新的线程用于等待连接,防止程序阻塞,并利用daemon标记,以便于主线程结束时,自动结束带有此标记的所有线程
threading.Thread(target=self.handleClient,
args=[s], daemon=True).start()
- 服务端等待连接
def handleClient(self, s: socket.socket):
# 服务器端等待连接
c = s.accept()[0]
self.stateLabel.setText("已连接")
self.s = c
# 启动等待接收的线程
threading.Thread(target=self.waitRecv, args=[c], daemon=True).start()
- 客户端建立连接
def buildClientConnection(self):
# 客户端建立连接
try:
# 获取IP地址
ip = self.connectIpEditText.text()
if ip == None:
QMessageBox.critical(
self, "错误", "当前无已输入的IP地址", QMessageBox.Ok)
return
port = int(self.connectPortEditText.text())
except ValueError:
QMessageBox.critical(self, "错误", "当前无已输入的端口号", QMessageBox.Ok)
return
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, port))
except ConnectionRefusedError:
QMessageBox.critical(self, "错误", "连接失败", QMessageBox.Ok)
return
except OSError:
QMessageBox.critical(self, "错误", "IP或端口错误", QMessageBox.Ok)
return
self.stateLabel.setText("已连接")
self.s = s
# 连接成功,启动等待接收的线程
threading.Thread(target=self.waitRecv, args=[s], daemon=True).start()
- 树和密文的传输
def sendTree(self):
# 发送树
if not self.s:
QMessageBox.critical(self, "错误", "请先建立连接", QMessageBox.Ok)
return
global CharacterSet
if not CharacterSet or not HFTree:
QMessageBox.critical(self, "错误", "当前树为空", QMessageBox.Ok)
return
content = 't' # 发送树的标志
for i, j in CharacterSet.items():
content += i+"\t"+str(j)+'\n'
# 将其转化为Byte进行发送
self.s.sendall(content.encode())
QMessageBox.information(self, "提示", "发送成功", QMessageBox.Ok)
def sendText(self):
# 发送密文
if not self.s:
QMessageBox.critical(self, "错误", "请先建立连接", QMessageBox.Ok)
return
global encodedTextEdit
content = encodedTextEdit.toPlainText()
if not checkDecodedText(content):
QMessageBox.critical(self, "错误", "存在无效字符", QMessageBox.Ok)
return
self.s.sendall(('c'+content).encode())
QMessageBox.information(self, "提示", "发送成功", QMessageBox.Ok)
- 等待连接
def waitRecv(self, s: socket.socket):
# 等待接受线程
try:
while True:
data = s.recv(10000000)
# 将内容转变为str类型
data = data.decode()
if data[0] == 't':
data = data[1:]
textlines = re.findall(r'([\s\S])\t(\S+)(\n|$)', data)
global CharacterSet
CharacterSet = {}
for i, j, _ in textlines:
try:
CharacterSet[i] = float(j)
except ValueError:
self.stateLabel.setText("接收到无用数据")
self.tableWidget.clearContents()
self.tableWidget.setRowCount(0)
CharacterSet = {}
return
global HFTree
if CharacterSet != {}:
HFTree = HuffmanTree(CharacterSet)
self.stateLabel.setText("已收到树")
else:
self.stateLabel.setText("收到空树")
elif data[0] == 'c':
self.stateLabel.setText("已收到密文")
data = data[1:]
self.setEncodedTextSign.emit(data)
else:
self.stateLabel.setText("接收到无用数据")
except ConnectionResetError: # 对方断开
self.stateLabel.setText("连接断开")
self.s = None
except ConnectionAbortedError: # 自己断开
pass
- 断开连接
def breakConnection(self):
# 断开连接按钮事件
try:
self.s.close()
self.s = None
self.stateLabel.setText("未连接")
except:
pass
七. 调试分析
- 初始化(Initialization)
初始化有两种形式一种是直接根据原文生成,另一种是根据通过直接导入字符
- 编码(Encoding)
- 译码(Decoding)
- 打印代码文件(Print)
- 打印赫夫曼树(Tree printing)
- 网络通信(Network)
缺点:
- 无法限制密文的输入(由于textedit具有极为丰富的功能,例如带格式粘贴等,因此无法对密文的格式进行限制,只可在传输与读写时,进行内容判断)
- 在服务端与客户端断开后,服务器端无法等待下一次重连
- 每次接收的长度不可超过10000000
- 由于浮点数的精度原因,可能无法将6.0与6判为相同
- 保存图片的过程较为繁杂
优点:
- 运用正则表达式对于字符、字频、端口号以及ip地址的输入进行了限制
- 对多种非法操作进行特判
- 根据光标所在位置进行图像缩放,并使用矢量图绘图,放大缩小时不失真
八、 实验心得与体会(总结)
通过本次数据结构课程设计的学习,对于数据结构中的算法有了更深的理解,尤其是关于赫夫曼树的构建以及赫夫曼编码器的编码译码,在写代码是出现了挺多的bug,但是进过不断调试之后,之前不怎么理解的代码也更加熟悉了。当自己的程序出现bug时应当先检查自己的程序是不是有一些小细节出了问题,同时通过编译器的报错来寻找错误的地方并且进行改进。如果实在寻找不出问题所在的话需要通过借阅网上资料,通过他们的经验来帮助自己完成程序的调试。
通过本次的课程设计,增强了自己单独设计程序的能力以及调试程序的能力,让自己受益匪浅。