一、背景介绍
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文件(创建应用界面)
如上图所示,打开已安装的QtDesign,开始设计应用界面,具体界面如下(稍后会有代码地址),设计完成后会生成后缀名ui的文件,保存在主目录下。
然后在pycharm中点击ui文件右键使用PyUi5转换成py文件(py文件可在文章末尾获取),下图的qt.ui就是使用QtDesign设计后的ui文件,qt.py就是使用PyUi5转换qt.ui后的py文件。到这里,应用界面的代码也已经全部出来了。
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也没有解决问题,想不透原因是什么。
4.打包
在主目录下打开命令行,使用 activate pyqt
激活环境
pyinstaller -F -i BBH.ico main.py qt.py analysis.py
输入打包命令,即可生成应用。调试完成后我会把-w参数加上,不显示黑窗。
最后生成的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>"))
全部文件可点击下载。