词云小程序设计并不难,但在封装时却碰到了一个大坑。历经一个多小时的搜索,问题才得以解决。做个后记,以备后用。
预期功能
- 打开任意一个UTF-8编码的文本文件和一张图片,能按图片轮廓生成词云;
- 生成词云时,能停用一些无用的关键词。
开发工具
- Python 3.8.6
- PyCharm 2021.1.1
- Qt Designer
- PyUIC
需要加载的库
- sys 系统内置库
- OpenCV 一款开源的计算机视觉和机器学习软件库
- matplotlib 一款 Python 的绘图库
- jieba 一款优秀的 Python 第三方中文分词库
- wordcloud 一款优秀的词云展示第三方库
- PyQt5 Python中一款优秀的GUI界面模块
UI界面的设计
-
使用Qt Designer进行界面设计和布局。得到界面图如下:
- 使用扩展工具PyUIC将UI界面文件Txt_Image.ui转为Txt_Image.py文件。代码如下:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'Txt_Image.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(958, 758)
MainWindow.setMinimumSize(QtCore.QSize(800, 600))
MainWindow.setMaximumSize(QtCore.QSize(16777215, 16777215))
MainWindow.setStyleSheet("font: 10pt \"微软雅黑\";")
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.formLayout = QtWidgets.QFormLayout(self.centralwidget)
self.formLayout.setObjectName("formLayout")
self.verticalLayout_2 = QtWidgets.QVBoxLayout()
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.verticalLayout_3 = QtWidgets.QVBoxLayout()
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setStyleSheet("font: 57 40pt \"李旭科书法 v1.4\";\n"
"color: rgb(85, 170, 0);")
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.label.setObjectName("label")
self.verticalLayout_3.addWidget(self.label)
self.label_2 = QtWidgets.QLabel(self.centralwidget)
self.label_2.setAlignment(QtCore.Qt.AlignCenter)
self.label_2.setObjectName("label_2")
self.verticalLayout_3.addWidget(self.label_2)
self.verticalLayout_2.addLayout(self.verticalLayout_3)
self.groupBox = QtWidgets.QGroupBox(self.centralwidget)
self.groupBox.setTitle("")
self.groupBox.setAlignment(QtCore.Qt.AlignCenter)
self.groupBox.setObjectName("groupBox")
self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.groupBox)
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.label_3 = QtWidgets.QLabel(self.groupBox)
self.label_3.setObjectName("label_3")
self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1)
self.lineEdit = QtWidgets.QLineEdit(self.groupBox)
self.lineEdit.setObjectName("lineEdit")
self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
self.pushButton = QtWidgets.QPushButton(self.groupBox)
self.pushButton.setObjectName("pushButton")
self.gridLayout.addWidget(self.pushButton, 0, 2, 1, 1)
self.label_4 = QtWidgets.QLabel(self.groupBox)
self.label_4.setObjectName("label_4")
self.gridLayout.addWidget(self.label_4, 1, 0, 1, 1)
self.lineEdit_2 = QtWidgets.QLineEdit(self.groupBox)
self.lineEdit_2.setObjectName("lineEdit_2")
self.gridLayout.addWidget(self.lineEdit_2, 1, 1, 1, 1)
self.pushButton_2 = QtWidgets.QPushButton(self.groupBox)
self.pushButton_2.setObjectName("pushButton_2")
self.gridLayout.addWidget(self.pushButton_2, 1, 2, 1, 1)
self.horizontalLayout_3.addLayout(self.gridLayout)
self.verticalLayout_2.addWidget(self.groupBox)
self.groupBox_2 = QtWidgets.QGroupBox(self.centralwidget)
self.groupBox_2.setTitle("")
self.groupBox_2.setObjectName("groupBox_2")
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2)
self.gridLayout_2.setObjectName("gridLayout_2")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.textEdit = QtWidgets.QTextEdit(self.groupBox_2)
self.textEdit.setObjectName("textEdit")
self.verticalLayout.addWidget(self.textEdit)
self.textEdit_2 = QtWidgets.QTextEdit(self.groupBox_2)
self.textEdit_2.setObjectName("textEdit_2")
self.verticalLayout.addWidget(self.textEdit_2)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.pushButton_3 = QtWidgets.QPushButton(self.groupBox_2)
self.pushButton_3.setObjectName("pushButton_3")
self.horizontalLayout.addWidget(self.pushButton_3)
self.pushButton_4 = QtWidgets.QPushButton(self.groupBox_2)
self.pushButton_4.setObjectName("pushButton_4")
self.horizontalLayout.addWidget(self.pushButton_4)
self.verticalLayout.addLayout(self.horizontalLayout)
self.verticalLayout.setStretch(0, 4)
self.verticalLayout.setStretch(1, 1)
self.horizontalLayout_2.addLayout(self.verticalLayout)
self.widget = QtWidgets.QWidget(self.groupBox_2)
self.widget.setObjectName("widget")
self.gridLayout_3 = QtWidgets.QGridLayout(self.widget)
self.gridLayout_3.setObjectName("gridLayout_3")
self.label_5 = QtWidgets.QLabel(self.widget)
self.label_5.setMaximumSize(QtCore.QSize(16777215, 16777215))
self.label_5.setScaledContents(False)
self.label_5.setAlignment(QtCore.Qt.AlignCenter)
self.label_5.setObjectName("label_5")
self.gridLayout_3.addWidget(self.label_5, 0, 0, 1, 1)
self.horizontalLayout_2.addWidget(self.widget)
self.horizontalLayout_2.setStretch(0, 1)
self.horizontalLayout_2.setStretch(1, 2)
self.gridLayout_2.addLayout(self.horizontalLayout_2, 0, 0, 1, 1)
self.verticalLayout_2.addWidget(self.groupBox_2)
self.formLayout.setLayout(0, QtWidgets.QFormLayout.SpanningRole, self.verticalLayout_2)
MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.label_3.setBuddy(self.lineEdit)
self.label_4.setBuddy(self.lineEdit_2)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "酷炫词云 程序设计:王立武"))
self.label.setText(_translate("MainWindow", "酷炫词云"))
self.label_2.setText(_translate("MainWindow", "程序设计:王立武 2021年6月8日"))
self.label_3.setText(_translate("MainWindow", "打开生成词云文本"))
self.lineEdit.setPlaceholderText(_translate("MainWindow", "请打开UTF-8编码的TXT格式的文本文件"))
self.pushButton.setText(_translate("MainWindow", "浏览"))
self.label_4.setText(_translate("MainWindow", "打开词云轮廓图片"))
self.lineEdit_2.setPlaceholderText(_translate("MainWindow", "建议使用白底的图片以得至更好的轮廓词云图"))
self.pushButton_2.setText(_translate("MainWindow", "浏览"))
self.textEdit.setPlaceholderText(_translate("MainWindow", "在这里粘贴的文字不能生成词云!"))
self.textEdit_2.setPlaceholderText(_translate("MainWindow", "注意:每个关键词之间用空格隔开!"))
self.pushButton_3.setText(_translate("MainWindow", "过滤文本"))
self.pushButton_4.setText(_translate("MainWindow", "生成词云"))
self.label_5.setText(_translate("MainWindow", "词云图片展示区"))
主程序的建立
- 导入相关的库,代码如下:
import sys
import cv2 # 导入图片处理模块 需安装库OpenCV。
import matplotlib.pyplot as plt
import jieba # 导入jieba分词模块
import wordcloud # 导入词云图模块
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtWidgets import *
from Txt_Image import Ui_MainWindow # 加载UI界面Txt_Image ,实现UI界面和代码分离
使用 from Txt_Image import Ui_MainWindow 的目的是为了实现UI界面和代码的分离,以便后续的修改和维护。
- 搭建主程序基本框架,相关代码如下:
class MyWindow(QtWidgets.QMainWindow, Ui_MainWindow): # 分离时,要加载UI界面类
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent) # 调用父类的构造方法
self.setupUi(self) # 调用构建子控件的方法
def view_txt(self): # 打开文本文件
pass
def view_image(self): # 打开背景轮廓图片文件
pass
def filter_key(self): # 过滤不生成图云的关键词
psss
def show_tuyu(self): # 生成图云
pass
if __name__ == "__main__":
app = QApplication(sys.argv) # 实例化一个QApplication对象
myWindow = MyWindow() # 实例化自定义的窗口类
myWindow.show() # 展示窗口
sys.exit(app.exec_()) # 启动事件循环,并将退出码传递给系统退出方法
- 添加信号和槽机制,代码如下:
self.pushButton.clicked.connect(self.view_txt)
self.pushButton_2.clicked.connect(self.view_image)
self.pushButton_3.clicked.connect(self.filter_key)
self.pushButton_4.clicked.connect(self.show_tuyu)
-
完善每个槽函数的功能设计。
a. 打开文本文件功能设计(view_txt)。因常见的文本文件编码格式有几种方式,在项目中指定打开的编码方式为UTF-8,因此在打开其他编码(如:ANSI)时,会报错,故在代码中加入异常处理,提醒要求打开的文本文件为UTF-8编码。
具体代码如下:
def view_txt(self): # 打开文本文件
try:
global file_content
file_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选取文件", "./", "*.txt")
self.lineEdit.setText(file_name)
file_content = open(file_name, "r",encoding='utf-8').read()
self.textEdit.setText(file_content)
# print(file_content)
except:
QMessageBox.critical(self,'提示','请选择一个文本文件!\n或者将文本文件的编码改为utf-8')
b. 打开图片文件功能设计(view_image)。具体代码如下:
def view_image(self): # 打开背景轮廓图片文件
global image_name
image_name, _ = QtWidgets.QFileDialog.getOpenFileName(self, "选取文件", "./", "*.png;;*.jpg;;*.jpeg")
self.lineEdit_2.setText(image_name)
c. 过滤关键词文本功能设计(filter_key)。具体代码如下:
def filter_key(self): # 过滤不生成图云的关键词
global stopwords
stopwords = set('')
filter_values = self.textEdit_2.toPlainText()
update_key = filter_values.split(" ")
# print(update_key)
stopwords.update(update_key)
这个槽函数中 stopwords 的实际为以后封装失败埋下了根源,后文再说。
代码 update_key = filter_values.split(" ") 的目的,是将用户在文本编辑框中输入的停用关键词,利用空格作为分隔符生成一个列表,以供在生成词云时调用。
d. 生成词云图功能设计(show_tuyu)。因用户有可能在没有打开文本文件或图像文件时,程序会报错,所以在这里也加入了异常处理,以提醒使用者。如图
具体代码如下:
def show_tuyu(self): # 生成图云
try:
cut_text = jieba.cut(file_content)
word = ' '.join(cut_text)
img = cv2.imread(image_name) # 读取数据
if self.textEdit_2.toPlainText() == "":
wd = wordcloud.WordCloud(
mask=img, # 背景图形,如果根据图片绘制,则需要设置
font_path='simhei.ttf', # 可以改成自己喜欢的字体
background_color='white', # 词云图背景颜色可以换成自己喜欢的颜色
)
else:
wd = wordcloud.WordCloud(
mask=img, # 背景图形,如果根据图片绘制,则需要设置
font_path='simhei.ttf', # 可以改成自己喜欢的字体
stopwords=stopwords,
background_color='white', # 词云图背景颜色可以换成自己喜欢的颜色
)
wd.generate(word)
plt.imshow(wd)
plt.axis('off') # 关闭显示x轴、y轴下标
# plt.show()
plt.savefig('.\images\chiyu.png', dpi=100)
jpg = QtGui.QPixmap("./images/chiyu.png")
self.label_5.setScaledContents(True)
self.label_5.setPixmap(jpg)
except:
QMessageBox.critical(self, "提示", "请先打开文本文件和轮廓图片文件后\n再运行生成词云!")
因为这只是一个练手的项目,所以没有过多去考虑把生成的词云图片都保存下来的想法,只是简单的写了一句保存图片代码:
plt.savefig('.\images\chiyu.png', dpi=100)
实际上,在这里利用获取生成图片的当前日期和时间为文件名,更为妥当。这个代码,这里就不给出了。
测试运行
-
效果图如下:
封装小程序
前面的工作都很顺利,没有遇到什么麻烦,但封装时却碰到了一个大问题。
回到命令行环境,并转到相应的路径下,运行如下代码开始封装:
pyinstaller -F -w Tuyun.py # Tuyun.py为主程序文件名
封装完成,没有报错,本以为大功告成了,但在运行程序时,却报错了,如下图:
出这种错的原因,大多是外加的资源文件没有添加到生成EXE文件的目录中,但此程序没有加入图标等资源文件。原因到底在哪呢?
通过某娘搜索得知出在词云库 wordcloud 中,解决办法:
- 找到词云库 wordcloud 的安装目录,将文件夹中的 stopwords 文件拷贝到生成EXE的文件夹中。
- 找到词云库 wordcloud 的安装目录,打开文件夹中的 wordcloud.py 文件,找到第33行:
FILE = os.path.dirname(__file__)
将这句代码改为:
FILE = os.path.dirname(sys.executable)
建议在做此步操作时,对原文件做好备份,以防不测。完成这些操作后,再次进行封装,运行,终于完成了。