基于占位符的自动生成报告脚本的笔记

自动生成报告脚本

一、太长不看版结论

根据实际需求,将报告中需要修改的内容使用占位符代替,例如:。之后利用Pandas库读取excel文件获取数据,并将数据存入字典,从而得到包含所有需要填入报告的数据的字典。最后使用DocxTemplate库将字典中的数据嵌入进报告中

1
2
3
4
5
# 包含所有需要填入报告的数据的字典
Tag = {
'name': '张三',
'age': 100
}

DocxTemplate库中的render函数会识别word文件中的占位符{{...}},并将字典中的数据一一对应进行填充

1
我叫{{name}},今年{{age}}岁------DocxTemplate.render(Tag)------>我叫张三,今年100

二、基础内容

(一)脚本

4-1

问个比较弱智的问题:已知我电脑上的微信安装在这个路径下,请问要如何启动微信?

第一种方式,也是很普遍直接的方式:直接双击WeChat.exe

第二种方式,那就是打开cmd窗口,在cmd窗口下进入到这个路径,输入WeChat.exe后回车即可,如下:

4-2

现在假设不允许使用第一种方式,只能使用第二种指令式。那这样的话我们每次想要打开微信就都需要输入这么两串指令,好麻烦…

windows电脑上有一类文件叫做批处理文件,后缀名是.bat。.bat文件是可执行文件(可直接双击运行的那种),由一系列Dos命令构成,其中可以包含对其他程序的调用

那我们可以创建个bat文件,并将这两串指令写入,就可以实现类似第一种方式的启动方式

  1. 首先新建个txt文件,然后修改文件名。注意:后缀名要修改成bat!
  2. 然后右键,点击编辑
  3. 键入以下指令,保存退出

最后双击运行这个即可启动微信

4-3

@echo off:这条指令的意思是关闭在cmd窗口中对每条指令的输入展示。可以删掉这条指令再运行bat脚本,就可以看出具体区别

Pause:这条指令意思是当运行到Pause这条指令时,暂停运行,需要手动从键盘按下任意键来继续运行。如果不加这条Pause的话,在bat脚本中所有指令运行结束后,窗口会自动关闭,尤其是当运行的指令不怎么复杂时,窗口差不多是一闪而过,这样就看不到运行过程了。可以删掉这条指令再运行bat脚本,就可以看出具体区别

可以发现bat脚本实现的效果其实就是双击微信图标的效果

现在我们换个场景,把微信程序换成Python程序。在不使用Pycharm、Jupyter等软件中的基础上,我们既想要实现双击直接运行的效果,又想要看到程序运行的过程,怎么做呢?

4-4

使用bat脚本就能实现。我们简单看个例子,编写个脚本:输出前20位斐波那契数列

1、编写python程序,命名为FBNQ_seq.py,输出前20位斐波那契数列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 初始数据
# 首位置0
first = 0
# 末位置1
last = 1

# 输出前20位
for i in range(20):
# 当前数为前两项之和
temp = first + last

last = first
first = temp

print("第{0}位:{1}".format(i+1, temp))
2、创建bat文件,命名为start.bat,键入相关指令
1
2
3
@echo off
python FBNQ_seq.py
Pause
3、完成,双击start.bat即可运行

4-5

(二)“自动生成报告”

在不了解自动生成报告的时候如果尝试去理解、去猜这个是如何实现的时候,通常会进入一个误区:“自动生成报告脚本”的核心在于生成报告,似乎只要实现自动生成报告,所有的困难就全都解决了。(最开始摸索的时候我也进到了这个误区)

但根据实操经验,并不是这样,其核心是隐藏在这几个字背后的数据处理。DocxTemplate库,可以自动将数据填充进报告模板中,这个库实现的就是生成报告。但数据从哪来呢?得我们自己处理;模板从哪来呢?得我们自己处理

所以自动生成报告脚本的制作,难点其实在于数据处理这部分的代码编程以及模板的制作:

  • 数据处理难点:我们一是要读取excel的数据,二是要对每个标识符进行赋值
  • 模板制作难点:我们要对word中需要修改的数据用占位符+标识符的形式进行命名,并且数据不一样或者数据含义不一样的地方我们要使用不同的标识符,也就是每个数据的标识符都是唯一的

三、报告自动生成脚本制作流程

我们看个略微比较复杂的例子(代码及文件详见:报告自动生成脚本示例--简单示例文件夹),实现报告自动生成:

1、报告预览

这是要自动生成的报告,标黄处是需要我们进行修改的地方

4-6

2、数据预览

这是对应的数据表excel,包含两个sheet页

8-1 8-2
3、创建模板

接下来我们根据已有的报告,创建对应的报告模板。我们用占位符来代替我们要修改的内容,然后占位符中需要使用唯一的字符串标识来表示这个数据

4-9

4、处理数据

开始编程,读取excel,创建一个包含所以数据的字典,最终使用DocxTemplate将数据填充进模板中

(1)导库
1
2
3
import datetime
import pandas as pd
from docxtpl import DocxTemplate
(2)初步处理

针对报告模板中的所有标识符,使用字典的键值对来存储数据,以便后续使用相关库来填充数据

1
2
3
4
5
# 创建结果字典,用于存储所有键值对
res_dict = {}

# 获取年份:当前年份 - 1
res_dict['year'] = datetime.date.today().year - 1
(3)读取excel,处理数据

模板中存在两个段落,两个段落的数据刚好对应excel表中的两个sheet,那么我们可以分别进行处理

首先是第一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
first_par = {}

# 读取第一个页签
allyear_df = pd.read_excel('数据.xlsx', sheet_name='全年')

# 将第一个页签中的数据初步合并成一个字典
first_par = dict(zip(allyear_df['指标'].tolist(), allyear_df['人口数量(万人)'].tolist()))

# 最后结合模板中的标识符,分别进行赋值
res_dict['total'] = first_par['全国人口']
res_dict['birth'] = first_par['出生人口']
res_dict['death'] = first_par['死亡人口']

# compare标识符可以使用if-else语句进行赋值,这里为了代码简洁可以这么写
res_dict['compare'] = "多于" if res_dict['birth'] > res_dict['death'] else (
"少于" if res_dict['birth'] < res_dict['death'] else "等于")
# 上述这行代码等价于:
# if res_dict['birth'] > res_dict['death']:
# res_dict['compare'] = "多于"
# else:
# if res_dict['birth'] < res_dict['death']:
# res_dict['compare'] = "少于"
# else:
# res_dict['compare'] = "等于"

然后是第二段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sec_par = {}

# 读取第二个页签
alllevel_df = pd.read_excel('数据.xlsx', sheet_name='各年龄段')

# 将第二个页签中的数据初步合并成一个字典
first_par = dict(zip(alllevel_df['年龄段'].tolist(), alllevel_df['人口数量(万人)'].tolist()))

# 结合模板中的标识符,分别进行赋值
res_dict['level_one'] = first_par['0-15']
res_dict['level_two'] = first_par['16-59']
res_dict['level_three'] = first_par['60岁及以上']

# 计算各年龄段人数占比
res_dict['level_one_rate'] = round(first_par['0-15'] / res_dict['total'] * 100, 1)
res_dict['level_two_rate'] = round(first_par['16-59'] / res_dict['total'] * 100, 1)
res_dict['level_three_rate'] = round(first_par['60岁及以上'] / res_dict['total'] * 100, 1)

所有标识符处理完后,我们可以输出一下res_dict看下是什么样子

1
2
# 输出最终字典,看下存储结果
print(res_dict)

4-10

至此,数据处理的代码全部完成,可以编写生成报告的代码了(悄咪咪的说,生成报告的代码仅用三句即可实现)。

5、生成报告
1
2
3
4
# 给模板填充数据
tpl = DocxTemplate('报告模板.docx')
tpl.render(res_dict)
tpl.save('自动生成报告结果.docx')

打开文件,进行对比,会发现一模一样,成功!

4-11

6、形成脚本

创建个bat文件(创建txt文本文件,后缀名改为bat),同样写入三行代码

4-12

这样我们想要运行Python程序就可以直接双击这个bat文件

4-13

通览整个自动生成报告脚本的制作流程,自动生成报告脚本的制作难点的确在于数据处理这部分的代码编程以及模板的制作,因为生成报告的代码三句就能实现:joy::joy::joy:

四、拓展

根据上面的流程,我们了解到报告的自动生成就是把报告中需要修改的地方使用占位符+标识符的形式替换,从而将数据进行填充。在上面的例子中,主要是对文字内容的自动生成进行了讲解,但在简报中,除了文字部分,还有表格部分

通过上面的例子,我们知道,对于每个需要修改的地方,我们都需要使用一个唯一的标识符来标明。这是个什么概念呢?如果报告里面有100个地方需要修改,且这些地方相互之间不能复用,那我们就需要自行创建100个标识符,同时这些标识符还要确保我们自己能一眼看懂,不能用a、b、c这种简单的形式进行命名(数量多起来的话这种命名方式读起来就是在坐牢,编程时更加坐牢)。

对于文字部分来讲,相对需要创建的标识符还比较少,而且有些地方是可以复用的,所以文字部分需要创建的标识符的数量算是能接受的。但是对于表格部分来讲,那就是表格中的每个单元格都需要一个单独的标识符来标识了。这就非常恐怖了,一张表格要创建的标识符数量等于长 * 宽,数量还能接受,但要确保每个标识符都是唯一的,这就很折磨人了

当初弄那个自动生成脚本的时候,怎样对表格部分创建模板?用什么方式进行标识?着实是被折磨了一阵,甚至想要放弃了

结果运气比较好,灵光一闪,想到了个东西…

4-4

(一)二维坐标系

在二维坐标系里,对每个点位的表示是:(0,0)、(1,1)、(2,3)

我们可以把表格看成一个二维坐标系,每个单元格看成点位

欸!!这方法可太好了,这样子我们的命名方式就非常简单了,我们写代码的时候就不用对每个标识符的赋值都要写代码,而是可以直接用循环进行遍历,来对标识符进行命名以及赋值,省事多了

(二)“自动生成报告:表格部分”

我们同样按前面的步骤来看个稍微复杂一点的例子(代码及文件详见:报告自动生成脚本示例--拓展示例文件夹),实现报告自动生成:

1、报告预览

点开示例结果.docx,报告中包含两张表。第一张是街道统计表,第二张是社区统计表

我们可以计算下需要修改的单元格数量,看看在不使用二维坐标系单元格方式情况下的工作(代码)量:
$$
\begin{flalign*}
& 街道:10×7+6=76(个) \
& 社区:124×6+4=748(个) \
& 一共:76+748=824(个) \
\end{flalign*}
$$
如果不使用二维坐标系点位的方式,我们就得创建824个单独的标识符(如果有更多的表格的话,工作量更大),简直根本没法弄….:cold_sweat::cold_sweat::cold_sweat:

4-14

2、数据预览

这是对应的数据表excel,包含两个sheet页(“街道”、“社区”)

4-15 4-16
3、创建模板

以街道统计表第一行和第二行为例,这里使用了这种命名方式:

4-17

t1_0_1t1_1_0为例:

t1_0_1:

  • t1:表示table1,表1
  • 0:表示第0行(相当于自然数第1行)
  • 1:表示第1列(相当于自然数第2列)

t1_1_0:

  • t1:表示table1,表1
  • 1:表示第1行(相当于自然数第2行)
  • 0:表示第0列(相当于自然数第1列)

另外,因为合计行与这些行的格式不一致,所以对于合计行,我们单独再设计一个命名方式:

4-18

t1s_0_1为例:

t1s_0_1:

  • t1s:表示table 1 sum,即表1的合计
  • 0:表示第0行(相当于自然数第1行)
  • 1:表示第1列(相当于自然数第2列)

按照这样,我们就能将两张表需要修改的单元格分别进行命名,从而创建模板(具体见报告模板.docx):

4-19

**这里需要注意:**通过上面这张图,会发现表格的格式并不是很适配,部分单元格因为换行导致看着很奇怪。这是因为填入标识符的表格是按原来的正常表格填入的。因为只有这样,之后Python程序填入数据的时候才会将表格还原到原来的格式,否则如果填入标识符后再次进行格式适配的话,就会导致填充数据后的表格格式混乱以致于需要人工进行调整

4-20

4、处理数据

这里按表格划分为两部分

(1)表1:街道统计表

导库,读取excel街道表的原始数据:

1
2
3
4
5
6
7
8
9
10
import datetime
import pandas as pd
from docxtpl import DocxTemplate

'''
sheet_name:读取的页签,这里表示读取“街道”页签
nrows:读取的行数,这里表示读取第1至12行
usecols:表示要读取的字段
'''
origin_dataframe = pd.read_excel('数据.xlsx', sheet_name='街道', nrows=12,usecols=['街道', '本月巡查量(宗)', '本月办结量(宗)', '本月办巡比','本月巡查量环比', '本月巡查量同比', '平均值 办结时长'])

因为表1的数据行和合计行的标识是不一样的,所以接下来分别进行处理,之后再将各自的数据字典合并从而得到表1的数据字典

首先是数据行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 表1数据内容,0:9表示获取第0至9行,因为dataframe会默认把第一行当做表头,所以不会计入。这里从0开始获取表示从表头的下面一行开始获取,可以自行print看看效果
df_num = pd.DataFrame(origin_dataframe.loc[0:9])

# 格式化数据内容,这里我们需要将数据按照一定规则进行格式化,比如要加上千位分隔符,百分比要加上百分号并取小数点后两位,这样最终填充的数据才会是我们想要的格式
df_num['本月巡查量(宗)'] = df_num['本月巡查量(宗)'].map(lambda x: format(x, ','))
df_num['本月办结量(宗)'] = df_num['本月办结量(宗)'].map(lambda x: format(x, ','))
df_num['本月办巡比'] = df_num['本月办巡比'].map(lambda x: "-" if str(x) == "nan" else "{:.2f}%".format(x * 100))
df_num['本月巡查量环比'] = df_num['本月巡查量环比'].map(lambda x: "-" if str(x) == "nan" else "{:.2f}%".format(x * 100))
df_num['本月巡查量同比'] = df_num['本月巡查量同比'].map(lambda x: "-" if str(x) == "nan" else "{:.2f}%".format(x * 100))
df_num['平均值 办结时长'] = df_num['平均值 办结时长'].map(lambda x: format(x, ','))

# 创建表1的数据行的字典
t1_dict_num = {}

# 列优先遍历df_num,建二维数组字典:t1_x_x --> content
i = 0
j = 0
for index in df_num:
for content in df_num[index]:
str_ = 't1_' + str(j) + '_' + str(i)
t1_dict_num[str_] = content
j = j + 1
i = i + 1
j = 0

最后的t1_dict_num就是我们需要的字典:

4-21

这部分代码包含两个略微比较复杂的内容,一个是对数据的格式化,另一个则是列优先遍历dataframe

这里简单说明下,后面类似的代码可参照此处进行理解:

1、数据格式化
1
df_num['本月巡查量(宗)'] = df_num['本月巡查量(宗)'].map(lambda x: format(x, ','))

上述这句代码意思是批量添加千位分隔符

1
df_num['本月办巡比'] = df_num['本月办巡比'].map(lambda x: "-" if str(x) == "nan" else "{:.2f}%".format(x * 100))

上述这句代码意思是批量进行修改:当值为nan时,改为“-”;否则将值乘以100后取2位小数并加上%符号(即将小数形式改为百分比形式,并保留小数点后两位)

2、列优先遍历
1
2
3
4
5
6
7
8
9
i = 0
j = 0
for index in df_num:
for content in df_num[index]:
str_ = 't1_' + str(j) + '_' + str(i)
t1_dict_num[str_] = content
j = j + 1
i = i + 1
j = 0

我们解构下上述代码:

1
2
i = 0
j = 0

i表示列数,j表示行数

1
for index in df_num:

遍历df_num的列索引:街道、本月巡查量(宗)、本月办结量(宗)、本月办巡比……

1
for content in df_num[index]:
(2)表2:社区统计表
5、生成报告
6、形成脚本