R

利用R撰写word,自动化文档输出 —— 抛砖引玉

R Package officer 1

Posted by Frank Zhou on 2019-04-23

【package officer】利用R撰写word,自动化文档输出 —— 抛砖引玉

近日,因为朋友A工作需要,提出了一个需求:每天(或每周)都需要生成日报(周报),输出到word,但是一个个填写替换,真的很麻烦,有没有快捷的方法?

例如下面的每日新闻:

或者下面的每日市场概况:

我想是不是考虑用R来完成这项工作?幸运的是,给我找到了officer这个包,专门处理word或者PowerPoint。

我对需求的理解:

  1. 如果只是搜集文字资料然后输出,本没什么难,用剪切板工具(例如Copied) + markdown就可以胜任,但是对于大部分商业公司来说,Word才是通用的文件格式,对于一份公司文档来说,可能不仅仅是对内交流的材料,也可能会作为外部文档交付给客户。其中的页眉页脚、标题文字格式、间距大小、公司Logo、目录等等style elements都有要求,在这一点上markdown就无法胜任了,需要兼顾文字搜集 + 排版两方面的需求。
  2. 因此,将文字搜集 + 排版进行分离。
    • 文字搜集:采用Excel表格的方式进行;以一定的规则,设定好标题等级模板,剩下来User所需要做的,就是在特定的表格内粘贴所需的文字即可。
    • 排版:交给R + package(officer)。事先设定好公司所需要的模板,资料搜集完成之后,运行程序,得到docx。

Package Officer介绍

officer包能够让R user操作Word以及PowerPoint文件,可以添加图片、表格、文字到上述的文件中,而原始文档的属性以及风格都会保留。和ReporteRs相比,Officer速度更快,并且不需要rJava。作者提到:

Make corporate reporting with minimum hassle
目标就是让企业报告的制作变得更加简单

Github 地址:https://github.com/davidgohel/officer
Online documentation:https://davidgohel.github.io/officer/

其中主要包括了六大类型主要操作:

  1. 添加元素
  2. 光标(Cursor)操作
  3. 移除操作
  4. 替换操作
  5. 表格与图的注解

下面我将以任务需求为导向,简述工作流程,对用到的操作语句给予解释。而其他的操作,感兴趣的读者可以查看文档。

工作流程

准备工作

主要分为两个方面:Word 与 Excel

Word

主要是对于公司文档的样式进行检查,如果不符合标准,则进行调整。有意识统一文档格式的公司,应该员工都会有标准的word模板(dtom文件),但是我看到过太多文档格式不统一的文件了,累觉不爱,每个人都有每个人的freestyle。

题外话:如果有心的读者,往后需要和word大量打交道,可以好好系统学习一下Word的排版逻辑,事半功倍。当初我也是小白,后来因为要帮导师撰写以及排版一本10万字左右的书籍,所以就花时间学习了下,真的太值了。除了对代码的支持不够好,还真没发现什么致命的缺点,排版王者不是盖的。(一不小心,成了word吹了,匿了匿了)

调整也不难:

  1. 确认自己文档的标题等级:
    • 保证将每一个标题都涵盖进来,做到文档的标题和样式中的标题都有对应. 不管是多少级,哪怕6级7级,都要严格有对应。之所以需要做对应,是因为有很多朋友写word习惯很差,可能很多标题层级下来,写着写着想来一个小标题,这个小标题没有用样式中的标题样式,而是直接加粗一下,就算是一个小标题了, 现在是这样没问题,后期编辑和大纲查看的时候,就有的受了。
    • 根据自身需求对每一级标题做格式的调整,这个是个性化的需求
  2. 确认自己文档中的正文:除了标题以外的内容,都是正文。正文和结构无关,只有标题和结构有关。一般而言,正文只有一种样式。但是也有的文档采用了多种正文样式。例如普通文字是一种,引文是一种,重点是一种。不管有几种,也都要保证正文在样式中也有对应

PS:样式大家不陌生,就是这个

Excel

按照文档格式当中的标题等级,填充Excel。

前三列为标题,分别为一、二、三级标题,没有就空着,只需要填写一次即可(第一次出现),第四列为正文,每一格正文都代表一个段落。

可以看做是树状的结构,例如,第三行、第四行的正文都属于行业新闻 - 昨日头条下的正文;第五行正文则属于行业新闻 - 大公司下的正文。

朋友A的需求就是前三列的标题都是固定的,然后去填充正文内容。所以,只需要事先将前三列标题填好且保存,然后每日在excel中的context录入就可以了。可以将每一个标题下多插入一些空行,没问题,后面的程序遇到空值会自动跳过,如此,就不必因为某一标题下的内容过多,而需要额外再在excel中插入空行,节省操作。

The Program

1
2
3
4
5
6
# All the packages I used
library(lubridate)
library(officer)
library(stringr)
library(tidyverse)
library(magrittr)

officer中采用read_docx()来创建一个R Object,名为rdocx。

1
2
my_doc <- read_docx() # 新的rdocx对象
styles_info(my_doc) # 查看文档的style信息

注意:style信息是非常重要的,可以说Word排版大部分精髓都在这里,这里的style包含了Word中的样式,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# read_docx可以读取已存在的word文件
my_doc <- read_docx('raw_word.docx')
# 此处以我的文档为例
> styles_info(my_doc)
style_type style_id style_name is_custom is_default
1 paragraph a0 Normal FALSE TRUE
2 paragraph 1 heading 1 FALSE FALSE
3 paragraph 2 heading 2 FALSE FALSE
4 paragraph 3 heading 3 FALSE FALSE
5 paragraph 4 heading 4 FALSE FALSE
6 paragraph 5 heading 5 FALSE FALSE
7 paragraph 6 heading 6 FALSE FALSE
8 paragraph 7 heading 7 FALSE FALSE
9 paragraph 8 heading 8 FALSE FALSE
10 paragraph 9 heading 9 FALSE FALSE
11 character a2 Default Paragraph Font FALSE TRUE
12 table a3 Normal Table FALSE TRUE
13 numbering a4 No List FALSE TRUE
14 paragraph a1 List Paragraph FALSE FALSE
15 paragraph a5 Normal (Web) FALSE FALSE
16 character a6 Strong FALSE FALSE
17 paragraph a7 header FALSE FALSE
18 character a8 页眉 字符 TRUE FALSE
19 paragraph a9 footer FALSE FALSE
20 character aa 页脚 字符 TRUE FALSE
21 paragraph ab Balloon Text FALSE FALSE
22 character ac 批注框文本 字符 TRUE FALSE
23 paragraph ad Title FALSE FALSE
24 character ae 标题 字符 TRUE FALSE
25 character 10 标题 1 字符 TRUE FALSE
26 character 20 标题 2 字符 TRUE FALSE
27 character 30 标题 3 字符 TRUE FALSE
28 paragraph a No Spacing FALSE FALSE
29 character 40 标题 4 字符 TRUE FALSE
30 character 50 标题 5 字符 TRUE FALSE
31 character 60 标题 6 字符 TRUE FALSE
32 character 70 标题 7 字符 TRUE FALSE
33 character 80 标题 8 字符 TRUE FALSE
34 character 90 标题 9 字符 TRUE FALSE

比较重要的是style_typeandstyle_namestyle_type反映的是样式的类型,paragraph就是word中的样式了,也是我们会用到最多的。style_name在后面会用到,添加文字时设定样式时用style_name来指定。举例而言:正文就是Normal,一级标题就是heading 1,主标题就是Title。

接下来是我的具体操作方式:
首先读取原始word文档,该文档包含了你所设定好的标题等级、页眉页脚、字体格式等等,只不过内容是我们不需要的,替换更新。

1
2
3
4
5
6
7
8
# 读取原始文档
my_doc <- read_docx('raw_word.docx')

# 将原始文档的文字全部删除
for(i in 1:(length(my_doc) - 1)) {
body_remove(my_doc)
cursor_end(my_doc)
}

介绍下officer包的几个常见操作

1
2
3
4
5
6
7
8
9
10
11
12
13
# 添加段落(paragraph),所有的文字都是以body_add_par函数添加
# value是添加的内容,style用于指定格式
# style可以是主标题、正文、1级标题、2级标题等等,和styles_info中一致
my_doc <- my_doc %>%
body_add_par(value = "XXX", style = "heading 1")

# 添加目录
my_doc <- my_doc %>%
body_add_toc(level = 2)

# 添加隔页符(下一页)
my_doc <- my_doc %>%
body_add_break()

设定标题

1
2
3
4
5
6
7
8
9
# 添加标题,Title
# 今天的日期
today_string <- str_c(year(today()), '年',
month(today()), '月',
day(today()), '日 ',
weekdays(today()),
' 今日新闻')
my_doc <- my_doc %>%
body_add_par(value = today_string, style = 'Title')

从Excel中读取之前搜集的文字数据,csv格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
> data <- read_csv("data_test_2.csv")
Parsed with column specification:
cols(
heading_1 = col_character(),
heading_2 = col_character(),
heading_3 = col_character(),
context = col_character()
)
> data
# A tibble: 15 x 4
heading_1 heading_2 heading_3 context
<chr> <chr> <chr> <chr>
1 行业新闻 昨日头条 NA 正文1
2 NA NA NA 正文2
3 NA NA NA 正文3
4 NA 大公司 NA 行整合,提升线上线下药品销售的体验,加速在数字零售领域的创新与发展。这是微软今年布局大健康产业的第一步,与谷歌、苹果相比,微软…
5 NA NA NA 微博斥资3.5亿元人民币投资无他相机。完成交易后,天鸽互动与微博分别持有无他所属公司股份51.2%、34.8%。此外,新浪香港…
6 市场概况 宏观预期 NA 我国全行业对外直接投资1298.3亿美元,同比增长4.2%。其中,对外金融类直接投…
7 NA NA NA 度引爆市场、总募集规模高
8 NA 外汇 NA 321321
9 NA A股市场 NA ffdasf
10 国际市场 北美 美国市场 正文1
11 NA NA 美国经济 正文2
12 NA 欧洲 欧洲股市 ceshi
13 NA 日韩 日韩股市 fdasfdasfdas
14 大宗商品 黄金 NA 正文黄金
15 NA 石油 NA 正文石油

正文写入
对于具体的实现,肯定有其他方式,这里我根据我的理解以及结合数据搜集的特点,采用了矩阵 + 遍历的方式,in order to finish the task as soon as possible,期待大神优化和指点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
m <- as.matrix(data)
n <- dim(m)[1] # 行数
for (i in 1:n) {
for (j in seq(4)) { # 总共就4列,前3列为三级标题,第4列为正文
if (is.na(m[i, j])) { # 空值跳过
next
} else if (j %% 4 == 1) { # 如果是第一列,heading 1
if (i != 1) { # 除了第一个一级标题外,其余一级标题和前文之间空一行
my_doc <- my_doc %>%
body_add_par(value = '\n')
}
my_doc <- my_doc %>%
body_add_par(value = m[i, j], style = 'heading 1')
} else if (j %% 4 == 2) { # 如果是第二列,heading 2
if (is.na(m[i, j - 1])){ # 如果紧跟着一级标题,二级标题不空行
my_doc <- my_doc %>%
body_add_par(value = '\n')
}
my_doc <- my_doc %>%
body_add_par(value = m[i, j], style = 'heading 2')
} else if (j %% 4 == 3) { # 如果是第三列,heading 3
if (is.na(m[i, j - 1])){ # 如果紧跟着二级标题,三级标题不空行
my_doc <- my_doc %>%
body_add_par(value = '\n')
}
my_doc <- my_doc %>%
body_add_par(value = m[i, j], style = 'heading 3')
} else if (j %% 4 == 0) { # 如果是第四列,正文
my_doc <- my_doc %>%
body_add_par(value = m[i, j], style = 'No Spacing')
}
}
}

文档输出

1
print(my_doc, target = 'result.docx')

就测试数据而言,最终得到的测试文档docx如下图所示:


标题、字体、页眉页脚都与原文档一致,而正文内容也得到了相应的填充。

流程梳理

所以,最终的流程如下:

  1. 打开预先设置好的excel表格,将搜集或编辑的文字贴入excel。
  2. 运行程序
  3. 得到排版好的word

反思与探索

反思1

如果只是固定的标题模板,个人认为利用R和手动这两种方式差别不大,因为手动的话,只要事先把正文清空而标题保留的docx保存,下次再写的时候,复制粘贴编辑即可**(粘贴可在word中设定为只粘贴为纯文本,如此粘贴进来的文字自动就匹配文档格式了)**,也没什么麻烦的。

甚至,直接用word也没什么麻烦的。(😭 那我写这个文章干嘛?唉,介绍下officer这个包还是可以的,强行有用)直接用Word:

  1. 首先定义好各级标题样式、正文样式
  2. 关键: 设定好常用的几个样式的快捷键,例如Alt + 1,Alt + M等。可以做到不亚于Markdown的输入流畅度(但是代码是不行了,比不过)。
  3. 开心地去写吧。

在这一点上,我不想欺骗自己,发现折腾了一下,和快手耿哥一样:尽是搞一些没什么卵用的东西。

反思2

但是如果大纲模板不固定,这个方法可能稍许方便一些。例如,我的标题不再是什么行业新闻、市场概况,而是别的内容,如摘要、论文等等。

我实在是编不下去了,没看出有什么方便的地方。

反思3

其实一开始我以为需求是,文档文字内容固定,然后其中一些数值发生变化,比如当日股价收盘价XXX,涨幅XXX,当日黄金价格XXX。我觉得这种方式用工作流会有用一些。

比如,一个Excel中录入当日的这些数字,或者要替换的文字,然后运行程序,进行替换。这样效率比较高,因为如果手工进行,需要找到每个数值的位置,然后一次只能粘贴一个,效率就很低了。

我觉得应该是没问题的,officer中有光标定位函数、有替换函数,结合R字符处理函数,应该可以实现。

反思4

暂时没有考虑图片和表格的情况。但是officer中是可以添加图片和表格的。

excel中只能录入文字,如果遇到其他格式的内容怎么办?因为朋友A的文档里没那么多复杂的东西,所以就没考虑那么多了。

Last but not least

本文借助一个工作中的小例子,简要介绍了officer package的用法,探索了R与Word协作的可能性,妄想了一种日报周报自动化文档的工作流程。

还有很多可以探索与学习的地方,权当抛砖引玉,欢迎大家一起交流,恳请大神不吝指教。