前言背景
年前运用Python
写的小应用,借此刻写本文的机会,再次整理总结。
我们公司电子工程师会使用Mentor
工具导出一张BOM
表,然后相关专员会将其手动输入到SAP
系统里 ( 为何不能一键导入?)。但由于数据庞杂,专员担心手动输入可能有误,于是又将SAP里数据导出,让工程师去比对确认材料
、位号
和数量
有无错误。
有一次看到某同事一直忙于在整理Excel、比较数据,于是我就想能否用Pandas
和PyQT5
去写个小工具自动比较并导出结果?
工具界面十分简单,只要选择SAP
数据表和Mentor
数据表后,点击保存至Excel即可。有差异的地方按照自己想要的样式呈现在Excel,如此节省工程师整理比较数据时间。
SAP表里可能会有个某个大类产品下多个型号的设备BOM数据,而Mentor 表是单一型号设备数据。SAP 表每一行都是某个位号的材料信息,而Mentor 表里每行是某个Part No信息,2张表样式(部分)如下:
从上图中可看出SAP和Mentor数据表里都有很多列,但我们需要比较的分别是SAP 的
Material
,Componet
(同Mentor里的PART NO) 和Installation point
(同Mentor里的REF DES),和Mentor里的PART NO
,REF DES
以及COUNT
。SAP Material列筛选后有4个不同的数据,50149261-003,50149261-013,50149261-023,50149261-033,它们是用于区分同类产品下的4个不同module的设备。Mentor表文件名称包含这些字段,如文件名50149261-033(SCBIP-24V)。
工具界面
小工具界面SAP数据
我们Upload SAP BOM表时需要对Excel进行简单判断,要求其必须含有Installation point
列(Mentor不含此列)。选择Mentor数据表时,要求Excel文件名称含关键字段,否则给予错误提示。
def loadSAPData(self):
file_Path, _ = QFileDialog.getOpenFileName(self, 'Open file', "C:\\", 'Excel files(*.xlsx , *.xls)')
# 如果用户不选择文件,直接取消选择,
if file_Path =='':
return
df = pd.read_excel(file_Path) #根据路径读取excel文件
columns = df.columns.values.tolist() # 获取表所有的列名
if 'Installation point' in columns: # 判断关键字在列名称列表里
self.label_sap_file.setText(file_Path.split('/')[-1]) #文件名显示在界面上
self.sap_df = df
self.materials = df['Material'].unique() #列表,存放不同型号
self.label_sap.setStyleSheet("QLabel{border-image: url(:icons/excel.png);}") #label设置图片
else:
self.showMessage() #选择的文件不含有Installation point列,则提示错误信息
def showMessage(self):
self.materials = [] #清空列表里的material
messageBox = QMessageBox()
messageBox.setWindowIcon(QIcon(':icons/error.png')) #注意为了打包后APP上能够显示图片,此处使用冒号,icons 是项目里的一个自己创建的文件夹
messageBox.setWindowTitle('Error Message')
messageBox.setText(
"This file doesn't include a column named 'Installation point'.\nWould you like reload a file? ")
messageBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
buttonY = messageBox.button(QMessageBox.Yes)
buttonY.setText('Select File')
buttonN = messageBox.button(QMessageBox.No)
buttonN.setText('Cancel')
messageBox.exec_()
if messageBox.clickedButton() == buttonY:
self.loadSAPData()
选择文件错误提示
根据material清洗整理SAP数据
前文中说到SAP数据表里包含多个module的数据,而Mentor数据表是单个型号的数据,其文件名称里包含material字段。
首先需要根据material来筛选出不同的数据:
def getSperatedDF(self, material):
# 根据材料名筛选,如'50149261-003'
df = self.sap_df.loc[self.sap_df['Material'] == material]
# 无重复的location 数量
count_df = df.groupby('Component', sort=False)[['Installation point']].nunique().reset_index()
# 同一颗的不同位置拼接在一起并空格分隔
gpby_df = df.groupby('Component', sort=False).apply(lambda x: ' '.join(x['Installation point'])).reset_index()
# 取出'Component', 'Installation point' 2 列数据
gpby_df.columns = ['Component', 'Installation point']
gpby_df['SAP_COUNT'] = count_df['Installation point']
# 对位号字符串排序
sorted_df = self.tool.getSortedLocationsDF(gpby_df,'Installation point')
return sorted_df
接下来需要对Installation point
列各单元格里杂乱的位号字符串进行排序 ,如将C1 C20 C4 C3
排成C1 C3 C4 C20
。
def sortLocationstr(self, installation_point_str):
if installation_point_str !='': #判断不为空
split_list = re.findall(r'[0-9]+|[a-zA-Z]+',installation_point_str)
first_letters = split_list[0] #获取第一个字母,是C/R/…
numStr_list = list(set(split_list)) # 对所有去重,用set集合方法
numStr_list.remove(first_letters)# 去掉字母,只留下数字
num_list= []
for num_str in numStr_list:
num_list.append(int(num_str))
num_list = sorted(num_list) #数字进行排序
locations = [] #位置
for num in num_list:
location = first_letters + str(num)
locations.append(location)
return ' '.join(locations)
else:
return ''
根据列名称和df文件生成排序后的文件:
def getSortedLocationsDF(self, df, column_name):
for i in range(df.shape[0]): # shape[0]获取行数
installation_point_str = df.loc[i, column_name]
df.loc[i, column_name] = self.sortLocationstr(installation_point_str) # 对位置字符串排序
return df
Mentor数据
def loadMentorData(self):
file_Path, _ = QFileDialog.getOpenFileName(self, 'Open file', "C:\\", 'Excel files(*.xlsx , *.xls)')
# 如果用户不选择文件,直接取消选择,
if file_Path == '':
return
df = pd.read_excel(file_Path)
material = file_Path.split('/')[-1].split('.')[0]
if '(' in material:
material = material.split('(')[0]
if material in self.materials:
self.label_mentor_file.setText( file_Path.split('/')[-1])
self.label_mentor.setStyleSheet("QLabel{border-image: url(:icons/excel.png);}")
self.mentor_file_name = material
self.mentor_df = df
self.mentor_df = self.mentor_df[['PART NO','REF DES','COUNT']].fillna('') # 选出 'PART NO','REF DES','COUNT' 3列数据
self.tool.getSortedLocationsDF(self.mentor_df,'REF DES') # 对'REF DES' 列单元格里的位置排序
self.material_df = self.getSperatedDF(material)
else:
#以下代码主要是弹出错误提示
self.mentor_file_name = ''
messageBox = QMessageBox()
messageBox.setWindowTitle('Error Message')
messageBox.setWindowIcon(QIcon(':icons/error.png'))
messageBox.setText(
"This file isn't part of the SAP data. \nWould you like reload a file? ")
messageBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
buttonY = messageBox.button(QMessageBox.Yes)
buttonY.setText('Select File')
buttonN = messageBox.button(QMessageBox.No)
buttonN.setText('Cancel')
messageBox.exec_()
if messageBox.clickedButton() == buttonY:
self.loadMentorData()
合并数据并比较
至此我们需要的material_df 和 mentor_df已经处理完毕,接下来需要将2者merge并进行比较。
def mergeData(self):
compare_df = pd.merge(self.material_df,self.mentor_df,left_on='Component',right_on='PART NO',how= 'outer')
compare_df = compare_df.fillna('')
compare_df['Component_Bool'] = compare_df.apply(lambda x: self.tool.compare(x['Component'],x['PART NO']),axis = 1)
compare_df['COUNT_Bool'] = compare_df.apply(lambda x: self.tool.compare(x['SAP_COUNT'],x['COUNT']),axis = 1 )
compare_df['Location_Bool'] = compare_df.apply(lambda x: self.tool.compare(x['Installation point'],x['REF DES']),axis = 1 )
compare_df['Location_Change'] = compare_df.apply(lambda x: self.tool.compareLocation(str(x['Installation point']), str(x['REF DES'])), axis = 1)
return compare_df
这里主要比较对应的两列内容是否一致,一致就返回True,否则为False。
def compare(self, a, b):
if a == b:
return 'True'
else:
return 'False'
比较 2个位号字符串差异并返回差异结果,如C2,C4,C7 和C2,C3,C7, 它们的差异就是C3和C4。
def compareLocation(self, sap_loction, mentor_locatoin):
if set(sap_loction.split(' ')).difference(mentor_locatoin.split(' ')) != {''}:
location_list = list(set(sap_loction.split(' ')).difference(set(mentor_locatoin.split(' '))))
else:
location_list = list(set(mentor_locatoin.split(' ')).difference(set(sap_loction.split(' '))))
return ' '.join(location_list)
Excel格式化
创建一个Excel 类并设置其相关格式,根据你自己的需求进行格式修改。
import numpy as np
from openpyxl import load_workbook
from openpyxl.styles import Alignment, Side, Border, PatternFill, Font
from openpyxl.styles.colors import BLACK, RED, YELLOW
class Excel(object):
def __init__(self,excel_path):
super().__init__()
self.wb = load_workbook(excel_path)
self.ws = self.wb.active
self.excel_path = excel_path
self.font_name = 'Biome Light'
def formatExcel(self):
font = Font(name = self.font_name , size=10, color = BLACK) #字体
false_font = Font(name = self.font_name, bold=True, size=10, color = BLACK) #false单元格字体
alignment = Alignment(horizontal='center', #水平居中
vertical='center', #垂直居中
wrap_text=True, # 文字换行
shrink_to_fit=False, # 自适应宽度,改变文字大小,上一项false
)
thin = Side(border_style="thin", color=BLACK) #边框线颜色
border = Border(top=thin, left=thin, right=thin, bottom=thin) #边框线
header_fill = PatternFill(fill_type='solid', fgColor= YELLOW ) #表头填充颜色
false_fill = PatternFill(fill_type='solid', fgColor= RED) #false单元格填充颜色
header_font = Font(name=self.font_name, bold=True, size=12, color=BLACK) #表头单元格字体
for row in self.ws.rows:
for cell in row:
cell.font = font
cell.alignment = alignment
cell.border = border
if cell.value == 'False':
cell.fill = false_fill
cell.font = false_font
# 如果是第一行,列名称那行
for cell in list(self.ws.rows)[0]:
cell.fill = header_fill
cell.font = header_font
self.setColumnsWidth(25)
self.wb.save(self.excel_path)
def setColumnsWidth(self,width):
column = self.ws.max_column # 获取表格列数
numbers = np.arange(65, 65 + column) # 大写字母A 是65
for i in [chr(i) for i in numbers]: #生产字母A,B,C,D……
self.ws.column_dimensions[i].width = width
导出Excel文件
def saveToExcel(self):
compare_df = self.mergeData()
sorted_df = self.tool.getSortedLocationsDF(compare_df,'Location_Change')
sorted_df.to_excel('{}_compare.xlsx'.format(self.mentor_file_name),sheet_name= 'compare', engine = 'openpyxl',index=False)
excel = Excel('{}_compare.xlsx'.format(self.mentor_file_name))
excel.formatExcel() #设置格式
对比结果(部分)
打包调用图片
我们需要创建一个名称为makeqrc.py
的文件,运行此文件会生成2个文件,分别是images.qrc
和images.py
文件。
十分重要的一点是,必须在导包的文件里import images
,images 是images.py。
import subprocess, os
images = os.listdir('icons') #icons是存放图片文件的文件夹
f = open('images.qrc', 'w+')
f.write(u'<!DOCTYPE RCC>\n<RCC version="1.0">\n<qresource>\n')
for item in images:
f.write(u'<file alias="icons/'+ item +'">icons/'+ item +'</file>\n')
# for item in qss:
# f.write(u'<file alias="qss/'+ item +'">qss/'+ item +'</file>\n')
f.write(u'</qresource>\n</RCC>')
f.close()
pipe = subprocess.Popen(r'pyrcc5 -o images.py images.qrc', stdout = subprocess.PIPE, stdin = subprocess.PIPE, stderr = subprocess.PIPE, creationflags=0x08)
导包
在Terminal 里输入下面代码:
pyinstaller -F -w - i icon.ico **.py
其中icon.ico
为exe文件添加图标的文件名, -w
是无黑窗口,即报错时看不到打印信息。
网友评论