PyQt5笔记1:词云小程序设计后记

词云小程序设计并不难,但在封装时却碰到了一个大坑。历经一个多小时的搜索,问题才得以解决。做个后记,以备后用。

预期功能

  • 打开任意一个UTF-8编码的文本文件和一张图片,能按图片轮廓生成词云;
  • 生成词云时,能停用一些无用的关键词。

开发工具

  • Python 3.8.6
  • PyCharm 2021.1.1
  • Qt Designer
  • PyUIC

需要加载的库

  • sys 系统内置库
  • OpenCV 一款开源的计算机视觉和机器学习软件库
  • matplotlib 一款 Python 的绘图库
  • jieba 一款优秀的 Python 第三方中文分词库
  • wordcloud 一款优秀的词云展示第三方库
  • PyQt5 Python中一款优秀的GUI界面模块

UI界面的设计

  1. 使用Qt Designer进行界面设计和布局。得到界面图如下:


    UI界面图
  1. 使用扩展工具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", "词云图片展示区"))

主程序的建立

  1. 导入相关的库,代码如下:
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界面和代码的分离,以便后续的修改和维护。

  1. 搭建主程序基本框架,相关代码如下:
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_())  # 启动事件循环,并将退出码传递给系统退出方法
  1. 添加信号和槽机制,代码如下:
        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)
  1. 完善每个槽函数的功能设计。
    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 中,解决办法:

  1. 找到词云库 wordcloud 的安装目录,将文件夹中的 stopwords 文件拷贝到生成EXE的文件夹中。
  2. 找到词云库 wordcloud 的安装目录,打开文件夹中的 wordcloud.py 文件,找到第33行:
FILE = os.path.dirname(__file__)

将这句代码改为:

FILE = os.path.dirname(sys.executable)

建议在做此步操作时,对原文件做好备份,以防不测。完成这些操作后,再次进行封装,运行,终于完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容