一、背景介绍

1.简介

职业:数据分析师
需求背景:在业务的数据处理过程中,渐渐的形成了一些固定输入报表类的工作,有一部分不需要进行再加工的就直接开发个报表工具了,有一部分是需要再次加工的,也就是这次我介绍的数据处理小工具。
需求目的:使用 pyqt+pyinstall 建立数据处理小工具,让非数据分析人员可以一键获取想要的数据,不再麻烦我们数据分析师。

2.说明

鉴于以上的需求和目的,决定使用 pycharm+pyqt5+pyinstaller 创建一个QT小工具,区别于B/S结构,它可以在符合要求的电脑一键使用(免安装、免部署)。
由于pycharm很久之前就安装好了,pyqt5相关的pyuic5和QtDesign也在之前安装好了,这次的主题是pyqt的创建,所以就不再演示这些过程,下面将附上相关的链接,如果有问题可以网上搜索或在评论给我留言。
如何安装2019Pycharm最新版本-详细教程–(附如何激活2019Pycharm–2019Pycharm激活)Python3+Pycharm+PyQt5环境搭建步骤图文详解Python创建virtualenv(虚拟环境)方法

温馨小提示:自己安装部署过程中的问题总是一个接一个,网络上的方法千万种,寻根溯源、对号入座,总会找到解决的办法的。

二、安装详细步骤

1.现有条件

  • 虚拟环境已经创建,包含的包列表如下图所示;
  • pycharm已经创建项目,使用已创建的虚拟环境;
  • pyuic5和QtDesign已配置完成;
  • 业务逻辑已封装;

温馨小提示:创建虚拟环境可以避免安装不使用的包,这样在最后的打包过程中就可以减少包的体积,使应用轻量化。

2.使用QtDesign创建UI文件(创建应用界面)

pycharm 如何清空 python console_pycharm+pyqt


如上图所示,打开已安装的QtDesign,开始设计应用界面,具体界面如下(稍后会有代码地址),设计完成后会生成后缀名ui的文件,保存在主目录下。

pycharm 如何清空 python console_pycharm+pyqt_02

然后在pycharm中点击ui文件右键使用PyUi5转换成py文件(py文件可在文章末尾获取),下图的qt.ui就是使用QtDesign设计后的ui文件,qt.py就是使用PyUi5转换qt.ui后的py文件。到这里,应用界面的代码也已经全部出来了。

pycharm 如何清空 python console_数据处理_03

3.创建功能代码

功能代码主要是指这个界面所应用的功能,比如填写提示、提交功能、提交检测、警告提示等。
不详细述说,具体细节可以参考这篇文章。
这里面有个麻烦的地方,也是QT不可避免的地方,就是当一段代码运行时间过长时,就使得界面不能动弹,只有当代码运行完毕后,界面才会恢复正常,这使得体验很差,所以这里加了线程,让界面和业务逻辑分开,保证视觉效果。
代码如下:

# -*- coding: utf-8 -*-
import sys
import qt
import analysis
import threading
import datetime
import time
from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QWidget
from PyQt5 import QtGui

class QMBox(QWidget):
    def qshow(self):
        # 没有输入月份的提示
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/眼睛是心灵的窗户.jpg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        QMessageBox.setWindowIcon(self, icon)
        QMessageBox.critical(self, "注意", "<h3><font color=black>输入月份</font></h3>")

    def qshow1(self):
        # 有月份但是库中无此数据的提示
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/眼睛是心灵的窗户.jpg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        QMessageBox.setWindowIcon(self, icon)
        QMessageBox.about(self, "sorry", "<h3><font color=black>没有该月份的数据</font></h3>")

    def qshow2(self):
        # 输入的月份在库中不能查找数据,比如203008
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/眼睛是心灵的窗户.jpg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        QMessageBox.setWindowIcon(self, icon)
        QMessageBox.information(self, "警告", "输入的时间不在合法时间范围内\n合法范围时间是201701至201907")

class SubUi(qt.Ui_MainWindow):

    def work(self, app):
        # self.pushButton_start.clicked.connect(lambda : self.exe(self.lineEdit.text()) if self.lineEdit.text() else None)
        def fresh():
            if analysis.doIT(self.lineEdit.text()).checkMonths():
                QMBox().qshow2()
                return None
            job = threading.Thread(target=self.exe,)
            job.setName('exe')
            job.start()

        self.pushButton_start.clicked.connect(lambda: fresh() if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_0.clicked.connect(lambda: self.loadData(6) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_1.clicked.connect(lambda: self.loadData(0) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_2.clicked.connect(lambda: self.loadData(1) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_3.clicked.connect(lambda: self.loadData(2) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_4.clicked.connect(lambda: self.loadData(3) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_5.clicked.connect(lambda: self.loadData(4) if self.lineEdit.text() else QMBox().qshow())
        self.pushButton_6.clicked.connect(lambda: self.loadData(5) if self.lineEdit.text() else QMBox().qshow())

    def appendText(self, content):
        # 输出文本到文本框(使用线程保证主进程不会被卡死)
        # def _showText(textBrowser, content):
        self.textBrowser.append(content)
        self.textBrowser.moveCursor(self.textBrowser.textCursor().End)
        time.sleep(1)
        QApplication.processEvents()

    def loadData(self, number):
        # 导出数据
        if not analysis.doIT(self.lineEdit.text()).checkData():
            QMBox().qshow1()
            return None

        def _loadData(months, number):
            if number <= 5:
                analysis.doIT(months).getOneData(number)
            else:
                analysis.doIT(months).getData()
        threading.Thread(target=_loadData, args=(self.lineEdit.text(), number)).start()

    def exe(self,):
        # 处理数据
        months = self.lineEdit.text()
        doit = analysis.doIT(months)
        self.progressBar.setProperty("value", 1)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t开始这次的任务')
        doit.createTB()
        self.progressBar.setProperty("value", 20)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t完成基础信息提取')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t开始提取基础--信息')
        doit.createRoutes()
        self.progressBar.setProperty("value", 40)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t完成基础--信息提取')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t处理问题')
        doit.solveOQ()
        self.progressBar.setProperty("value", 60)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t问题处理完毕')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t处理--问题')
        doit.solveOQRoutes()
        self.progressBar.setProperty("value", 80)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t--问题处理完毕')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t开始填充空值')
        doit.fillSeats()
        self.progressBar.setProperty("value", 90)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t空值数据处理完毕')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t开始创建中间表')
        doit.createLS()
        self.progressBar.setProperty("value", 100)
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t中间表已建立完成')
        self.appendText(datetime.datetime.now().strftime('%H:%M:%S')+'\t所有数据已经处理完毕,谢谢您的等待!')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    MainWindow = QMainWindow()
    ui = SubUi()
    ui.setupUi(MainWindow)
    ui.work(app)
    MainWindow.show()
    sys.exit(app.exec_())

主要有两处使用多线程

  • pushButton_start (飞机按钮)的点击事件会引起进度条和文本框的变化,这段代码如果不使用线程就会使得界面不能刷新,当代码运行完毕后进度条就会忽然变成100%;
  • pushButton_0 - pushButton_6(下载数据)的点击事件也在 loadData 函数中使用了线程

QMBox是为了目的是现实提示框:没有输入数据提示、输入数据不合法提示、输入数据不在范围提示;
引入的模块 analysis 是封装好的业务代码,因为涉及隐私,所以大都使用pass代替。具体代码如下:

# -*- coding: utf-8 -*-
"""
Created on Wed Jul 17 11:01:54 2019
@author: wfxu
"""
import pandas as pd
import re

class doIT:

    def __init__(self, months):
        self.months = months
        self.months_last = str(int(months) - 100)

    def execute(self,sql):
        doIT.engine.execute(sql)

    def createTB(self, ):
        # 创建基础表并插入数据
        pass

    def createRoutes(self, ):
        # 创建基础表
        pass

    def solveOQ(self, ):
        # 解决问题
        pass

    def solveOQRoutes(self, ):
        # 解决部分
        pass
        
    def fillSeats(self, ):
        # 填补空值
        pass
        
    def createLS(self, ):
        # 新建中间表
        pass
        
    def getData(self, ):
        # 一次性获取所有数据
        df0 = pd.DataFrame({'id':[1,2,3,4,5,6],'name':['Alice','Bob','Cindy','Eric','Helen','Grace '],
                           'math':[90,89,99,78,97,93],'english':[89,94,80,94,94,90]})
        df1 = df0.copy()
        df2 = df0.copy()
        df3 = df0.copy()
        df4 = df0.copy()
        df5 = df0.copy()
        df = pd.DataFrame()
        df.to_excel('总览报告.xlsx')
        writer = pd.ExcelWriter('总览报告.xlsx')
        df0.to_excel(excel_writer=writer, index=False, sheet_name='数据1')
        df1.to_excel(excel_writer=writer, index=False, sheet_name='数据2')
        df2.to_excel(excel_writer=writer, index=False, sheet_name='数据3')
        df3.to_excel(excel_writer=writer, index=False, sheet_name='数据4')
        df4.to_excel(excel_writer=writer, index=False, sheet_name='数据5')
        df5.to_excel(excel_writer=writer, index=False, sheet_name='数据6')
        writer.save()
        writer.close()
        
    def getOneData(self, number,):
        if number == 0:
            files = '数据1.xlsx'
        elif number == 1:
            files = '数据2.xlsx'
        elif number == 2:
            files = '数据3.xlsx'
        elif number == 3:
            files = '数据4.xlsx'
        elif number == 4:
            files = '数据5.xlsx'
        elif number == 5:
            files = '数据6.xlsx'
        df = pd.DataFrame({'id':[1,2,3,4,5,6],'name':['Alice','Bob','Cindy','Eric','Helen','Grace '],
                           'math':[90,89,99,78,97,93],'english':[89,94,80,94,94,90]})
        df.to_excel(files, index=False)
    
    def checkData(self,):
        # 检查这个月份是否有数据
        try:
            return True # 判断代码,输出bool
        except:
            return False
    
    def checkMonths(self, ):
        # 检查这个月份是否符合输入
        months_list = ['201701', '201702', '201703', '201704', '201705', '201706', '201707', '201708', '201709', '201710', '201711', '201712',
                       '201801', '201802', '201803', '201804', '201805', '201806', '201807', '201808', '201809', '201810', '201811', '201812',
                       '201901', '201902', '201903', '201904', '201905', '201906', '201907']
        if self.months not in months_list:
            return True
        else:
            return False

在pycharm中点击run即可使用该界面

目前未解决的问题:在触发pushButton_start(飞机按钮)的点击事件后,持续更新进度条和文本框,在实际运行中,这段代码运行了40分钟,中间没有问题,但是如果在点击飞机后又切换到其他界面,最后就会弹出python 停止工作的警告并且会关闭界面,如果没有切换则会一切如常。使用过QThread也没有解决问题,想不透原因是什么。

pycharm 如何清空 python console_数据处理_04

4.打包

在主目录下打开命令行,使用 activate pyqt 激活环境

pyinstaller -F -i BBH.ico main.py qt.py analysis.py

输入打包命令,即可生成应用。调试完成后我会把-w参数加上,不显示黑窗。

pycharm 如何清空 python console_qt_05


最后生成的exe文件也就是我们最终的成果,记住吧images文件夹也放到一起,不然界面中的一些图标显示不了。

至此全部工作已完成。

qt.py 代码如下:

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

# Form implementation generated from reading ui file 'qt.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_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1021, 480)
        MainWindow.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
        MainWindow.setMouseTracking(False)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap("images/眼睛是心灵的窗户.jpg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        MainWindow.setWindowIcon(icon)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.frame = QtWidgets.QFrame(self.centralwidget)
        self.frame.setEnabled(True)
        self.frame.setGeometry(QtCore.QRect(40, 30, 380, 401))
        self.frame.setFrameShape(QtWidgets.QFrame.NoFrame)
        self.frame.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.frame.setObjectName("frame")
        self.lineEdit = QtWidgets.QLineEdit(self.frame)
        self.lineEdit.setGeometry(QtCore.QRect(140, 20, 80, 30))
        font = QtGui.QFont()
        font.setFamily("新宋体")
        self.lineEdit.setFont(font)
        self.lineEdit.setText("")
        self.lineEdit.setAlignment(QtCore.Qt.AlignCenter)
        self.lineEdit.setObjectName("lineEdit")
        self.label = QtWidgets.QLabel(self.frame)
        self.label.setGeometry(QtCore.QRect(20, 20, 100, 30))
        font = QtGui.QFont()
        font.setFamily("黑体")
        font.setPointSize(9)
        self.label.setFont(font)
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setIndent(1)
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(self.frame)
        self.label_2.setGeometry(QtCore.QRect(20, 70, 321, 331))
        font = QtGui.QFont()
        font.setBold(False)
        font.setItalic(True)
        font.setUnderline(False)
        font.setWeight(50)
        font.setStrikeOut(False)
        font.setKerning(True)
        font.setStyleStrategy(QtGui.QFont.PreferDefault)
        self.label_2.setFont(font)
        self.label_2.setCursor(QtGui.QCursor(QtCore.Qt.ForbiddenCursor))
        self.label_2.setToolTip("")
        self.label_2.setTextFormat(QtCore.Qt.RichText)
        self.label_2.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
        self.label_2.setWordWrap(True)
        self.label_2.setObjectName("label_2")
        self.pushButton_start = QtWidgets.QPushButton(self.frame)
        self.pushButton_start.setGeometry(QtCore.QRect(260, 20, 61, 41))
        self.pushButton_start.setText("")
        icon1 = QtGui.QIcon()
        icon1.addPixmap(QtGui.QPixmap("images/起飞.jpg"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.pushButton_start.setIcon(icon1)
        self.pushButton_start.setIconSize(QtCore.QSize(50, 40))
        self.pushButton_start.setObjectName("pushButton_start")
        self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
        self.progressBar.setGeometry(QtCore.QRect(480, 30, 500, 30))
        self.progressBar.setProperty("value", 0)
        self.progressBar.setAlignment(QtCore.Qt.AlignCenter)
        self.progressBar.setOrientation(QtCore.Qt.Horizontal)
        self.progressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom)
        self.progressBar.setObjectName("progressBar")
        self.textBrowser = QtWidgets.QTextBrowser(self.centralwidget)
        self.textBrowser.setGeometry(QtCore.QRect(480, 70, 500, 200))
        self.textBrowser.setObjectName("textBrowser")
        self.frame_download = QtWidgets.QFrame(self.centralwidget)
        self.frame_download.setGeometry(QtCore.QRect(480, 300, 500, 131))
        self.frame_download.setFrameShape(QtWidgets.QFrame.Panel)
        self.frame_download.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame_download.setObjectName("frame_download")
        self.pushButton_6 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_6.setGeometry(QtCore.QRect(190, 90, 150, 30))
        self.pushButton_6.setObjectName("pushButton_6")
        self.pushButton_2 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_2.setGeometry(QtCore.QRect(190, 10, 150, 30))
        self.pushButton_2.setObjectName("pushButton_2")
        self.pushButton_3 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_3.setGeometry(QtCore.QRect(10, 50, 150, 30))
        self.pushButton_3.setObjectName("pushButton_3")
        self.pushButton_4 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_4.setGeometry(QtCore.QRect(190, 50, 150, 30))
        self.pushButton_4.setObjectName("pushButton_4")
        self.pushButton_1 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_1.setGeometry(QtCore.QRect(10, 10, 150, 30))
        self.pushButton_1.setObjectName("pushButton_1")
        self.pushButton_5 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_5.setGeometry(QtCore.QRect(10, 90, 150, 30))
        self.pushButton_5.setObjectName("pushButton_5")
        self.pushButton_0 = QtWidgets.QPushButton(self.frame_download)
        self.pushButton_0.setGeometry(QtCore.QRect(360, 20, 131, 81))
        self.pushButton_0.setText("")
        icon2 = QtGui.QIcon()
        icon2.addPixmap(QtGui.QPixmap("images/大蓝洞.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
        self.pushButton_0.setIcon(icon2)
        self.pushButton_0.setIconSize(QtCore.QSize(120, 60))
        self.pushButton_0.setAutoRepeatDelay(300)
        self.pushButton_0.setAutoDefault(True)
        self.pushButton_0.setDefault(True)
        self.pushButton_0.setFlat(True)
        self.pushButton_0.setObjectName("pushButton_0")
        self.pushButton_0.raise_()
        self.pushButton_6.raise_()
        self.pushButton_2.raise_()
        self.pushButton_3.raise_()
        self.pushButton_4.raise_()
        self.pushButton_1.raise_()
        self.pushButton_5.raise_()
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(0, 0, 72, 15))
        self.label_3.setCursor(QtGui.QCursor(QtCore.Qt.BlankCursor))
        self.label_3.setObjectName("label_3")
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

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

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "我是小工具"))
        self.lineEdit.setToolTip(_translate("MainWindow", "<html><head/><body><p>注意:要输入正确的时间哟!</p></body></html>"))
        self.label.setToolTip(_translate("MainWindow", "<html><head/><body><p>输入样式:201907</p></body></html>"))
        self.label.setText(_translate("MainWindow", "<html><head/><body><p align=\"center\"><span style=\" font-size:12pt;\">输入月份</span></p></body></html>"))
        self.label_2.setText(_translate("MainWindow", "<html><head/><body><p><span style=\" font-weight:600;\">操作说明:</span></p><p>1.输入时间,时间格式为:<span style=\" font-weight:600; color:#ff5500;\">201904</span>;</p><p>2.点击飞机,开始起飞;</p><p><span style=\" font-weight:600;\">进度查看:</span></p><p>1.查看进度条,可以看到指标计算进度;</p><p>2.查看右侧文本说明可以看到详细步骤;</p><p><span style=\" font-weight:600;\">数据下载:</span></p><p>1.点击右下侧框中所需数据,比如点击<span style=\" color:#ff5500;\">数据1</span>则会下载数据1;</p><p>2.如果想一次下载全部数据,点击大蓝洞;</p></body></html>"))
        self.pushButton_start.setToolTip(_translate("MainWindow", "<html><head/><body><p>点击起飞</p></body></html>"))
        self.textBrowser.setHtml(_translate("MainWindow", "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'SimSun\'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><br /></p></body></html>"))
        self.pushButton_6.setText(_translate("MainWindow", "数据6"))
        self.pushButton_2.setText(_translate("MainWindow", "数据2"))
        self.pushButton_3.setText(_translate("MainWindow", "数据3"))
        self.pushButton_4.setText(_translate("MainWindow", "数据4"))
        self.pushButton_1.setText(_translate("MainWindow", "数据1"))
        self.pushButton_5.setText(_translate("MainWindow", "数据5"))
        self.label_3.setText(_translate("MainWindow", "<html><head/><body><p><span style=\" font-size:6pt; font-weight:600; font-style:italic;\">AUTHOR</span><span style=\" font-size:6pt; font-style:italic; vertical-align:super;\">徐文汾</span></p></body></html>"))

全部文件可点击下载。