在日常工作中,团队的绩效监控和管理是确保项目顺利进行的重要环节。然而,面临着以下问题:
本文将介绍如何利用免费的软件和工具(如 Python、MySQL、Excel 等)实现绩效看板的自动化。通过 邮件自动推送和接收 实现数据采集,结合 MySQL 数据库的沉淀和计算,最终在 Excel 中实现数据的定时刷新和展示。
先来看效果, 需要知道整体团队的进度,团队的进度要通过自动化邮件发送接收,并且每半个小时更新数据。
为了实现本项目的自动化流程,以下是所需的软件和工具:
Python 3.x:
MySQL 数据库:
Excel:
ODBC 驱动程序:
邮件客户端(如 Outlook):
Python 库:
pandas
:用于数据处理。mysql-connector-python
:用于连接 MySQL 数据库。schedule
:用于定时任务。imaplib
和 email
:用于处理邮件接收。win32com
:用于操作 Outlook。bash
pip install pandas mysql-connector-python schedule
我们可以利用免费的软件和工具(如 Python、MySQL、Excel 等)实现绩效看板的自动化。
这种方法不仅合法且免费,还能显著提高团队的绩效管理效率。
为了实现 Excel 绩效看板的自动化,整个流程可以分为以下几个步骤:
schedule
库)自动执行。示例代码:
这段 Python 代码的主要功能是通过定时任务调度(使用 schedule
库)来执行一系列的批处理文件(.bat
文件),并对系统窗口进行管理和监控。以下是代码的主要功能模块:
窗口管理:
定时任务调度:
schedule
库设置定时任务,在指定时间运行特定的批处理文件。批处理文件执行:
subprocess.Popen
执行批处理文件,并在新控制台中运行。日期检查:
主循环:
python schedule.py
import calendar
import time
from datetime import datetime, timedelta
import schedule
import os
import subprocess
import win32gui
import win32con
path_newfssc = r'D:\\FsscProject\\lsh\\newfssc\\'
findtxt = 'C:\Windows\System32\cmd.exe'
findtxt2 = 'C:\WINDOWS\system32\cmd.exe'
def check_window():
hd = win32gui.GetDesktopWindow()
# 获取所有子窗口
hwndChildList = []
# EnumChildWindows 为指定的父窗口枚举子窗口
win32gui.EnumChildWindows(hd, lambda hwnd, param: param.append(hwnd), hwndChildList)
for hwnd in hwndChildList:
title = win32gui.GetWindowText(hwnd)
find = False
# print("句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
if findtxt in title:
find = True
if findtxt2 in title:
find = True
if find == True:
return True
return False
def close_window():
hd = win32gui.GetDesktopWindow()
# 获取所有子窗口
hwndChildList = []
# EnumChildWindows 为指定的父窗口枚举子窗口
win32gui.EnumChildWindows(hd, lambda hwnd, param: param.append(hwnd), hwndChildList)
for hwnd in hwndChildList:
title = win32gui.GetWindowText(hwnd)
find = False
# print("句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
if findtxt in title:
find = True
if findtxt2 in title:
find = True
if 'Chromium' in title:
find = True
if find == True:
try:
print("special句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
except:
print("can not erase window")
def TrService():
print_time('dashboard.py is running at ')
p = subprocess.Popen(r'd:\\dashboard.bat',creationflags=subprocess.CREATE_NEW_CONSOLE)
# schedule.every().day.at('09:00').do(TrService)
# schedule.every().day.at('10:00').do(TrService)
# schedule.every().day.at('11:00').do(TrService)
# schedule.every().day.at('12:00').do(TrService)
# schedule.every().day.at('13:00').do(TrService)
# schedule.every().day.at('14:00').do(TrService)
# schedule.every().day.at('15:00').do(TrService)
# schedule.every().day.at('16:00').do(TrService)
# schedule.every().day.at('17:00').do(TrService)
print_time('SCHEDULE is running at ')
while True:
schedule.run_pending()
time.sleep(1)
脚本必须使用另外的 CMD 执行,因为 python schedule.py 会占用一个 cmd, 所以这个 schedule 的设计很重要!每次执行时 ,通过 bat 脚本新增 cmd 视窗执行 另外的python。
python D:\FsscProject\lsh\dashbaord\dashboard.py
示例代码:
python
import datetime
import time
import smtplib
import win32com.client
import schedule
import pymysql
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from prettytable import PrettyTable
import pandas as pd
import os
path = r'D:\\dashboard\\'
time_obj = datetime.datetime.now()
str_time = time_obj.strftime('%Y%m%d %H%M')
excelName_wl = 'wl' + str_time + '.xlsx'
att_file_wl = os.path.join(path, excelName_wl)
excelName_11 = 'nd1100' + str_time + '.xlsx'
att_file_11 = os.path.join(path, excelName_11)
def getdkList():
try:
conn = getECSPrdMysqlConn()
sql = " SELECT * FROM v_db_wl; "
df = pd.read_sql(sql, conn)
df.to_excel(att_file_wl, na_rep='', index=False)
sql11 = " SELECT * FROM v_nd1100_open_list; "
df11 = pd.read_sql(sql11, conn)
df11.to_excel(att_file_11, na_rep='', index=False)
except Exception as e:
print('处理失败:', e)
print(e)
finally:
conn.close()
to_attr = '[email protected]'
subject = 'nd1100监测数据报告'
msg= """尊敬的用户您好:
Dashboard监控数据统计如下,如有异常,详细明细请参考附件。
系统邮件,请勿回复
Best regards
"""
try:
outlook = win32com.client.Dispatch('outlook.application')
for account in outlook.Session.Accounts:
# 赋值发件账户
send_account = account
break
mail = outlook.CreateItem(0)
mail._oleobj_.Invoke(*(64209, 0, 8, 0, send_account))
mail.To = to_attr
mail.Subject = subject
mail.Body = msg
mail.Attachments.Add(att_file_wl)
mail.Attachments.Add(att_file_11)
mail.send
print('发送邮件:' + subject)
# print('收件人:' + item[2] + ' | 被抄送人:' + item[3])
# send_email(to_rep,to_cc, subjectxt, msg, file_excel_name)
print('执行完成')
print("------------------done----------------")
except smtplib.SMTPException as e:
print(e)
print("Error: 无法发送邮件")
def getECSPrdMysqlConn():
"""
获取:RPA数据库连接
:return:
"""
strHost = ''
# strHost = 'xxxx'
strPort = 3306
strUserName = 'xxxx'
strPassWord = 'xxxx'
strdb = 'ecs_fssc'
retry_count = 1000000
init_connect_count = 0
connect_res = True
while connect_res and init_connect_count < retry_count:
try:
conn = pymysql.connect(host=strHost, port=strPort, db=strdb, user=strUserName, password=strPassWord, charset="utf8")
connect_res = False
except pymysql.Error as e:
init_connect_count += 1
print("第:" + str(init_connect_count) + "次数据库连接失败,尝试重连...,错误信息:{0}".format(e))
return conn
getdkList()
这段 Python 代码的主要功能是从 MySQL 数据库中提取数据,将数据保存为 Excel 文件,并通过 Outlook 发送包含这些 Excel 文件的邮件。以下是代码的主要功能模块:
数据库连接与数据提取:
pymysql
连接到 MySQL 数据库。邮件发送:
win32com.client
操作 Outlook 应用程序。定时任务(未启用):
schedule
库实现定时运行。错误处理:
python
import datetime
import time
import smtplib
import win32com.client
import schedule
import pymysql
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from prettytable import PrettyTable
import pandas as pd
import os
datetime
和 time
**:用于处理日期和时间。smtplib
**:用于发送邮件(未在代码中使用)。win32com.client
**:用于操作 Windows 系统的 Outlook 应用程序。schedule
**:用于定时任务调度(未在代码中使用)。pymysql
**:用于连接 MySQL 数据库。email.mime
**:用于构造邮件内容(未在代码中使用)。prettytable
**:用于生成表格(未在代码中使用)。pandas
**:用于处理和保存数据到 Excel 文件。os
**:用于文件路径操作。python
path = r'D:\\dashboard\\'
time_obj = datetime.datetime.now()
str_time = time_obj.strftime('%Y%m%d %H%M')
excelName_wl = 'wl' + str_time + '.xlsx'
att_file_wl = os.path.join(path, excelName_wl)
excelName_11 = 'nd1100' + str_time + '.xlsx'
att_file_11 = os.path.join(path, excelName_11)
path
**:定义 Excel 文件的存储路径。time_obj
**:获取当前时间。str_time
**:将当前时间格式化为 YYYYMMDD HHMM
的字符串。excelName_wl
和 excelName_11
**:定义两个 Excel 文件的名称,包含时间戳。att_file_wl
和 att_file_11
**:定义两个 Excel 文件的完整路径。python
def getECSPrdMysqlConn():
"""
获取:RPA数据库连接
:return:
"""
strHost = ''
strPort = 3306
strUserName = 'xxxx'
strPassWord = 'xxxx'
strdb = 'ecs_fssc'
retry_count = 1000000
init_connect_count = 0
connect_res = True
while connect_res and init_connect_count < retry_count:
try:
conn = pymysql.connect(host=strHost, port=strPort, db=strdb, user=strUserName, password=strPassWord, charset="utf8")
connect_res = False
except pymysql.Error as e:
init_connect_count += 1
print("第:" + str(init_connect_count) + "次数据库连接失败,尝试重连...,错误信息:{0}".format(e))
return conn
pymysql.connect
方法连接数据库。retry_count
次。python
def getdkList():
try:
conn = getECSPrdMysqlConn()
sql = " SELECT * FROM v_db_wl; "
df = pd.read_sql(sql, conn)
df.to_excel(att_file_wl, na_rep='', index=False)
sql11 = " SELECT * FROM v_nd1100_open_list; "
df11 = pd.read_sql(sql11, conn)
df11.to_excel(att_file_11, na_rep='', index=False)
except Exception as e:
print('处理失败:', e)
print(e)
finally:
conn.close()
getECSPrdMysqlConn
获取数据库连接。DataFrame
)。to_excel
方法将数据框保存为 Excel 文件。python
to_attr = '[email protected]'
subject = 'nd1100监测数据报告'
msg= """尊敬的用户您好:
Dashboard监控数据统计如下,如有异常,详细明细请参考附件。
系统邮件,请勿回复
Best regards
"""
try:
outlook = win32com.client.Dispatch('outlook.application')
for account in outlook.Session.Accounts:
send_account = account
break
mail = outlook.CreateItem(0)
mail._oleobj_.Invoke(*(64209, 0, 8, 0, send_account))
mail.To = to_attr
mail.Subject = subject
mail.Body = msg
mail.Attachments.Add(att_file_wl)
mail.Attachments.Add(att_file_11)
mail.send
print('发送邮件:' + subject)
except smtplib.SMTPException as e:
print(e)
print("Error: 无法发送邮件")
win32com.client.Dispatch
创建 Outlook 应用程序对象。Attachments.Add
方法附加 Excel 文件。mail.send
发送邮件。python
getdkList()
getdkList
函数,执行数据提取和邮件发送的完整流程。在python代码中分别有两段sql 语句,获取绩效看板所需要的
sql = " SELECT * FROM v_db_wl; "
sql11 = " SELECT * FROM v_nd1100_open_list; "
解释如下
CREATE VIEW
v_db_wl
(
审单日,
业务类型,
审单人,
单量
) AS
SELECT
`a`.`审单日` AS `审单日`,
`a`.`业务类型` AS `业务类型`,
`a`.`审单人` AS `审单人`,
COUNT(1) AS `单量`
FROM
`v_dash_wl_nd1100` `a`
WHERE
(
`a`.`审单日` >= (curdate() - interval 31 DAY))
GROUP BY
`a`.`审单日`,
`a`.`业务类型`,
`a`.`审单人`;
这段 SQL 代码的目的是创建一个名为 v_db_wl
的视图(View),用于统计过去 31 天内的审单数据。视图是一个虚拟表,基于 SQL 查询的结果集,用户可以像操作普通表一样操作视图。
以下是代码的详细解析:
sql
CREATE VIEW v_db_wl (
审单日,
业务类型,
审单人,
单量
) AS
CREATE VIEW
**:用于创建视图。v_db_wl
**:视图的名称。(审单日, 业务类型, 审单人, 单量)
**:定义视图的列名。AS
**:表示视图的内容由后续的 SELECT
查询定义。sql
SELECT
`a`.`审单日` AS `审单日`,
`a`.`业务类型` AS `业务类型`,
`a`.`审单人` AS `审单人`,
COUNT(1) AS `单量`
SELECT
**:从表中选择数据。a.审单日 AS 审单日
**:从表 v_dash_wl_nd1100
中选择列 审单日
,并将其命名为视图中的列 审单日
。COUNT(1) AS 单量
**:统计每个分组中的记录数,并将其命名为视图中的列 单量
。sql
FROM
`v_dash_wl_nd1100` `a`
v_dash_wl_nd1100
**:数据来源于另一个视图(或表)。a
**:为表 v_dash_wl_nd1100
设置别名,方便后续引用。sql
WHERE
(
`a`.`审单日` >= (curdate() - interval 31 DAY)
)
WHERE
**:用于过滤数据。a.审单日 >= (curdate() - interval 31 DAY)
**:
curdate()
**:返回当前日期(不包含时间部分)。interval 31 DAY
**:表示 31 天的时间间隔。curdate() - interval 31 DAY
**:计算当前日期之前的第 31 天。a.审单日 >= (curdate() - interval 31 DAY)
**:筛选出 审单日
在当前日期前 31 天内的记录。sql
GROUP BY
`a`.`审单日`,
`a`.`业务类型`,
`a`.`审单人`;
GROUP BY
**:将数据按指定的列分组。a.审单日, a.业务类型, a.审单人
**:
审单日
、业务类型
和 审单人
进行分组。v_db_wl
。审单日
:审单日期。业务类型
:业务的类型。审单人
:执行审单的人员。单量
:每个审单日、业务类型和审单人组合的记录数。v_dash_wl_nd1100
中提取数据。审单日
在当前日期前 31 天内的记录。审单日
、业务类型
和 审单人
分组,统计每组的记录数。该视图的主要用途是统计过去 31 天内每天的审单情况,具体包括:
通过这个视图,用户可以快速查询过去 31 天内的审单数据,而无需每次都编写复杂的查询语句。
CREATE VIEW
v_nd1100_open_list
(
OUT_DATE,
CREATE_DATE,
BILL_CODE,
表單類型,
流程类型,
业务类型代码,
业务类型,
SHARE_TASK_STATUS
) AS
SELECT
`st`.`OUT_DATE` AS `OUT_DATE`,
`st`.`CREATE_DATE` AS `CREATE_DATE`,
`bm`.`BILL_CODE` AS `BILL_CODE`,
`f_c`(`bm`.`BILL_DEFINE_ID`) AS `表單類型`,
`F_GETN`(`bm`.`F_LCLX`) AS `流程类型`,
concat('YN',`F_GETC`(`bm`.`F_YWLX`)) AS `业务类型代码`,
`F_GETN`(`bm`.`F_YWLX`) AS `业务类型`,
`st`.`SHARE_TASK_STATUS` AS `SHARE_TASK_STATUS`
FROM
(`t_share_runtime_task` `st`
JOIN
`t_bill_main_area` `bm`
ON
((
`st`.`RUN_OBJECT_ID` = `bm`.`BILL_MAIN_ID`)))
WHERE
((
-(-(`bm`.`BILL_CODE`)) = 'PA03250303016194')
AND (
`st`.`SHARE_TASK_TYPE` = 'SHARE_ACCOUNTANT_FIRST_APPROVAL')
AND (
`st`.`SHARE_TASK_STATUS` IN ('DISPATCHING',
'OPEN')));
这段 SQL 代码的目的是创建一个名为 v_nd1100_open_list
的视图(View),用于查询特定条件下的任务数据。视图是一个虚拟表,基于 SQL 查询的结果集,用户可以像操作普通表一样操作视图。
以下是代码的详细解析:
sql
CREATE VIEW v_nd1100_open_list (
OUT_DATE,
CREATE_DATE,
BILL_CODE,
表單類型,
流程类型,
业务类型代码,
业务类型,
SHARE_TASK_STATUS
) AS
CREATE VIEW
**:用于创建视图。v_nd1100_open_list
**:视图的名称。(OUT_DATE, CREATE_DATE, BILL_CODE, 表單類型, 流程类型, 业务类型代码, 业务类型, SHARE_TASK_STATUS)
**:定义视图的列名。AS
**:表示视图的内容由后续的 SELECT
查询定义。sql
SELECT
`st`.`OUT_DATE` AS `OUT_DATE`,
`st`.`CREATE_DATE` AS `CREATE_DATE`,
`bm`.`BILL_CODE` AS `BILL_CODE`,
`f_c`(`bm`.`BILL_DEFINE_ID`) AS `表單類型`,
`F_GETN`(`bm`.`F_LCLX`) AS `流程类型`,
concat('YN',`F_GETC`(`bm`.`F_YWLX`)) AS `业务类型代码`,
`F_GETN`(`bm`.`F_YWLX`) AS `业务类型`,
`st`.`SHARE_TASK_STATUS` AS `SHARE_TASK_STATUS`
SELECT
**:从表中选择数据。st.OUT_DATE
:从表 t_share_runtime_task
中选择列 OUT_DATE
,并将其命名为视图中的列 OUT_DATE
。st.CREATE_DATE
:从表 t_share_runtime_task
中选择列 CREATE_DATE
,并将其命名为视图中的列 CREATE_DATE
。bm.BILL_CODE
:从表 t_bill_main_area
中选择列 BILL_CODE
,并将其命名为视图中的列 BILL_CODE
。f_c(bm.BILL_DEFINE_ID)
:调用函数 f_c
,传入 BILL_DEFINE_ID
列的值,结果命名为 表單類型
。F_GETN(bm.F_LCLX)
:调用函数 F_GETN
,传入 F_LCLX
列的值,结果命名为 流程类型
。concat('YN', F_GETC(bm.F_YWLX))
:将字符串 'YN'
和函数 F_GETC
的返回值拼接,结果命名为 业务类型代码
。F_GETN(bm.F_YWLX)
:调用函数 F_GETN
,传入 F_YWLX
列的值,结果命名为 业务类型
。st.SHARE_TASK_STATUS
:从表 t_share_runtime_task
中选择列 SHARE_TASK_STATUS
,并将其命名为视图中的列 SHARE_TASK_STATUS
。sql
FROM
(`t_share_runtime_task` `st`
JOIN
`t_bill_main_area` `bm`
ON
(`st`.`RUN_OBJECT_ID` = `bm`.`BILL_MAIN_ID`))
FROM
**:指定数据来源。t_share_runtime_task
**:主表,别名为 st
。t_bill_main_area
**:从表,别名为 bm
。JOIN
**:表示内连接(INNER JOIN),只返回两个表中匹配的记录。ON st.RUN_OBJECT_ID = bm.BILL_MAIN_ID
**:连接条件,表示 t_share_runtime_task
表的 RUN_OBJECT_ID
列与 t_bill_main_area
表的 BILL_MAIN_ID
列相等。sql
WHERE
((-(-(`bm`.`BILL_CODE`)) = 'PA03250303016194')
AND (`st`.`SHARE_TASK_TYPE` = 'SHARE_ACCOUNTANT_FIRST_APPROVAL')
AND (`st`.`SHARE_TASK_STATUS` IN ('DISPATCHING', 'OPEN')));
WHERE
**:用于过滤数据。-(-bm.BILL_CODE) = 'PA03250303016194'
**:
-(-bm.BILL_CODE)
是多余的,等价于 bm.BILL_CODE = 'PA03250303016194'
。BILL_CODE
列值为 'PA03250303016194'
的记录。st.SHARE_TASK_TYPE = 'SHARE_ACCOUNTANT_FIRST_APPROVAL'
**:
SHARE_TASK_TYPE
列值为 'SHARE_ACCOUNTANT_FIRST_APPROVAL'
的记录。st.SHARE_TASK_STATUS IN ('DISPATCHING', 'OPEN')
**:
SHARE_TASK_STATUS
列值为 'DISPATCHING'
或 'OPEN'
的记录。v_nd1100_open_list
。OUT_DATE
:任务完成日期。CREATE_DATE
:任务创建日期。BILL_CODE
:单据代码。表單類型
:单据类型(通过函数 f_c
计算)。流程类型
:流程类型(通过函数 F_GETN
计算)。业务类型代码
:业务类型代码(通过函数 F_GETC
和字符串拼接计算)。业务类型
:业务类型(通过函数 F_GETN
计算)。SHARE_TASK_STATUS
:任务状态。t_share_runtime_task
。t_bill_main_area
。st.RUN_OBJECT_ID = bm.BILL_MAIN_ID
。BILL_CODE = 'PA03250303016194'
。SHARE_TASK_TYPE = 'SHARE_ACCOUNTANT_FIRST_APPROVAL'
。SHARE_TASK_STATUS IN ('DISPATCHING', 'OPEN')
。该视图的主要用途是查询特定单据代码(BILL_CODE = 'PA03250303016194'
)下,任务类型为 'SHARE_ACCOUNTANT_FIRST_APPROVAL'
且任务状态为 'DISPATCHING'
或 'OPEN'
的任务数据。
通过这个视图,用户可以快速获取符合条件的任务信息,而无需每次都编写复杂的查询语句。
1.1 -1.4 的过程 是在远端的主机,获取数据推送
定时任务示例代码,这里就不展开说明了:
import calendar
import time
from datetime import datetime, timedelta
import schedule
import os
import subprocess
import win32gui
import win32con
path_newfssc = r'/lsh/newfssc\\'
findtxt = 'C:\Windows\System32\cmd.exe'
findtxt2 = 'C:\WINDOWS\system32\cmd.exe'
def check_window():
hd = win32gui.GetDesktopWindow()
# 获取所有子窗口
hwndChildList = []
# EnumChildWindows 为指定的父窗口枚举子窗口
win32gui.EnumChildWindows(hd, lambda hwnd, param: param.append(hwnd), hwndChildList)
for hwnd in hwndChildList:
title = win32gui.GetWindowText(hwnd)
find = False
# print("句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
if findtxt in title:
find = True
if findtxt2 in title:
find = True
if find == True:
return True
return False
def close_window():
hd = win32gui.GetDesktopWindow()
# 获取所有子窗口
hwndChildList = []
# EnumChildWindows 为指定的父窗口枚举子窗口
win32gui.EnumChildWindows(hd, lambda hwnd, param: param.append(hwnd), hwndChildList)
for hwnd in hwndChildList:
title = win32gui.GetWindowText(hwnd)
find = False
# print("句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
if findtxt in title:
find = True
if findtxt2 in title:
find = True
if 'Chromium' in title:
find = True
if find == True:
try:
print("special句柄:", hwnd, "标题:", win32gui.GetWindowText(hwnd))
win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
except:
print("can not erase window")
def print_time(task):
print(task, datetime.now())
def TrService():
print_time('dashboard.py is running at ')
p = subprocess.Popen(r'd:\\dashboard.bat',creationflags=subprocess.CREATE_NEW_CONSOLE)
def window_close():
print_time('window_close is running at ')
r = check_window()
if r == True:
close_window()
schedule.every().day.at('09:10').do(TrService)
schedule.every().day.at('10:10').do(TrService)
schedule.every().day.at('11:10').do(TrService)
schedule.every().day.at('12:10').do(TrService)
schedule.every().day.at('13:10').do(TrService)
schedule.every().day.at('14:10').do(TrService)
schedule.every().day.at('15:10').do(TrService)
schedule.every().day.at('16:10').do(TrService)
schedule.every().day.at('17:10').do(TrService)
schedule.every().day.at('09:40').do(TrService)
schedule.every().day.at('10:40').do(TrService)
schedule.every().day.at('11:40').do(TrService)
schedule.every().day.at('12:40').do(TrService)
schedule.every().day.at('13:40').do(TrService)
schedule.every().day.at('14:40').do(TrService)
schedule.every().day.at('15:40').do(TrService)
schedule.every().day.at('16:40').do(TrService)
schedule.every().day.at('17:40').do(TrService)
schedule.every().day.at('18:40').do(TrService)
print_time('SCHEDULE is running at ')
while True:
schedule.run_pending()
time.sleep(1)
这段 Python 代码的主要功能是通过定时任务调度(使用 schedule
库)来执行特定的批处理文件(.bat
文件),并对系统窗口进行管理和监控。以下是代码的主要功能模块:
窗口管理:
定时任务调度:
schedule
库设置定时任务,在指定时间运行特定的批处理文件。批处理文件执行:
subprocess.Popen
执行批处理文件,并在新控制台中运行。主循环:
这段 Python 代码的主要功能是从 Outlook 邮箱中提取邮件及其附件,并根据附件的内容将其存储到 MySQL 数据库中。以下是代码的主要功能模块:
日志记录:
logtext
记录程序运行日志。邮件配置:
邮件处理:
win32com.client
操作 Outlook 邮箱。附件处理:
wl
或 nd1100
)。数据库操作:
mysql.connector
连接到 MySQL 数据库。错误处理:
python
logtext = ''
def log(intext):
global logtext
logtext = logtext + '\n' + str(datetime.now())[:19] + ' : ' + intext
logtext
存储日志信息,每次调用 log
函数时追加新的日志内容。python
from_adr = '[email protected]'
fld_itm1 = 'dashboard'
flg_itm1 = 'Y'
# ... 其他配置
python
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
win32com.client
操作 Outlook 的 MAPI 接口。python
if flg_itm1 == 'Y':
try:
root_folder = outlook.Folders.Item(from_adr).Folders.Item(fld_itm1)
except Exception as e:
log('1.1填写资料夹名称有误:' + folder_name + str(e))
flag_err = 'Y'
python
if flg_tim == 'Y' and datetime.strptime(str(message.ReceivedTime)[:19], "%Y-%m-%d %H:%M:%S") <= dt1 : continue
if flg_sub == 'Y' and not filt_sub in message.Subject : continue
python
for x in range(1, num_attach + 1):
attachment = attachments.Item(x)
file_name = f"{str(message.ReceivedTime)[:10]}_{attachment.FileName}"
full_path = os.path.join(folder_a, file_name)
attachment.SaveASFile(full_path)
attachment_paths.append(full_path)
python
cursor = conn.cursor()
for excel_path in attachment_paths:
try:
df = pd.read_excel(excel_path, sheet_name='Sheet1')
pandas.read_excel
读取附件中的 Excel 文件。wl
或 nd1100
)。t_db_wl
的处理python
if 'wl' in excel_path.lower():
cursor.execute("TRUNCATE TABLE t_db_wl")
insert_sql = """INSERT INTO t_db_wl
(审单日, 业务类型, 审单人, 单量)
VALUES (%s, %s, %s, %s)"""
for _, row in df.iterrows():
cursor.execute(insert_sql, (
row['审单日'].date(),
row['业务类型'],
row['审单人'],
int(row['单量'])
))
wl
文件中的数据插入到 t_db_wl
表中。t_db_wl
。t_db_nd1100_open
的处理python
elif 'nd' in excel_path.lower():
cursor.execute("TRUNCATE TABLE t_db_nd1100_open")
insert_sql = """INSERT INTO t_db_nd1100_open
(OUT_DATE, CREATE_DATE, BILL_CODE, 表單類型,
流程类型, 业务类型代码, 业务类型, SHARE_TASK_STATUS)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
for _, row in df.iterrows():
cursor.execute(insert_sql, (
row['OUT_DATE'],
row['CREATE_DATE'],
row['BILL_CODE'],
row['表單類型'],
row['流程类型'],
row['业务类型代码'],
row['业务类型'],
row['SHARE_TASK_STATUS']
))
nd1100
文件中的数据插入到 t_db_nd1100_open
表中。t_db_nd1100_open
。python
except Exception as e:
log(f'处理附件失败 {excel_path}: {str(e)}')
python
conn.commit()
cursor.close()
完整代码展示
import os
from datetime import datetime, timedelta
import logging
import numpy as np
import pandas as pd
import win32com.client
import mysql.connector
from mysql.connector import Error
# 配置日志记录
logging.basicConfig(
filename='mail_processing.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# 全局变量
attachment_paths = [] # 用于存储附件路径
file_log = r'E:\Project\FSSC\dashboard\mail_log.xlsx' # 邮件日志文件
folder_a = r'E:\Project\FSSC\dashboard' # 附件存储文件夹
# 数据库连接配置
DB_CONFIG = {
'host': 'localhost',
'user': 'root',
'password': '123456', # 请修改为实际密码
'database': 'ofssc' # 请修改为实际数据库名
}
def log_info(message):
"""记录信息日志"""
logging.info(message)
def log_error(message):
"""记录错误日志"""
logging.error(message)
def connect_to_db():
"""连接到 MySQL 数据库"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
log_info("成功连接到数据库")
return conn
except Error as e:
log_error(f"数据库连接失败: {e}")
raise
def process_emails(outlook, from_adr, fld_itm1, flg_itm1, fld_itm2, flg_itm2, flag_err):
"""处理邮件"""
root_folder = None
if flg_itm1 == 'Y':
try:
root_folder = outlook.Folders.Item(from_adr).Folders.Item(fld_itm1)
except Exception as e:
log_error(f"1.1 邮箱文件夹名称有误: {e}")
flag_err = 'Y'
if flg_itm2 == 'Y' and not flag_err:
try:
root_folder = root_folder.Folders.Item(fld_itm2)
except Exception as e:
log_error(f"1.2 邮箱子文件夹名称有误: {e}")
flag_err = 'Y'
if flag_err:
return None
messages = root_folder.Items
messages.Sort("[ReceivedTime]", True)
return messages
def save_attachments(message, folder_a, attachment_paths):
"""保存邮件附件"""
num_attach = len(message.Attachments)
attach_file = ''
for x in range(1, num_attach + 1):
attachment = message.Attachments.Item(x)
file_name = f"{message.ReceivedTime.date()}_{attachment.FileName}"
full_path = os.path.join(folder_a, file_name)
attachment.SaveASFile(full_path)
attach_file += f"|{full_path}"
attachment_paths.append(full_path)
log_info(f"附件已保存: {full_path}")
if num_attach > 0:
log_info(f"下载附件: {attach_file}")
def process_attachments(attachment_paths):
"""处理附件并将数据导入数据库"""
conn = connect_to_db()
cursor = conn.cursor()
try:
for excel_path in attachment_paths:
try:
df = pd.read_excel(excel_path, sheet_name='Sheet1')
log_info(f"处理附件: {excel_path}")
if 'wl' in excel_path.lower():
process_wl_file(df, excel_path, cursor)
elif 'nd' in excel_path.lower():
process_nd_file(df, excel_path, cursor)
else:
log_error(f"文件 {excel_path} 不符合任何处理条件")
except Exception as e:
log_error(f"处理附件失败 {excel_path}: {e}")
conn.commit()
except Exception as e:
conn.rollback()
log_error(f"数据库操作失败: {e}")
finally:
cursor.close()
conn.close()
def process_wl_file(df, excel_path, cursor):
"""处理 wl 文件"""
try:
# 清空表
cursor.execute("TRUNCATE TABLE t_db_wl")
# 提取时间戳并插入更新记录
base_name = os.path.splitext(os.path.basename(excel_path))[0]
wl_index = base_name.lower().index('wl')
time_str = base_name[wl_index + 2:wl_index + 15]
cursor.execute("INSERT INTO t_db_updatetime (lastupdate) VALUES (%s)", (time_str,))
# 插入数据
insert_sql = """INSERT INTO t_db_wl
(审单日, 业务类型, 审单人, 单量)
VALUES (%s, %s, %s, %s)"""
for _, row in df.iterrows():
cursor.execute(insert_sql, (
row['审单日'].date() if isinstance(row['审单日'], datetime) else row['审单日'],
row['业务类型'],
row['审单人'],
int(row['单量'])
))
except Exception as e:
log_error(f"处理 wl 文件失败 {excel_path}: {e}")
def process_nd_file(df, excel_path, cursor):
"""处理 nd 文件"""
try:
# 清空表
cursor.execute("TRUNCATE TABLE t_db_nd1100_open")
# 插入数据
insert_sql = """INSERT INTO t_db_nd1100_open
(OUT_DATE, CREATE_DATE, BILL_CODE, 表單類型,
流程类型, 业务类型代码, 业务类型, SHARE_TASK_STATUS)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
for _, row in df.iterrows():
cursor.execute(insert_sql, (
row['OUT_DATE'].strftime("%Y-%m-%d %H:%M:%S") if isinstance(row['OUT_DATE'], pd.Timestamp) else row['OUT_DATE'],
row['CREATE_DATE'].strftime("%Y-%m-%d %H:%M:%S") if isinstance(row['CREATE_DATE'], pd.Timestamp) else row['CREATE_DATE'],
row['BILL_CODE'],
row['表單類型'],
row['流程类型'],
row['业务类型代码'],
row['业务类型'],
row['SHARE_TASK_STATUS']
))
except Exception as e:
log_error(f"处理 nd 文件失败 {excel_path}: {e}")
def main():
"""主程序"""
try:
# 连接 Outlook
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
# 邮件配置
from_adr = 'your email account'
fld_itm1 = '收件箱名称'
flg_itm1 = 'Y'
fld_itm2 = ''
flg_itm2 = ''
flag_err = ''
# 处理邮件
messages = process_emails(outlook, from_adr, fld_itm1, flg_itm1, fld_itm2, flg_itm2, flag_err)
if not messages:
log_error("邮件处理失败,检查配置和文件夹名称")
return
# 遍历邮件并保存附件
for message in messages:
save_attachments(message, folder_a, attachment_paths)
# 处理附件
process_attachments(attachment_paths)
log_info("执行完成")
except Exception as e:
log_error(f"程序运行失败: {e}")
if __name__ == "__main__":
main()
pandas
库将接收到的数据存储到 MySQL 数据库。table layout
CREATE TABLE
t_db_wl
(
审单日 DATE,
业务类型 VARCHAR(20) COLLATE utf8mb4_general_ci NOT NULL,
审单人 VARCHAR(20),
单量 DECIMAL(42,0),
id INT(-1) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
)
CREATE TABLE
t_db_nd1100_open
(
OUT_DATE DATETIME,
CREATE_DATE DATETIME,
BILL_CODE VARCHAR(50) COLLATE utf8mb4_general_ci,
表單類型 VARCHAR(50) COLLATE utf8mb4_general_ci,
流程类型 VARCHAR(50) COLLATE utf8mb4_general_ci,
业务类型代码 VARCHAR(50) COLLATE utf8mb4_general_ci,
业务类型 VARCHAR(50) COLLATE utf8mb4_general_ci,
SHARE_TASK_STATUS VARCHAR(50) COLLATE utf8mb4_general_ci,
ID INT(-1) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (ID)
)
python
CREATE TABLE
t_db_updatetime
(
lastupdate VARCHAR(50)
)
v_db_vl_by_30day
示例 SQL:v_db_vl_by_30day
的视图(View)。视图是基于一个或多个表的预定义查询,可以像表一样被查询和使用。以下是对该语句的详细说明:
sql
CREATE VIEW v_db_vl_by_30day
v_
通常表示这是一个“视图”(view),后面的 db_vl_by_30day
可能代表“单量按30天统计”(具体含义需结合业务背景)。sql
(
审单日,
单据量,
折算单量,
标准工时人力
)
sql
SELECT
`a`.`审单日` AS `审单日`,
SUM(`a`.`单据量`) AS `单据量`,
ROUND(SUM(`a`.`折算单量`),1) AS `折算单量`,
ROUND((SUM(`a`.`工时量`) / 420),1) AS `标准工时人力`
a
.审单日
AS 审单日
**:选择 v_db_wl_time
视图中的 审单日
字段,并保持相同的列名。a
.单据量
) AS 单据量
:计算过去30天内每天的单据总量。a
.折算单量
),1) AS 折算单量
:计算过去30天内每天折算后的单据总量,并保留一位小数。a
.工时量
) / 420),1) AS 标准工时人力
:计算过去30天内每天的总工时量除以420(假设420为标准工时,如每小时的工时量),得到所需的标准工时人力,并保留一位小数。sql
FROM `v_db_wl_time` `a`
v_db_wl_time
**:这是数据来源的视图或表,名称前加了别名 a
,便于在查询中引用。v_db_wl_time
) 创建的,这在实际应用中是可行的,但需要确保底层视图的数据准确性和性能。sql
WHERE
(
`a`.`审单日` >= (CURDATE() - INTERVAL 30 DAY)
)
CURDATE()
**:返回当前日期(不包含时间部分)。INTERVAL 30 DAY
**:表示时间间隔为30天。审单日
在当前日期前30天及以后的记录,即过去30天内的数据。sql
GROUP BY `a`.`审单日`;
GROUP BY
**:按照 审单日
进行分组,以便对每个日期分别计算 单据量
、折算单量
和 标准工时人力
。sql
CREATE VIEW v_db_vl_by_30day (
审单日,
单据量,
折算单量,
标准工时人力
) AS
SELECT
`a`.`审单日` AS `审单日`,
SUM(`a`.`单据量`) AS `单据量`,
ROUND(SUM(`a`.`折算单量`),1) AS `折算单量`,
ROUND((SUM(`a`.`工时量`) / 420),1) AS `标准工时人力`
FROM
`v_db_wl_time` `a`
WHERE
`a`.`审单日` >= CURDATE() - INTERVAL 30 DAY
GROUP BY
`a`.`审单日`;
CREATE VIEW
v_db_vl_yesterday
(
审单日
) AS
SELECT
MAX(`v_db_wl_time`.`审单日`) AS `审单日`
FROM
`v_db_wl_time`
WHERE
((
`v_db_wl_time`.`审单日` < curdate())
AND (
`v_db_wl_time`.`单据量` > 0));
该视图 v_db_vl_yesterday
的主要功能是获取 v_db_wl_time
中最新的一个日期,该日期满足以下两个条件:
单据量
大于零。通过 MAX(v_db_wl_time.审单日)
,视图返回满足上述条件的最大日期,通常可以理解为“昨天”或最近的一个有活动的日期。
CREATE VIEW
v_db_wl_time_last_2days
(
审单人,
审单日,
单据量,
业务类型名称,
折算单量
) AS
SELECT
`a`.`审单人` AS `审单人`,
`a`.`审单日` AS `审单日`,
`a`.`单据量` AS `单据量`,
`y`.`业务类型名称` AS `业务类型名称`,
`a`.`折算单量` AS `折算单量`
FROM
((`v_db_wl_time` `a`
JOIN
`t_db_ywlx` `y`
ON
((
`y`.`业务类型` = `a`.`业务类型`)))
JOIN
`v_db_vl_yesterday` `d`
ON
((
`a`.`审单日` >= `d`.`审单日`)));
这段 SQL 语句试图创建一个视图 v_db_wl_time_last_2days
,通过连接多个视图和表来获取审核人员、审核日期、单据量、业务类型名称及折算单量等信息。
该视图 v_db_td_wl_time
的主要功能是:
t_db_nd1100_open
和 t_dash_ppp_checker_std_v
表中提取任务的基本信息和标准时间。PPP_MONTH
为 '202502'
、TIME_BREAK
不为 'Y'
且 TASK_DEF_ID
为 'ND1100'
的任务。CREATE VIEW
v_db_td_wl_time
(
单据,
表單類型,
流程类型,
业务类型,
任务日期,
到期时间,
到期日,
std_time,
到期天数
) AS
SELECT
`a`.`BILL_CODE` AS `单据`,
`a`.`表單類型` AS `表單類型`,
`a`.`流程类型` AS `流程类型`,
`a`.`业务类型` AS `业务类型`,
`a`.`CREATE_DATE` AS `任务日期`,
`a`.`OUT_DATE` AS `到期时间`,
str_to_date(`a`.`OUT_DATE`,'%Y-%m-%d') AS `到期日`,
`ex`.`STD_TIME` AS `std_time`,
(
CASE
WHEN (CAST(`a`.`OUT_DATE` AS DATE) = curdate())
THEN ROUND((timestampdiff(HOUR,now(),`a`.`OUT_DATE`) / 9),1)
ELSE ((((to_days(`a`.`OUT_DATE`) - to_days(now())) - 1) + ROUND((timestampdiff(MINUTE,
now(),concat(curdate(),' 18:00:00')) / 540),1)) + (
CASE
WHEN (CAST(`a`.`OUT_DATE` AS TIME) BETWEEN '09:00:00' AND '18:00:00')
THEN ROUND((timestampdiff(MINUTE,concat(CAST(`a`.`OUT_DATE` AS DATE),
' 09:00:00'),`a`.`OUT_DATE`) / 540),1)
WHEN (CAST(`a`.`OUT_DATE` AS TIME) < '09:00:00')
THEN 0
ELSE 1
END))
END) AS `到期天数`
FROM
(`t_db_nd1100_open` `a`
JOIN
`t_dash_ppp_checker_std_v` `ex`
ON
((
`a`.`业务类型代码` = `ex`.`BUSINESS_TYPE`)))
WHERE
((
`ex`.`PPP_MONTH` = '202502')
AND (
`ex`.`TIME_BREAK` <> 'Y')
AND (
`ex`.`TASK_DEF_ID` = 'ND1100'));
当天到期:
sql
WHEN (CAST(`a`.`OUT_DATE` AS DATE) = CURDATE())
THEN ROUND((TIMESTAMPDIFF(HOUR, NOW(), `a`.`OUT_DATE`) / 9), 1)
OUT_DATE
是今天,计算当前时间到到期时间的小时差,除以9(可能是标准工时小时数),并保留一位小数,得到剩余的工作天数。非当天到期:
sql
ELSE (
((((to_days(`a`.`OUT_DATE`) - to_days(now())) - 1) + ROUND((timestampdiff(MINUTE, now(), concat(curdate(),' 18:00:00')) / 540),1)) + (
CASE
WHEN (CAST(`a`.`OUT_DATE` AS TIME) BETWEEN '09:00:00' AND '18:00:00')
THEN ROUND((timestampdiff(MINUTE, concat(CAST(`a`.`OUT_DATE` AS DATE), ' 09:00:00'), `a`.`OUT_DATE`) / 540),1)
WHEN (CAST(`a`.`OUT_DATE` AS TIME) < '09:00:00')
THEN 0
ELSE 1
END))
)
to_days
计算日期差,并结合分钟差来计算剩余的工作时间。540
分钟可能代表标准工作小时(9小时 * 60分钟)。4.1 配置 ODBC 数据源
SUMIF
、COUNTIF
)展示绩效数据。示例 VBA 代码:
vba
Sub RefreshQuery()
' 声明工作簿对象变量
Dim wb As Workbook
' 检查名为 "dashboard.xlsm" 的工作簿是否已打开
On Error Resume Next ' 忽略错误,继续执行下一行
Set wb = Workbooks("dashboard.xlsm") ' 尝试设置 wb 为 "dashboard.xlsm"
On Error GoTo 0 ' 恢复正常的错误处理
' 如果工作簿未打开,显示消息框提示用户
If wb Is Nothing Then
MsgBox "工作簿 'dashboard.xlsm' 未打开!", vbExclamation
Else
' 如果工作簿已打开,设置 ws 为该工作簿的活动工作表
Set ws = wb.ActiveSheet
End If
' 设置下一次刷新的时间为当前时间加上 30 分钟
NewTime = Now + TimeValue("00:30:00")
' 获取当前日期的整点时间
today = Round(Now, 0)
' 设置 n1 为当前时间加 1 天,并设置为早上 8:15 的时间
n1 = Round(Now + 1, 0) + TimeValue("08:15:00")
' 如果当前时间小于等于 18:00,则设置刷新时间为 NewTime
If Time <= "18:00:00" Then
timeset = NewTime
Else
' 如果当前时间大于 18:00,则设置刷新时间为第二天的早上 8:15
timeset = n1
End If
' 刷新工作簿中的所有查询
ActiveWorkbook.RefreshAll
' 遍历当前工作簿中的所有查询,并刷新指定的查询
Dim qry As WorkbookQuery
For Each qry In ThisWorkbook.Queries
' 检查查询名称是否在指定的列表中
If qry.Name = "查询3" Or qry.Name = "v_db_wl_time_last_2days" Or qry.Name = "v_db_productivity" Or _
qry.Name = "raw_user" Or qry.Name = "raw_sla" Or qry.Name = "更新时间" Then
qry.Refresh ' 刷新指定的查询
End If
Next qry
' 禁用所有警告消息,防止弹出提示框干扰自动化流程
Application.DisplayAlerts = False
' 暂停执行 10 秒,等待刷新操作完成
Application.Wait Now + TimeValue("00:00:10")
' 刷新多个数据透视表
With Worksheets("pivot单量与人力推移")
.PivotTables("数据透视表13").PivotCache.Refresh
End With
With Worksheets("pivot SLA")
.PivotTables("数据透视表20").PivotCache.Refresh
End With
With Worksheets("pivot人力需求")
.PivotTables("数据透视表16").PivotCache.Refresh
End With
With Worksheets("v_db_wl_time_last_2days")
.PivotTables("数据透视表39").PivotCache.Refresh
End With
' 返回 "dashborad" 工作表并刷新其数据透视表
With Worksheets("dashborad")
.PivotTables("数据透视表26").PivotCache.Refresh
' 选择特定区域并设置背景颜色为特定值(6299648 为某种颜色的代码)
With .Range("AN24:AZ50").Interior
.Pattern = xlSolid
.Color = 6299648
End With
.Range("A1").Select ' 选择 A1 单元格
End With
' 注释掉重复的刷新和格式化代码(建议删除或移除以优化性能)
' (以下部分代码被重复,通常不需要多次执行相同的操作)
' 应用程序将在指定的 timeset 时间再次调用 RefreshQuery 过程
Application.OnTime timeset, "RefreshQuery"
End Sub
工作簿检查:
"dashboard.xlsm"
的工作簿是否已经打开。如果未打开,则显示警告消息并停止后续操作。设置刷新时间:
刷新查询:
ActiveWorkbook.RefreshAll
刷新工作簿中的所有查询。禁用警告和等待:
Application.DisplayAlerts = False
禁用所有警告消息,防止在刷新过程中弹出提示框中断自动化流程。Application.Wait
暂停代码执行 10 秒,给刷新操作足够的时间完成。刷新数据透视表:
"dashborad"
工作表中,选择特定区域并设置其背景颜色,可能是为了高亮显示某些数据或标记刷新完成。安排下次刷新:
Application.OnTime
方法,在之前设定的 timeset
时间点再次调用 RefreshQuery
过程,实现定时自动刷新功能。Alt + F11
打开 VBA 编辑器,插入模块并运行 RefreshQuery
。 就会 每隔30分钟刷新数据了