From 47fc52eda4c46301b0ce833b13f3807c956bcd61 Mon Sep 17 00:00:00 2001 From: Julin Date: Thu, 20 Apr 2023 10:40:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E4=BC=81=E6=95=88=E8=83=BD=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=88=86=E6=9E=90=E5=B7=A5=E5=85=B7=EF=BC=9A=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=80=8A=E9=A1=B9=E4=BC=81=E6=B5=81=E7=A8=8B=E6=95=88?= =?UTF-8?q?=E8=83=BD=E5=88=86=E6=9E=90=E7=BB=93=E6=9E=9C=E5=85=AC=E7=A4=BA?= =?UTF-8?q?=EF=BC=88=E5=85=A8=E5=91=98=EF=BC=89=E3=80=8B=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/pep-stats-report/README.md | 21 ++ code/pep-stats-report/app/db_helper.py | 184 +++++++++++++ code/pep-stats-report/config.ini | 12 + code/pep-stats-report/main.py | 346 +++++++++++++++++++++++++ code/pep-stats-report/requirements.txt | 36 +++ 5 files changed, 599 insertions(+) create mode 100644 code/pep-stats-report/README.md create mode 100644 code/pep-stats-report/app/db_helper.py create mode 100644 code/pep-stats-report/config.ini create mode 100644 code/pep-stats-report/main.py create mode 100644 code/pep-stats-report/requirements.txt diff --git a/code/pep-stats-report/README.md b/code/pep-stats-report/README.md new file mode 100644 index 0000000..a15b353 --- /dev/null +++ b/code/pep-stats-report/README.md @@ -0,0 +1,21 @@ +## 配置文件 +`config.ini`为项目配置文件。 +- `[clickhouse]`配置节为 ClickHouse 数据库的配置信息: + - `port`为数据库的`TCP`端口。 + - `password`为数据库密码,如果没有密码,则不需要配置任何值。 +- `[qiniu]`配置节为七牛云的配置信息: + - `access-key`和`secret-key`为七牛云密钥。 + - `bucket`为七牛云存储空间。 + - `domain`为存储空间对应的外链域名。 + +## requirements.txt +项目所需的依赖库。 +``` +pip freeze > requirements.txt +``` + +## 打包`.exe`文件 +使用`--distpath`选项指定输出目录的路径。例如,将`.exe`文件输出至当前文件夹下: +``` +(venv) E:\WorkSpace\FS-Git\fs-pep-stats-report>pyinstaller -F main.py --distpath=. +``` diff --git a/code/pep-stats-report/app/db_helper.py b/code/pep-stats-report/app/db_helper.py new file mode 100644 index 0000000..64d2bc0 --- /dev/null +++ b/code/pep-stats-report/app/db_helper.py @@ -0,0 +1,184 @@ +def querystring_user_procinst_duration(start, end, except_senior, department): + filter_user = "and u.name not in ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥')" if except_senior else "" + filter_department = "and department_name='{}'".format(department) if department else "" + return ''' +select department_name, + user_name, + procinst_id, + sum(duration_in_minutes) as procinst_duration_in_minutes +from ( + select distinct d.id as department_id, + d.name as department_name, + u.id as user_id, + u.name as user_name, + wp.name as process_name, + wpa.procinst_id as procinst_id, + wpa.task_id as taskinst_id, + wpa.task_name as task_name, + wpa.start_time as start_time, + wpa.end_time as end_time, + wpa.total_minutes as duration_in_minutes + from workflow_process_achievements as wpa + inner join workflow_process_history as wph on wpa.procinst_id=wph.procinst_id + inner join workflow_process_version as wpv on wph.version_id=wpv.id + inner join workflow_process as wp on wpv.process_id=wp.id + inner join user as u on wpa.deal_user_id=u.id + inner join department_user as du on u.id=du."user" + inner join department as d on du.department=d.id + where wpa.end_time >='{}' and wpa.end_time < '{}' and wp.deleted=false and wp.is_enable=true and d.delete=0 and u.delete=0 and u.state=1 and u.active_status=1 + {} {} +) as r +group by department_name, user_name, procinst_id + '''.format(start, end, filter_user, filter_department) + + +def querystring_senior_procinst_duration(start, end): + return ''' +SELECT user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user +from ( +select user_name, + procinst_id, + sum(duration_in_minutes) as procinst_duration_in_minutes +from ( + select distinct u.id as user_id, + u.name as user_name, + wp.name as process_name, + wpa.procinst_id as procinst_id, + wpa.task_id as taskinst_id, + wpa.task_name as task_name, + wpa.start_time as start_time, + wpa.end_time as end_time, + wpa.total_minutes as duration_in_minutes + from workflow_process_achievements as wpa + inner join workflow_process_history as wph on wpa.procinst_id=wph.procinst_id + inner join workflow_process_version as wpv on wph.version_id=wpv.id + inner join workflow_process as wp on wpv.process_id=wp.id + inner join user as u on wpa.deal_user_id=u.id + where wpa.end_time >='{}' and wpa.end_time < '{}' and wp.deleted=false and wp.is_enable=true and u.delete=0 and u.state=1 and u.active_status=1 + and u.name in ('刘文峰', '金亮', '姜珍', '余莎莎', '张阳根', '唐国华', '刘国勇', '刘会连', '肖琥') +) as r +group by user_name, procinst_id +) as r2 +GROUP by r2.user_name + '''.format(start, end) + + +# 按月统计项企平台使用人数 +def querystring_user_count(start, end): + return ''' +select count(distinct deal_user_id) as pep_using_user_count +from workflow_process_achievements as wpa +inner join user as u on wpa.deal_user_id=u.id +where wpa.end_time >='{}' and wpa.end_time < '{}' and u.delete=0 and u.state=1 and u.active_status=1 + '''.format(start, end) + + +# 个人处理流程平均耗时 +def querystring_procinst_duration_by_company(start, end): + return ''' +SELECT ROUND(AVG(procinst_duration_in_minutes_by_user) /60, 2) as procinst_duration_in_hours from ( + SELECT user_name, + ROUND(AVG(procinst_duration_in_minutes), 2) as procinst_duration_in_minutes_by_user + from ({}) as r2 + GROUP by r2.user_name +) as r3 + '''.format(querystring_user_procinst_duration(start, end, False, '')) + + +# 部门数(高于公司平均数) +def querystring_department_count_gt_avg(start, end, avg): + return ''' +SELECT count(*) from ( + SELECT department_name, + ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department + from ( + SELECT department_name, + user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user + from ({}) as r2 + GROUP by r2.department_name, r2.user_name + ) as r3 + GROUP by r3.department_name +) as r4 +WHERE procinst_duration_in_hours_by_department > {} + '''.format(querystring_user_procinst_duration(start, end, True, ''), avg) + + +# 用户数(高于公司平均数) +def querystring_user_count_gt_avg(start, end, avg): + return ''' +SELECT count(*) from ( + SELECT user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user + from ({}) as r2 + group by r2.user_name +) as r3 +WHERE r3.procinst_duration_in_hours_by_user > {} + '''.format(querystring_user_procinst_duration(start, end, False, ''), avg) + + +# 部门处理流程平均耗时 +def querystring_procinst_duration_by_department(start, end): + return """ +SELECT department_name, + ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department +from ( + SELECT department_name, + user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user + from ({}) as r2 + GROUP by r2.department_name, r2.user_name +) as r3 +GROUP by r3.department_name +order by procinst_duration_in_hours_by_department DESC + """.format(querystring_user_procinst_duration(start, end, True, '')) + + +# 部门处理流程平均耗时(高管) +def querystring_procinst_duration_by_senior(start, end): + return ''' +SELECT ROUND(AVG(procinst_duration_in_hours_by_user), 2) as procinst_duration_in_hours_by_department +from ({}) as r3 + '''.format(querystring_senior_procinst_duration(start, end)) + + +# 用户单流程处理耗时(高管) +def querystring_procinst_duration_by_user_senior(start, end): + return ''' +{} +order by procinst_duration_in_hours_by_user DESC + '''.format(querystring_senior_procinst_duration(start, end)) + + +# 各部门耗时较长用户(高管) +def querystring_user_gt_senior_avg(start, end, avg): + return ''' +SELECT * from ({}) as r3 +WHERE r3.procinst_duration_in_hours_by_user > {} +order by procinst_duration_in_hours_by_user DESC + '''.format(querystring_senior_procinst_duration(start, end), avg) + + +# 各部门耗时较长用户 +def querystring_user_gt_department_avg(start, end, department, avg): + return """ +SELECT * from ( + SELECT user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user + from ({}) as r2 + group by r2.user_name +) as r3 +WHERE r3.procinst_duration_in_hours_by_user > {} +order by procinst_duration_in_hours_by_user desc + """.format(querystring_user_procinst_duration(start, end, True, department), avg) + + +# 按部门统计用户单流程处理耗时 +def querystring_procinst_duration_by_user(start, end, department): + return """ +SELECT user_name, + ROUND(AVG(procinst_duration_in_minutes) /60, 2) as procinst_duration_in_hours_by_user +from ({}) as r2 +group by r2.user_name + """.format(querystring_user_procinst_duration(start, end, True, department)) diff --git a/code/pep-stats-report/config.ini b/code/pep-stats-report/config.ini new file mode 100644 index 0000000..a742da8 --- /dev/null +++ b/code/pep-stats-report/config.ini @@ -0,0 +1,12 @@ +[clickhouse] +host = 10.8.30.161 +port = 30900 +username = default +password = +database = pepca8 + +[qiniu] +access-key=5XrM4wEB9YU6RQwT64sPzzE6cYFKZgssdP5Kj3uu +secret-key=w6j2ixR_i-aelc6I7S3HotKIX-ukMzcKmDfH6-M5 +bucket=pep-resource +domain=https://pepsource.anxinyun.cn diff --git a/code/pep-stats-report/main.py b/code/pep-stats-report/main.py new file mode 100644 index 0000000..e4a7d9b --- /dev/null +++ b/code/pep-stats-report/main.py @@ -0,0 +1,346 @@ +import os +import datetime +import configparser +from clickhouse_driver import Client +from docx import Document +from docx.shared import Inches, Pt, RGBColor +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_LINE_SPACING +from docx.oxml.ns import qn +import matplotlib.pyplot as plt +from app import db_helper +import qiniu +import logging + +REPORT_DIR = 'output' +LOG_DIR = 'log' + + +def get_report_year_month(): + # 获取上月的最后一天 + last_day = datetime.datetime.now().replace(day=1) - datetime.timedelta(days=1) + return last_day.year, last_day.month + + +def get_report_start_end(): + # 获取当前日期 + now = datetime.datetime.now() + # 获取本月第一天 + this_month_start = datetime.datetime(now.year, now.month, 1) + # 获取上个月最后一天 + last_month_end = this_month_start - datetime.timedelta(days=1) + # 获取上个月第一天 + last_month_start = datetime.datetime(last_month_end.year, last_month_end.month, 1) + return last_month_start, this_month_start + + +def set_paragraph_space(p): + # 设置间距 + p.paragraph_format.space_before = 0 # 段前 + p.paragraph_format.space_after = 0 # 段后 + p.paragraph_format.line_spacing = Pt(20) # 设置段落行距为20磅 + + +def set_paragraph_format(p): + set_paragraph_space(p) + # 设置首行缩进2个字符 + p.paragraph_format.first_line_indent = Inches(0.3) + + +def set_heading_format(h, r): + set_paragraph_space(h) + r.font.name = 'Times New Roman' + r._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') + r.font.size = Pt(10.5) # 五号 + r.font.color.rgb = RGBColor(0, 0, 0) + r.bold = True + + +def set_title_format(h, r): + h.paragraph_format.space_before = Pt(12) # 段前1行 + h.paragraph_format.space_after = Pt(12) # 段后1行 + h.paragraph_format.line_spacing = Pt(20) # 设置段落行距为20磅 + h.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r.font.name = u'Times New Roman' + r._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') + r.font.size = Pt(10.5) # 五号 + r.font.color.rgb = RGBColor(0, 0, 0) + r.bold = True + + +def set_global_normal_style(doc): + style = doc.styles['Normal'] + style.font.name = 'Times New Roman' # 英文、数字字体 + style._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') # 中文字体 + style.font.size = Pt(10.5) # 五号 + style.paragraph_format.space_before = 0 # 段前 + style.paragraph_format.space_after = 0 # 段后 + style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.SINGLE # 单倍行距 + + +def create_bar_chart(doc, x, y, mean, title, filename): + plt.rcParams["font.sans-serif"] = ["SimHei"] # 设置字体 + plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 + + if title == '部门流程处理平均耗时': + plt.figure(figsize=(12, 6), layout='tight') + plt.margins(x=0.01) + else: + plt.figure(figsize=(6.4, 3.4), layout='tight') + + # 绘制柱状图 + plt.bar(x, y, color=(31 / 255, 168 / 255, 201 / 255)) + # 绘制均值线 + plt.axhline(mean, linestyle='dashed', color='#FF8C00') + # 添加标题 + plt.title(title) + # 设置x轴标签旋转角度 + plt.xticks(rotation=-30, ha='left') + # 添加图例 + plt.legend(['均值:{}'.format(mean)], loc='upper right') + # 保存图形 + plt.savefig('{}/{}'.format(REPORT_DIR, filename)) + plt.close() + # 插入图形到 Word 文档中 + p = doc.add_paragraph() + p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + r = p.add_run() + r.add_picture('{}/{}'.format(REPORT_DIR, filename), width=Inches(6)) + + +def add_section(doc, rank, department, d_elapse): + h = doc.add_heading(level=2) + r = h.add_run('{}. {}'.format(rank, department)) + set_heading_format(h, r) + + try: + qs = db_helper.querystring_user_gt_senior_avg( + start_time, end_time, d_elapse + ) if department == '高管' else db_helper.querystring_user_gt_department_avg( + start_time, end_time, department, d_elapse) + user_gt_department_avg = client.execute(qs) + + qs = db_helper.querystring_procinst_duration_by_user_senior( + start_time, end_time + ) if department == '高管' else db_helper.querystring_procinst_duration_by_user( + start_time, end_time, department) + procinst_duration_by_user = client.execute(qs) + except Exception as error: + print('数据库查询错误:', error) + logging.error('数据库查询错误:{}'.format(error)) + raise RuntimeError('数据库查询错误:', error) + + users = [] + for user, _ in user_gt_department_avg: + users.append(user) + + p = doc.add_paragraph() + p.add_run('{}{}月份个人处理流程平均耗时时长在公司排名为'.format(department, last_month)) + p.add_run('{}'.format(rank)).underline = True + if len(procinst_duration_by_user) == 1: + p.add_run(',{}公司平均水平。'.format( + '高于' if d_elapse > procinst_duration_by_company else + '低于' if d_elapse < procinst_duration_by_company else '等于')) + else: + p.add_run(',其中耗时较长的有') + p.add_run('{}'.format('、'.join(users))).underline = True + p.add_run(',{}({}小时/流程)。' + .format('高于公司平均水平和部门平均水平' if d_elapse >= procinst_duration_by_company else '低于公司平均水平但高于部门平均水平', + d_elapse)) + set_paragraph_format(p) + + x = [] + y = [] + for user, elapse in procinst_duration_by_user: + x.append(user) + y.append(elapse) + create_bar_chart(doc, x, y, d_elapse, "个人流程处理平均耗时", "{}-流程处理平均耗时.png".format(department)) + caption = doc.add_paragraph('图 2-{} {}个人处理流程平均耗时'.format(rank, department)) + caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + + +def add_chapter_2(doc): + h = doc.add_heading(level=1) + r = h.add_run('二、各部门成员详情') + set_heading_format(h, r) + + departments_excluded = ['汇派-质量部', '汇派-生产部', '汇派-计划部', '汇派-人事部', '汇派-采购部', '党建工会', '工程项目中心', '北京技术中心'] + + for index, (department, elapse) in enumerate(procinst_duration_by_department): + if department not in departments_excluded: + add_section(doc, index + 1, department, elapse) + + +def add_chapter_1(doc): + h = doc.add_heading(level=1) + r = h.add_run('一、公司各部门横向比较情况') + set_heading_format(h, r) + + try: + qs = db_helper.querystring_user_count(start_time, end_time) + user_count = client.execute(qs)[0][0] + + qs = db_helper.querystring_department_count_gt_avg(start_time, end_time, procinst_duration_by_company) + department_count_gt_avg = client.execute(qs)[0][0] + + qs = db_helper.querystring_user_count_gt_avg(start_time, end_time, procinst_duration_by_company) + user_count_gt_avg = client.execute(qs)[0][0] + except Exception as error: + print('数据库查询错误:', error) + logging.error('数据库查询错误:{}'.format(error)) + raise RuntimeError('数据库查询错误:', error) + + if procinst_duration_by_senior > procinst_duration_by_company: + department_count_gt_avg += 1 + + p = doc.add_paragraph() + p.add_run('{}年{}月,公司共计'.format(last_year, last_month)) + p.add_run('{}'.format(user_count)).underline = True + p.add_run('人使用项企平台,个人处理流程平均耗时') + p.add_run('{}'.format(procinst_duration_by_company)).underline = True + p.add_run('小时/流程,其中,平均单一流程处理耗时高于公司平均数的有') + p.add_run('{}'.format(department_count_gt_avg)).underline = True + p.add_run('个部门,') + p.add_run('{}'.format(user_count_gt_avg)).underline = True + p.add_run('人。') + set_paragraph_format(p) + + # 准备数据 + x = [] + y = [] + for department, elapse in procinst_duration_by_department: + x.append(department) + y.append(elapse) + + # 创建柱状图并保存为文件 + create_bar_chart(doc, x, y, procinst_duration_by_company, "部门流程处理平均耗时", "部门流程处理平均耗时.png") + caption = doc.add_paragraph('图 1 部门流程处理平均耗时') + caption.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER + + +def upload_to_qiniu(filename): + logging.info('上传报表至七牛云...') + + # 获取 qiniu 配置参数 + qn_cfg = config['qiniu'] + access_key = qn_cfg['access-key'] + secret_key = qn_cfg['secret-key'] + bucket_name = qn_cfg['bucket'] + + q = qiniu.Auth(access_key, secret_key) + bucket = qiniu.BucketManager(q) + + key = 'pep-stats-report/{}'.format(filename) + localfile = '{}/{}'.format(REPORT_DIR, filename) + + ret, info = bucket.stat(bucket_name, key) + if info.status_code == 200: + print('文件已存在,删除文件...') + ret, info = bucket.delete(bucket_name, key) + if info.status_code == 200: + print('删除成功') + else: + print('删除失败') + + print('上传文件...') + token = q.upload_token(bucket_name, key) + ret, info = qiniu.put_file(token, key, localfile) + if info.status_code == 200: + print('上传成功') + logging.info('上传成功') + # 获取文件访问 URL + url = '{}/{}?attname='.format(qn_cfg['domain'], ret['key']) + print('文件访问 URL:', url) + logging.info('文件访问 URL:{}'.format(url)) + else: + print('上传失败,错误信息:', info.error) + logging.error('上传失败,错误信息:{}'.format(error)) + + +def generate_word_report(): + # 创建一个新的Word文档 + doc = Document() + set_global_normal_style(doc) + + # 添加标题 + h = doc.add_heading(level=0) + r = h.add_run('项企流程效能分析结果公示') + set_title_format(h, r) + + # 添加段落 + p1 = doc.add_paragraph( + '为了提升公司整体工作效率,确保跨部门工作高效推进,充分发挥我司自研平台的优势,现基于项企系统数据,利用Superset工具打造了项企流程效能分析平台,能够对我司项企平台的数据进行分析,进而通过量化数据发现我司办公流程流转中的问题,' \ + '现将{}年{}月公司员工处理流程平均响应时长(响应时长为从流程流转到当前节点到当前节点处理完成的耗时,单位:小时)情况结果进行公示如下:' + .format(last_year, last_month)) + set_paragraph_format(p1) + + # 一、公司各部门横向比较情况 + add_chapter_1(doc) + + # 二、各部门成员详情 + add_chapter_2(doc) + + # 保存文档 + report_path = '{}/项企流程效能分析结果公示(全员)-{}年{}月.docx'.format(REPORT_DIR, last_year, last_month) + doc.save(report_path) + logging.info('本地报表创建成功:{}'.format(report_path)) + + # 上传七牛云 + upload_to_qiniu('项企流程效能分析结果公示(全员)-{}年{}月.docx'.format(last_year, last_month)) + + +def create_clickhouse_client(): + # 获取 clickhouse 配置参数 + ch_cfg = config['clickhouse'] + host = ch_cfg['host'] + port = ch_cfg.getint('port') + username = ch_cfg['username'] + password = ch_cfg['password'] + database = ch_cfg['database'] + # 创建 ClickHouse 客户端对象 + return Client(host=host, port=port, user=username, password=password, database=database) + + +def try_create_dir(dir_name): + # 判断目录是否存在,如果不存在,则创建目录 + if not os.path.exists(dir_name): + print(f"目录'{dir_name}'不存在,即将创建...") + os.makedirs(dir_name) + print(f"目录'{dir_name}'创建成功!") + + +if __name__ == '__main__': + try: + try_create_dir(REPORT_DIR) + try_create_dir(LOG_DIR) + logging.basicConfig(filename='{}/runtime.log'.format(LOG_DIR), level=logging.INFO, + format='%(asctime)s.%(msecs)03d - %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + + config = configparser.ConfigParser() + config.read('config.ini') + + client = create_clickhouse_client() + + last_year, last_month = get_report_year_month() + start_time, end_time = get_report_start_end() + + qs1 = db_helper.querystring_procinst_duration_by_company(start_time, end_time) + procinst_duration_by_company = client.execute(qs1)[0][0] + + qs2 = db_helper.querystring_procinst_duration_by_department(start_time, end_time) + procinst_duration_by_department = client.execute(qs2) + + qs3 = db_helper.querystring_procinst_duration_by_senior(start_time, end_time) + procinst_duration_by_senior = client.execute(qs3)[0][0] + + for index, (department, elapse) in enumerate(procinst_duration_by_department): + if procinst_duration_by_senior >= elapse: + procinst_duration_by_department.insert(index, ('高管', procinst_duration_by_senior)) + break + + generate_word_report() # 生成报表文件 + except Exception as error: + print('程序运行出错:', error) + logging.error('程序运行出错:{}'.format(error)) + + # input("请按任意键退出...") diff --git a/code/pep-stats-report/requirements.txt b/code/pep-stats-report/requirements.txt new file mode 100644 index 0000000..749d13d --- /dev/null +++ b/code/pep-stats-report/requirements.txt @@ -0,0 +1,36 @@ +altgraph==0.17.3 +APScheduler==3.10.1 +backports.zoneinfo==0.2.1 +certifi==2022.12.7 +charset-normalizer==3.1.0 +clickhouse-driver==0.2.5 +contourpy==1.0.7 +cycler==0.11.0 +decorator==5.1.1 +fonttools==4.39.3 +idna==3.4 +importlib-resources==5.12.0 +kiwisolver==1.4.4 +logging==0.4.9.6 +lxml==4.9.2 +matplotlib==3.7.1 +numpy==1.24.2 +packaging==23.0 +pefile==2023.2.7 +Pillow==9.5.0 +pyinstaller==5.10.1 +pyinstaller-hooks-contrib==2023.2 +pyparsing==3.0.9 +python-dateutil==2.8.2 +python-docx==0.8.11 +pytz==2023.3 +pytz-deprecation-shim==0.1.0.post0 +pywin32-ctypes==0.2.0 +qiniu==7.10.0 +requests==2.28.2 +self==2020.12.3 +six==1.16.0 +tzdata==2023.3 +tzlocal==4.3 +urllib3==1.26.15 +zipp==3.15.0