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

目录

实例要求

原始pdf文件格式

输出xls文件格式

运行界面

完整代码

代码分析

遍历表格

布局界面

控件简介

写入表格

表格排序

事件循环

异步编程


实例要求

使用PySimpleGUI做一个把单位考勤系统导出的pdf文件合并输出Excel的应用,故事出自:

https://hannyang.blog.csdn.net/article/details/135395946

当时时间紧,没有好好做界面且输出csv文件了事。今天趁周六休息,把代码做一下升级处理,使用库PySimpleGUI做了一个稍微漂亮一点的界面;又用pdfplumber直接遍历多个pdf文件,得到数据后输出Excel文件,比我原本先做合并pdf文件再去取数要快,原先的pdf文件合并操作纯粹有点多余。最后,又尝试对pdf文件读取函数的改造,使用了asyncio异步编程效果非常不错。

下面请听我慢慢道来:

原始pdf文件格式

输出xls文件格式

运行界面

完整代码

import xlwt, pyperclip, asyncio, pdfplumber
import os, time, datetime as dt
import PySimpleGUI as sg# 全局变量
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表格
async def read_table(file):dct = dict()with pdfplumber.open(file) as pdf:for page in pdf.pages:tables = page.extract_tables(table_settings = {})for table in tables:for lst in table:tmp = lst[1:]if not any(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]if lst[0]=='时间':dct[lst[0]] = tmp[0]else:dct[','.join([lst[0],tmp[0]])] = ','.join(tmp[1:])return dct# 写入xls文件
def write_sheet():global data, date, table_head, ErrMessageif ErrMessage[:2] in ('错误','文件'): returnmyxl = xlwt.Workbook()style = xlwt.easyxf('align: wrap yes; align: horiz center; font: bold yes;') sheet = myxl.add_sheet('考勤表')wcol = [20,40,60,30,30,40,40,40,60]for i,w in enumerate(wcol):sheet.col(i).width = w * 80sheet.write_merge(0,0,0,8,'出勤统计报表',style)style = xlwt.easyxf('borders:top thin; borders:bottom thin; borders:left thin; borders:right thin;') sheet.write_merge(1,1,0,2,'考勤日期:'+date[0])for i,head in enumerate(['序号']+table_head.split(',')):sheet.write(2,i,head,style)for i,row in enumerate(data):for j,col in enumerate([str(i+1)]+row):sheet.write(3+i,j,col,style)for i,t in enumerate(SortedType):if t in ErrMessage: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)# 获取当前时间
def strDateTime(diff=0):now = dt.datetime.now()time = now + dt.timedelta(days=diff)    return f'{time.year}{time.month:02}{time.day:02}{time.hour:02}{time.minute:02}{time.second:02}'# 选择并处理文件
async def on_text_changed(event, values):global date, data, path, ErrMessagenew_path = values["-FOLDER-"]window["-FOLDER-"].update(new_path.split('/')[-1])if path==new_path: returnelse: path = new_pathpdfs = [f for f in os.listdir(path) if f.endswith('.pdf') and not f.startswith('PDFmerged')]nums = len(pdfs)if nums==0:ErrMessage = '错误:所选文件夹中没有PDF文件!'window['-BAR-'].update(ErrMessage)window['-DATE-'].update(DateFormat)window['-TABLE-'].update(values=[])returndate, data, sheet = [], [], dict()tasks = []for pdf in pdfs:tasks.append(read_table('/'.join([path,pdf])))ErrMessage = f'文件读取中(共{nums}个PDF文件)......'window['-BAR-'].update(ErrMessage)window.refresh()results = await asyncio.gather(*tasks)for r in results:dt = r.get('时间',None)if dt: date.append(dt)sheet.update(r)if date:window['-DATE-'].update(date[-1])for k,v in sheet.items():if k in ('时间','姓名,所属组织','普通班个人出勤统计报表,'): continuedata.append(','.join([k,v]).split(','))window['-TABLE-'].update(values=data)persons = len(data)departments = len(set([d[1] for d in data]))if 0:#len(set(date))!=1:data = []ErrMessage = f'错误:请检查所选文件存在多个时间段:{",".join(set(date))}'else:ErrMessage = f'考勤人数:{persons} / 部门数:{departments}'window['-BAR-'].update(ErrMessage)# 表格排序
def on_table_sorted(event, data):global ErrMessageif not data: returnslist = ['x[-4][:-1]', 'x[-1]', 'x[-3]', 'x[-2]']style = slist[SortedType.index(event)]data = sorted(data, key=lambda x: float(eval(style)), reverse=True)window['-TABLE-'].update(values=data)ErrMessage = f'已按{event}更新!'window['-BAR-'].update(ErrMessage)# 创建窗口
window = sg.Window("考勤表汇总", layout, finalize=True)# 事件循环
while True:event, values = window.read()if event == sg.WINDOW_CLOSED or event == "退出":breakelif event == "-FOLDER-":asyncio.run(on_text_changed(event, values))elif event in SortedType:on_table_sorted(event, data)elif event == "输出Excel文件":write_sheet()# 关闭窗口
window.close()

代码分析

重点代码都用彩色字体加粗标注了:

遍历表格

读取代码如下:

import pdfplumber

......
    with pdfplumber.open(file) as pdf:
        for page in pdf.pages:
            tables = page.extract_tables(table_settings = {})
            for table in tables:
                for lst in table:
                    # 根据表格实际情况来清洗数据
    return dct

布局界面

import PySimpleGUI as pg

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)]]

控件简介

除了最常用的Text, Input, Button,使用了 FolderBrowse、Table、StatsBar 三个不是最常用的控件,分别是文件夹打开框、表格和状态栏。

表格最重要的三个参数: values, headings, auto_size_columns

sg.Table(values='', headings=table_head.split(','), auto_size_columns=False)

表格数据values和表头headings都列表(分别是二维和一维的),auto_size_columns=False建议不要缺省,否则列宽不可控,各列都自动缩进紧靠在一起。

表格更新数据的方法:window['-TABLE-'].update(values=data)

写入表格

import xlwt

def write_sheet():
    global data, date, table_head, ErrMessage
    if ErrMessage[:2] in ('错误','输出'): return
    myxl = xlwt.Workbook()
    style = xlwt.easyxf('align: wrap yes; align: horiz center; font: bold yes;') 
    sheet = myxl.add_sheet('考勤表')
    wcol = [20,40,60,30,30,40,40,40,60]
    for i,w in enumerate(wcol):
        sheet.col(i).width = w * 80
    sheet.write_merge(0,0,0,8,'出勤统计报表',style)
    style = xlwt.easyxf('borders:top thin; borders:bottom thin; borders:left thin; borders:right thin;') 
    sheet.write_merge(1,1,0,2,'考勤日期:'+date[0])
    for i,head in enumerate(['序号']+table_head.split(',')):
        sheet.write(2,i,head,style)
    for i,row in enumerate(data):
        for j,col in enumerate([str(i+1)]+row):
            sheet.write(3+i,j,col,style)
    for i,t in enumerate(SortedType):
        if t in ErrMessage:
            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 = ["出勤率排序","加班时长排序","迟到次数排序","早退次数排序"]
def on_table_sorted(event, data):
    global ErrMessage
    if not data: return
    slist = ['x[-4][:-1]', 'x[-1]', 'x[-3]', 'x[-2]']
    style = slist[SortedType.index(event)]
    data = sorted(data, key=lambda x: float(eval(style)), reverse=True)
    window['-TABLE-'].update(values=data)
    ErrMessage = f'已按{event}更新!'
    window['-BAR-'].update(ErrMessage)

虽然经常有人诟病eval()函数的安全性,但这里还是用eval()简化表格排序事件,否则要多写很多代码。

事件循环

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED or event == "退出":
        break
    elif event == "-FOLDER-":
        asyncio.run(on_text_changed(event, values))
    elif event in SortedType:
        on_table_sorted(event, data)
    elif event == "输出Excel文件":
        write_sheet()

异步编程

此时,请出本篇的主角“异步编程”,什么是异步编程呢?就是有点多任务操作的意思。

异步编程是一种编程范式,它允许某些操作在等待结果时不阻塞整个程序。在传统的同步编程中,程序会按照顺序执行,一旦遇到需要等待的操作(如文件I/O或网络请求),整个程序就会被阻塞,等待操作完成。而在异步编程中,程序并不会因为某个耗时的IO操作而停下其他所有任务,而是将这个任务交给系统处理,自身继续执行后续的操作,等到IO操作完成后,系统会通知程序进行下一步的处理。

asyncio

在上一段代码中,响应"-FOLDER-"时使用了asyncio.run()函数:

import asyncio
....... ......

while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED or event == "退出":
        break
    elif event == "-FOLDER-":
        asyncio.run(on_text_changed(event, values))

asyncio.run运行的这个是异步编程的主函数,需要用async def来定义:

async def

async def on_text_changed(event, values):
    ......其它代码略......
    tasks = []
    for pdf in pdfs:
        tasks.append(read_table('/'.join([path,pdf])))
    ErrMessage = f'文件读取中(共{nums}个PDF文件)......'
    window['-BAR-'].update(ErrMessage)
    window.refresh()
    results = await asyncio.gather(*tasks)
    for r in results:
       ......遍历取回的被调异步函数返回值的列表......

await

异步主函数中使用 await asyncio.gather(*tasks) 取回被函数的返回结果,返回结果是多个任务的返回值组成的列表;而主函数的任务呢就,是被调函数组成的列表:asks.append(read_table())

同样的,被调函数也需要用async def来定义,它一般都是文件I/O或网络请求等比较耗时的操作:

async def read_table(file):
    dct = dict()
    with pdfplumber.open(file) as pdf:
        # 读取pdf文件 I/O操作
    return dct


源码和2个例表已绑定上传资源,欢迎下载测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/609822.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CDN的介绍以及加速内容传输原理

目前在公司的开发过程中,发现很多存储在oss的静态资源(图片,安装包)的链接中域名都使用了cdn域名,后面了解到这个cdn域名的主要作用是加速资源的访问,于是抽空了解了一下CDN加速原理。 目前使用得比较多的是…

Python多线程同步

同步条件(Event) 在Python中,多线程同步可以通过threading模块中的Event对象来实现。Event对象允许一个或多个线程等待某个事件的发生,当事件发生时,等待的线程将被唤醒。 event.isSet():返回event的状态值 event.wait()&#x…

PyQt5-小总结

之前学习PyQt5,然后那段时间想做一个桌面小程序,后来由于学习内容较多就做了一小部分,但是可以进行页面跳转。大家如果是初学者对Python感兴趣而且刚学数据库时可以看看代码,可能会有点启发。 效果: 登录进来是这&…

FreeRTOS学习——任务通知

一、什么是任务通知 FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队 列&#…

解析游戏开发中的ECS设计模式:实体、组件、系统的完美协同

ECS(Entity-Component-System)是一种设计模式,通常用于构建和管理具有大量实体和复杂交互的系统,尤其在游戏开发中得到广泛应用。这个模式的核心思想是将系统中的组件、实体和系统进行分离,以提高代码的可维护性、可扩…

鸿蒙原生应用/元服务开发-长时任务

概述 功能介绍 应用退至后台后,对于在后台需要长时间运行用户可感知的任务,例如播放音乐、导航等。为防止应用进程被挂起,导致对应功能异常,可以申请长时任务,使应用在后台长时间运行。申请长时任务后,系统…

Linux调试------gdb的使用

目录 前言 一、gdb打开可执行程序 二、查看代码与操作断点 1.l 查看代码 2.b 打断点 3.info b 查看断点信息 4.d 删除断点 5.disable 和 enable 断点的禁用与启用 三、调试 1.r 启动调试 2. n 逐过程 3. s 逐语句 4.display显示变量 5.undisplay 取消显…

e2studio开发磁力计LIS2MDL(1)----轮询获取磁力计数据

e2studio开发磁力计LIS2MDL.1--轮询获取磁力计数据 概述视频教学样品申请源码下载速率新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置UART配置UART属性配置设置e2studio堆栈e2studio的重定向printf设置R_SCI_UART_Open()函数原型回调函数user_uart_callback ()prin…

根据MySql的表名,自动生成实体类,模仿ORM框架

ORM框架可以根据数据库的表自动生成实体类,以及相应CRUD操作 本文是一个自动生成实体类的工具,用于生成Mysql表对应的实体类。 新建Winform窗体应用程序AutoGenerateForm,框架(.net framework 4.5), 添加对System.Configuration的…

高效构建Java应用:Maven入门和进阶(三)

高效构建Java应用:Maven入门和进阶(三) 三. Maven的核心功能和构建管理3.1 依赖管理和配置3.2 依赖传递和冲突3.3 依赖导入失败场景和解决方案3.4 扩展构建管理和插件配置 三. Maven的核心功能和构建管理 3.1 依赖管理和配置 Maven 依赖管理…

CentOS安装Docker(超详细)

CentOS安装Docker 1 知识小课堂2 CentOS安装Docker2.1 1.1.卸载(可选)1.2.安装docker1.3.启动docker1.4.配置镜像加速 3 CentOS安装DockerCompose3.1.下载3.2.修改文件权限3.3.Base自动补全命令: 4 Docker镜像仓库4.1.简化版镜像仓库4.2.带有…

数据分析求职-知识脑图

今天和大家聊聊数据分析求职常见面试题,这是这个系列的第一篇文章,但是我不想开始就直接罗列题目,因为这样的文章实在太多了,同学们的兴趣程度肯定一般。所以,我想先和大家聊聊在准备面试题时候通常遇到的困扰&#xf…

GFP-CERTIFIED®FLUOFORTE®钙离子检测试剂盒

Enzo Life Sciences的GFP-CERTIFIED FLUOFORTE Calcium assay kit提供了一种荧光分析方法,用于检测广泛生物靶标的细胞内钙动员情况。相对于其他商业化的染料,GFP-CERTIFIED FLUOFORTE染料是最亮和最灵敏的荧光钙指示剂。该试剂盒为贴壁和非贴壁细胞系提…

生物信息学中的可重复性研究

科学就其本质而言,是累积渐进的。无论你是使用基于网络的还是基于命令行的工具,在进行研究时都应保证该研究可被其他研究人员重复。这有利于你的工作的累积与进展。在生物信息学领域,这意味着如下内容。 工作流应该有据可查。这可能包括在电脑…

linux 压力测试 AB ApacheBench

ab的简介 ab是apachebench命令的缩写。 ab是apache自带的压力测试工具。ab非常实用,它不仅可以对apache服务器进行网站访问压力测试,也可以对或其它类型的服务器进行压力测试。比如nginx、tomcat、IIS等 ab的原理 ab的原理:ab命令会创建多…

ros架构

ROS(Robot Operating System)是一个灵活的、分布式的系统架构,用于构建机器人软件。它由一系列工具、库和约定组成,提供了一套通用的功能和通信机制,以支持机器人系统的开发、部署和运行。 ROS架构主要包括以下几个核心…

C++面试宝典第17题:找规律填数

题目 仔细观察下面的数字序列,找到规律,并填写空白处的数字。 (1)1, 2, 4, 7, 11, 16, __ (2)-1, 2, 7, 28, __, 126 (3)6, 10, 18, 32, 57, __ (4)19, 6, 1, 2, 11, __ (5)2, 3, 5, 7, 11, __ (6)1, 8, 9, 4, __, 1/6 (7)1, 2, 3, 7, 16, __, 321 (8)1, 2, …

linux异常情况,排查处理中

登录客户环境后,发现一个奇怪情况如下图,之前也遇到过,直接fuser -ck /backup操作的话,主机将会重启,因数据库运行中,等待停机维护时间,同时也在想办法不重启的情况下解决该问题 [rootdb ~]# f…

git撤销提交到本地的commit

有些时候,当我们提交代码到本地后,突然发现因为某些原因需要撤销提交本地的代码。 就比如我,因为代码写错了分支,已经提交到本地了,而我需要取消,并且还要把代码搞得另外的分支上。 提交前: …

SpiderFlow爬虫平台 前台RCE漏洞复现(CVE-2024-0195)

0x01 产品简介 SpiderFlow是新一代爬虫平台,以图形化方式定义爬虫流程,以流程图的方式定义爬虫,不写代码即可完成爬虫,是一个高度灵活可配置的爬虫平台。 0x02 漏洞概述 SpiderFlow爬虫平台src/main/java/org/spiderflow/controller/FunctionController.java文件的Functi…