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

简介: 实例要求:使用PySimpleGUI库做一个把单位考勤系统导出的pdf文件合并输出Excel表格的应用,库pdfplumber直接遍历多个pdf文件,得到数据后输出xks文件,同时对pdf文件读取函数的进行改造,使用了asyncio异步编程效果非常不错。

pp.png


实例要求

使用PySimpleGUI库做一个把单位考勤系统导出的pdf文件合并输出Excel表格的应用,库pdfplumber直接遍历多个pdf文件,得到数据后输出xks文件,同时对pdf文件读取函数的进行改造,使用了asyncio异步编程效果非常不错。

下面请听我慢慢道来:

原始pdf文件格式

image.gif


输出xls文件格式

image.gif


运行界面

image.gif


完整代码

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): continue
                    tmp = [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, 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文件失败!'
    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, ErrMessage
    new_path = values["-FOLDER-"]
    window["-FOLDER-"].update(new_path.split('/')[-1])
    if path==new_path: return
    else: path = new_path
    pdfs = [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=[])
        return
    date, 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 ('时间','姓名,所属组织','普通班个人出勤统计报表,'): continue
        data.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 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)
# 创建窗口
window = sg.Window("考勤表汇总", layout, finalize=True)
# 事件循环
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()
# 关闭窗口
window.close()

image.gif

代码分析

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

遍历表格

读取代码如下:

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运行的这个是异步编程的主函数,需要用asyncdef来定义:

async def

asyncdefon_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 = awaitasyncio.gather(*tasks)

   for r in results:

      ......遍历取回的被调异步函数返回值的列表......

await

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

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

asyncdefread_table(file):

   dct = dict()

   with pdfplumber.open(file) as pdf:

       # 读取pdf文件 I/O操作

   return dct


目录
相关文章
|
10天前
|
Python
Python中的异步编程:使用asyncio和aiohttp实现高效网络请求
【10月更文挑战第34天】在Python的世界里,异步编程是提高效率的利器。本文将带你了解如何使用asyncio和aiohttp库来编写高效的网络请求代码。我们将通过一个简单的示例来展示如何利用这些工具来并发地处理多个网络请求,从而提高程序的整体性能。准备好让你的Python代码飞起来吧!
28 2
|
18天前
|
数据采集 数据安全/隐私保护 开发者
非阻塞 I/O:异步编程提升 Python 应用速度
非阻塞 I/O:异步编程提升 Python 应用速度
|
14天前
|
调度 开发者 Python
Python中的异步编程:理解asyncio库
在Python的世界里,异步编程是一种高效处理I/O密集型任务的方法。本文将深入探讨Python的asyncio库,它是实现异步编程的核心。我们将从asyncio的基本概念出发,逐步解析事件循环、协程、任务和期货的概念,并通过实例展示如何使用asyncio来编写异步代码。不同于传统的同步编程,异步编程能够让程序在等待I/O操作完成时释放资源去处理其他任务,从而提高程序的整体效率和响应速度。
|
11天前
|
Java 测试技术 持续交付
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
本文重点讲解如何搭建App自动化测试框架的思路,而非完整源码。主要内容包括实现目的、框架设计、环境依赖和框架的主要组成部分。适用于初学者,旨在帮助其快速掌握App自动化测试的基本技能。文中详细介绍了从需求分析到技术栈选择,再到具体模块的封装与实现,包括登录、截图、日志、测试报告和邮件服务等。同时提供了运行效果的展示,便于理解和实践。
46 4
【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
|
8天前
|
数据采集 调度 Python
探索Python中的异步编程:从基础到高级
【10月更文挑战第36天】在Python的世界中,异步编程是提升程序性能和响应速度的重要工具。本文将带你深入了解Python异步编程的核心概念,包括事件循环、协程与异步IO,并逐步展示如何在实际项目中应用这些概念来编写更高效、可扩展的代码。通过理论讲解与实践案例的结合,我们将一起构建一个异步Web爬虫,以直观感受异步编程的强大之处。
|
10天前
|
数据库 Python
异步编程不再难!Python asyncio库实战,让你的代码流畅如丝!
在编程中,随着应用复杂度的提升,对并发和异步处理的需求日益增长。Python的asyncio库通过async和await关键字,简化了异步编程,使其变得流畅高效。本文将通过实战示例,介绍异步编程的基本概念、如何使用asyncio编写异步代码以及处理多个异步任务的方法,帮助你掌握异步编程技巧,提高代码性能。
27 4
|
23天前
|
开发者 Python
探索Python中的异步编程
【10月更文挑战第20天】在本文中,我们将深入探讨Python的异步编程,这是一种能够提高程序执行效率的技术。我们将通过代码示例和实际应用场景来理解其工作原理和应用价值。无论你是初学者还是有经验的开发者,这篇文章都将为你提供新的视角和实用技巧。
|
19天前
|
数据库 开发者 Python
“Python异步编程革命:如何从编程新手蜕变为并发大师,掌握未来技术的制胜法宝”
【10月更文挑战第25天】介绍了Python异步编程的基础和高级技巧。文章从同步与异步编程的区别入手,逐步讲解了如何使用`asyncio`库和`async`/`await`关键字进行异步编程。通过对比传统多线程,展示了异步编程在I/O密集型任务中的优势,并提供了最佳实践建议。
16 1
|
21天前
|
调度 开发者 Python
异步编程在Python中的应用:Asyncio和Coroutines
异步编程在Python中的应用:Asyncio和Coroutines
18 1
|
23天前
|
关系型数据库 MySQL 数据处理
探索Python中的异步编程:从asyncio到异步数据库操作
在这个快节奏的技术世界里,效率和性能是关键。本文将带你深入Python的异步编程世界,从基础的asyncio库开始,逐步探索到异步数据库操作的高级应用。我们将一起揭开异步编程的神秘面纱,探索它如何帮助我们提升应用程序的性能和响应速度。