找回密码
 会员注册
查看: 15|回复: 0

Python异步编程|PySimpleGUI界面读取PDF转换Excel

[复制链接]

2

主题

0

回帖

7

积分

新手上路

积分
7
发表于 2024-9-7 21:09:17 | 显示全部楼层 |阅读模式
目录实例要求原始pdf文件格式输出xls文件格式运行界面完整代码代码分析遍历表格布局界面控件简介写入表格表格排序事件循环异步编程实例要求使用PySimpleGUI做一个把单位考勤系统导出的pdf文件合并输出Excel的应用,故事出自:https://hannyang.blog.csdn.net/article/details/135395946当时时间紧,没有好好做界面且输出csv文件了事。今天趁周六休息,把代码做一下升级处理,使用库PySimpleGUI做了一个稍微漂亮一点的界面;又用pdfplumber直接遍历多个pdf文件,得到数据后输出Excel文件,比我原本先做合并pdf文件再去取数要快,原先的pdf文件合并操作纯粹有点多余。最后,又尝试对pdf文件读取函数的改造,使用了asyncio异步编程效果非常不错。下面请听我慢慢道来:原始pdf文件格式输出xls文件格式运行界面完整代码importxlwt,pyperclip,asyncio,pdfplumberimportos,time,datetimeasdtimportPySimpleGUIassg#全局变量table_head='姓名,部门,应到,实到,出勤率,迟到次数,早退次数,加班(分钟)'path,font='',('宋体',12)date,data=[],[]DateFormat='..-..'ErrMessage='错误'SortedType=["出勤率排序","加班时长排序","迟到次数排序","早退次数排序"]#定义布局layout=[[sg.Text("昆山分行考勤表",font=('',16)),sg.Text(pad=(132,10)),sg.Text("请选择考勤文件:",font=font),sg.Input(key="-FOLDER-",enable_events=True,readonly=True,font=font,size=18),sg.FolderBrowse(button_text='...',enable_events=True,initial_folder='./')],[sg.Text("考勤日期:",font=font),sg.Text(DateFormat,key='-DATE-',font=font)],[sg.Table(values='',headings=table_head.split(','),key='-TABLE-',auto_size_columns=False,justification='left',num_rows=10)],[sg.Button("输出Excel文件",size=(12,1),pad=(15,30)),sg.Button(SortedType[0],enable_events=True,size=(10,1),pad=(15,30)),sg.Button(SortedType[1],enable_events=True,size=(10,1),pad=(15,30)),sg.Button(SortedType[2],enable_events=True,size=(10,1),pad=(15,30)),sg.Button(SortedType[3],enable_events=True,size=(10,1),pad=(15,30)),sg.Button("退出",size=(10,1),pad=(15,30))],[sg.StatusBar('',key="-BAR-",font=font,size=92)]]#读取pdf表格asyncdefread_table(file):dct=dict()withpdfplumber.open(file)aspdf:forpageinpdf.pages:tables=page.extract_tables(table_settings={})fortableintables:forlstintable:tmp=lst[1:]ifnotany(tmp):continuetmp=[tmp[0]]+tmp[3:8]+[tmp[-1]]tmp[0]=tmp[0].replace('\n','')tmp[0]=tmp[0].split('/')tmp[0]=tmp[0][-1]iflst[0]=='时间':dct[lst[0]]=tmp[0]else:dct[','.join([lst[0],tmp[0]])]=','.join(tmp[1:])returndct#写入xls文件defwrite_sheet():globaldata,date,table_head,ErrMessageifErrMessage[:2]in('错误','文件'):returnmyxl=xlwt.Workbook()style=xlwt.easyxf('align:wrapyes;align:horizcenter;font:boldyes;')sheet=myxl.add_sheet('考勤表')wcol=[20,40,60,30,30,40,40,40,60]fori,winenumerate(wcol):sheet.col(i).width=w*80sheet.write_merge(0,0,0,8,'出勤统计报表',style)style=xlwt.easyxf('borders:topthin;borders:bottomthin;borders:leftthin;borders:rightthin;')sheet.write_merge(1,1,0,2,'考勤日期:'+date[0])fori,headinenumerate(['序号']+table_head.split(',')):sheet.write(2,i,head,style)fori,rowinenumerate(data):forj,colinenumerate([str(i+1)]+row):sheet.write(3+i,j,col,style)fori,tinenumerate(SortedType):iftinErrMessage:tmp=SortedType[i]breakelse:tmp=""excel_file=f'昆山分行考勤表{date[0]}({tmp}{strDateTime()}).xls'ErrMessage=f'文件输出为:{excel_file}'try:myxl.save(excel_file)except:ErrMessage='写入excel文件失败!'finally:pyperclip.copy('\\'.join((os.getcwd(),excel_file)))window['-BAR-'].update(ErrMessage)#获取当前时间defstrDateTime(diff=0):now=dt.datetime.now()time=now+dt.timedelta(days=diff)returnf'{time.year}{time.month:02}{time.day:02}{time.hour:02}{time.minute:02}{time.second:02}'#选择并处理文件asyncdefon_text_changed(event,values):globaldate,data,path,ErrMessagenew_path=values["-FOLDER-"]window["-FOLDER-"].update(new_path.split('/')[-1])ifpath==new_path:returnelse:path=new_pathpdfs=[fforfinos.listdir(path)iff.endswith('.pdf')andnotf.startswith('PDFmerged')]nums=len(pdfs)ifnums==0:ErrMessage='错误:所选文件夹中没有PDF文件!'window['-BAR-'].update(ErrMessage)window['-DATE-'].update(DateFormat)window['-TABLE-'].update(values=[])returndate,data,sheet=[],[],dict()tasks=[]forpdfinpdfs:tasks.append(read_table('/'.join([path,pdf])))ErrMessage=f'文件读取中(共{nums}个PDF文件)......'window['-BAR-'].update(ErrMessage)window.refresh()results=awaitasyncio.gather(*tasks)forrinresults:dt=r.get('时间',None)ifdt:date.append(dt)sheet.update(r)ifdate:window['-DATE-'].update(date[-1])fork,vinsheet.items():ifkin('时间','姓名,所属组织','普通班个人出勤统计报表,'):continuedata.append(','.join([k,v]).split(','))window['-TABLE-'].update(values=data)persons=len(data)departments=len(set([d[1]fordindata]))if0:#len(set(date))!=1:data=[]ErrMessage=f'错误:请检查所选文件存在多个时间段:{",".join(set(date))}'else:ErrMessage=f'考勤人数:{persons}/部门数:{departments}'window['-BAR-'].update(ErrMessage)#表格排序defon_table_sorted(event,data):globalErrMessageifnotdata:returnslist=['x[-4][:-1]','x[-1]','x[-3]','x[-2]']style=slist[SortedType.index(event)]data=sorted(data,key=lambdax:float(eval(style)),reverse=True)window['-TABLE-'].update(values=data)ErrMessage=f'已按{event}更新!'window['-BAR-'].update(ErrMessage)#创建窗口window=sg.Window("考勤表汇总",layout,finalize=True)#事件循环whileTrue:event,values=window.read()ifevent==sg.WINDOW_CLOSEDorevent=="退出":breakelifevent=="-FOLDER-":asyncio.run(on_text_changed(event,values))elifeventinSortedTypen_table_sorted(event,data)elifevent=="输出Excel文件":write_sheet()#关闭窗口window.close()代码分析重点代码都用彩色字体加粗标注了:遍历表格读取代码如下:importpdfplumber......  withpdfplumber.open(file)aspdf:    forpageinpdf.pages:      tables=page.extract_tables(table_settings={})      fortableintables:        forlstintable:          #根据表格实际情况来清洗数据  returndct布局界面importPySimpleGUIaspglayout=[  [sg.Text("昆山分行考勤表",font=('',16)),   sg.Text(pad=(132,10)),   sg.Text("请选择考勤文件:",font=font),   sg.Input(key="-FOLDER-",enable_events=True,readonly=True,font=font,size=18),   sg.FolderBrowse(button_text='...',enable_events=True,initial_folder='./')   ],  [sg.Text("考勤日期:",font=font),   sg.Text(DateFormat,key='-DATE-',font=font)   ],  [sg.Table(values='',       headings=table_head.split(','),       key='-TABLE-',       auto_size_columns=False,       justification='left',       num_rows=10)],  [sg.Button("输出Excel文件",size=(12,1),pad=(15,30)),   sg.Button(SortedType[0],enable_events=True,size=(10,1),pad=(15,30)),   sg.Button(SortedType[1],enable_events=True,size=(10,1),pad=(15,30)),   sg.Button(SortedType[2],enable_events=True,size=(10,1),pad=(15,30)),   sg.Button(SortedType[3],enable_events=True,size=(10,1),pad=(15,30)),   sg.Button("退出",size=(10,1),pad=(15,30))],  [sg.StatusBar('',key="-BAR-",font=font,size=92)]]控件简介除了最常用的Text,Input,Button,使用了 FolderBrowse、Table、StatsBar三个不是最常用的控件,分别是文件夹打开框、表格和状态栏。表格最重要的三个参数:values,headings, auto_size_columnssg.Table(values='', headings=table_head.split(','), auto_size_columns=False)表格数据values和表头headings都列表(分别是二维和一维的),auto_size_columns=False建议不要缺省,否则列宽不可控,各列都自动缩进紧靠在一起。表格更新数据的方法:window['-TABLE-'].update(values=data)写入表格importxlwtdefwrite_sheet():  globaldata,date,table_head,ErrMessage  ifErrMessage[:2]in('错误','输出'):return  myxl=xlwt.Workbook()  style=xlwt.easyxf('align:wrapyes;align:horizcenter;font:boldyes;')   sheet=myxl.add_sheet('考勤表')  wcol=[20,40,60,30,30,40,40,40,60]  fori,winenumerate(wcol):    sheet.col(i).width=w*80  sheet.write_merge(0,0,0,8,'出勤统计报表',style)  style=xlwt.easyxf('borders:topthin;borders:bottomthin;borders:leftthin;borders:rightthin;')   sheet.write_merge(1,1,0,2,'考勤日期:'+date[0])  fori,headinenumerate(['序号']+table_head.split(',')):    sheet.write(2,i,head,style)  fori,rowinenumerate(data):    forj,colinenumerate([str(i+1)]+row):      sheet.write(3+i,j,col,style)  fori,tinenumerate(SortedType):    iftinErrMessage:      tmp=SortedType[i]      break  else:tmp=""  excel_file=f'昆山分行考勤表{date[0]}({tmp}{strDateTime()}).xls'  ErrMessage=f'输出文件为:{excel_file}'  try:    myxl.save(excel_file)  except:    ErrMessage='写入excel文件失败!'注意单格和多个单元格的写入区别: sheet.write()  sheet.write_merge()表格排序SortedType=["出勤率排序","加班时长排序","迟到次数排序","早退次数排序"]defon_table_sorted(event,data):  globalErrMessage  ifnotdata:return  slist=['x[-4][:-1]','x[-1]','x[-3]','x[-2]']  style=slist[SortedType.index(event)]  data=sorted(data,key=lambdax:float(eval(style)),reverse=True)  window['-TABLE-'].update(values=data)  ErrMessage=f'已按{event}更新!'  window['-BAR-'].update(ErrMessage)虽然经常有人诟病eval()函数的安全性,但这里还是用eval()简化表格排序事件,否则要多写很多代码。事件循环whileTrue:  event,values=window.read()  ifevent==sg.WINDOW_CLOSEDorevent=="退出":    break  elifevent=="-FOLDER-":    asyncio.run(on_text_changed(event,values))  elifeventinSortedType:    on_table_sorted(event,data)  elifevent=="输出Excel文件":    write_sheet()异步编程此时,请出本篇的主角“异步编程”,什么是异步编程呢?就是有点多任务操作的意思。异步编程是一种编程范式,它允许某些操作在等待结果时不阻塞整个程序。在传统的同步编程中,程序会按照顺序执行,一旦遇到需要等待的操作(如文件I/O或网络请求),整个程序就会被阻塞,等待操作完成。而在异步编程中,程序并不会因为某个耗时的IO操作而停下其他所有任务,而是将这个任务交给系统处理,自身继续执行后续的操作,等到IO操作完成后,系统会通知程序进行下一步的处理。asyncio在上一段代码中,响应"-FOLDER-"时使用了asyncio.run()函数:importasyncio.............whileTrue:  event,values=window.read()  ifevent==sg.WINDOW_CLOSEDorevent=="退出":    break  elifevent=="-FOLDER-":    asyncio.run(on_text_changed(event,values))asyncio.run运行的这个是异步编程的主函数,需要用asyncdef来定义:asyncdefasyncdefon_text_changed(event,values):  ......其它代码略......  tasks=[]  forpdfinpdfs:    tasks.append(read_table('/'.join([path,pdf])))  ErrMessage=f'文件读取中(共{nums}个PDF文件)......'  window['-BAR-'].update(ErrMessage)  window.refresh()  results=awaitasyncio.gather(*tasks)  forrinresults:    ......遍历取回的被调异步函数返回值的列表......await异步主函数中使用 awaitasyncio.gather(*tasks)取回被函数的返回结果,返回结果是多个任务的返回值组成的列表;而主函数的任务呢就,是被调函数组成的列表:asks.append(read_table())同样的,被调函数也需要用asyncdef来定义,它一般都是文件I/O或网络请求等比较耗时的操作:asyncdefread_table(file):  dct=dict()  withpdfplumber.open(file)aspdf:    #读取pdf文件I/O操作  returndct源码和2个例表已绑定上传资源,欢迎下载测试。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 05:57 , Processed in 0.727330 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表