3 min read

R语言实例:利用企业微信群机器人播报数据

群聊机器人

现在有很多企业使用第三方的钉钉或者企业微信作为办公使用的即时通讯工具,两者都提供了群聊下添加自定义机器人的功能,通过简单的协议就可以自动发送自定义的消息。

在数据开发和分析领域,使用该类自定义机器人,来实现数据的播报,是最常见的应用场景。

官方参考文档如下:

企业微信群机器人配置文档 https://work.weixin.qq.com/help?doc_id=13376

钉钉群自定义机器人接入 https://ding-doc.dingtalk.com/document#/org-dev-guide/qf2nxq

两者基本原理相同,都是在至少三人组成的群聊中,添加自定义机器人,获得对应该群机器人的唯一指向的Webhook地址,通过向该地址发送指定格式的内容请求,机器人就按照约定的协议将内容发送个到群里,并在文本类型和Markdown类型中支持 @ 某人。

企业微信群机器人,支持发送图片和文件。

该机器人本质就是一个非常简单的网络服务API,用什么程序语言都可以实现,最常用的应该是 Java 和 Python。

本实例使用R语言,利用 httr 包就可以轻松实现。

行业应用

群机器人有如下:运维,

声明

如下示例,已经实际运行可用,但最终提供出来的 webhook 地址是非法的,故而直接重复运行如下代码是不能执行成功的。

只要修改 webhook 地址为自己机器人的即可。

https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=240f6639-e7f8-43e2-8fd0-ddbc37bc94d6

httr

httr , 是 tidyverse 项目的一部分,将处理网络API的请求便捷化。

httr主页 https://httr.r-lib.org/

目标

使用 iris 数据集,将 Sepal.Length 的均值,发送到群中,实现 文本类,Markdow 类,图片、图文消息、文件等类型都实现一遍。

R语言实现

# 载入所需的R语言包
library(httr)      # 处理网络请求和返回结果
library(magrittr)  # 使用 %>% 这个 pipeline
library(glue)      # 处理发送的文本内容
library(dplyr)     # data.frame 数据处理
library(ggplot2)   # 画图
# 初始化webhook地址
key <- "240f6639-e7f8-43e2-8fd0-ddbc37bc94d6" # 群机器人的唯一ID,且与群对应

webhook <- glue("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}")
# 定义要提醒的群成员(使用企业微信绑定的手机号)
# 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
mentioned_mobile_list <- c("15821375093")

mentioned_mobile_list_all <- c("15821375093", "@all")

文本类型

{
    "msgtype": "text",
    "text": {
        "content": "广州今日天气:29度,大部分多云,降雨概率:60%",
        "mentioned_list":["wangqing","@all"],
        "mentioned_mobile_list":["13800001111","@all"]
    }
}
# 消息内容
# 在 glue 中可以使用表达式
text_content <- glue("在 iris 数据集中,Sepal.Length 的均值为 {iris %>% pull(Sepal.Length) %>% mean() %>% round(2)}")

print(text_content)
## 在 iris 数据集中,Sepal.Length 的均值为 5.84

只有文本类型才能提醒群内的成员

# 消息主体,不指定提醒群里的成员
text_message_body_1 <- list(msgtype = "text", text = list(content = text_content))

# 消息主体,提醒群中的指定成员
text_message_body_2 <- list(msgtype = "text", 
                          text = list(content = text_content,
                                      mentioned_mobile_list = c("15821375093")
                                      )
                          )

# 消息主体,提醒群中的指定成员(可选),@ll 是提醒所有人
text_message_body_3 <- list(msgtype = "text", 
                          text = list(content = text_content,
                                      mentioned_mobile_list = c("15821375093", "@all")
                                      )
                          )
# 发送请求,如果地址、手机号、内容没有问题
# 在群中机器人就会发出对应的消息
# POST(webhook, body = text_message_body_1, encode = "json")
# 
# POST(webhook, body = text_message_body_2, encode = "json")
# 
# POST(webhook, body = text_message_body_3, encode = "json")

markdown类型



# 消息内容
# 在 glue 中可以使用表达式
markdown_content <- glue('Sepal.Length 均值为<font color=\"warning\"> {iris %>% pull(Sepal.Length) %>% mean() %>% round(2)} </font>,三类 Species 均值分别为
> setosa: <font color=\"comment\"> {iris %>% filter(Species == "setosa") %>% pull(Sepal.Length) %>% mean() %>% round(2)} </font>
> versicolor: <font color=\"comment\"> {iris %>% filter(Species == "versicolor") %>% pull(Sepal.Length) %>% mean() %>% round(2)} </font>
> virginica: <font color=\"comment\"> {iris %>% filter(Species == "virginica") %>% pull(Sepal.Length) %>% mean() %>% round(2)} </font>
')


print(markdown_content)
## Sepal.Length 均值为<font color="warning"> 5.84 </font>,三类 Species 均值分别为
## > setosa: <font color="comment"> 5.01 </font>
## > versicolor: <font color="comment"> 5.94 </font>
## > virginica: <font color="comment"> 6.59 </font>
# 消息主体
markdown_message_body <- list(msgtype  = "markdown", 
                          markdown = list(content               = markdown_content,
                                          mentioned_mobile_list = mentioned_mobile_list_all
                                      )
                          )
# 发送请求,如果地址、手机号、内容没有问题
# 在群中机器人就会发出对应的消息
# POST(webhook, body = markdown_message_body, encode = "json")

图片类型

{
    "msgtype": "image",
    "image": {
        "base64": "DATA",
        "md5": "MD5"
    }
}
iris %>%
  ggplot(aes(x = Sepal.Length, y = Sepal.Width, color = Species)) +
  geom_point() +
  xlab("长") +
  ylab("宽") -> 
  iris_plot

ggsave(filename = "iris_plot.png", plot = iris_plot)
# knitr::image_uri 和 RCurl::base64Encode 的结果是包含了前面 “data:image/png;base64”
# 前面这个类型信息企业微信机器人信息是不需要的(包含的话会调用使用)用 base64enc::base64encode 能成功

# 图片消息
image_message_body <- list(msgtype = "image", 
                           image   = list(base64 = base64enc::base64encode("iris_plot.png"), 
                                          md5 = tools::md5sum("iris_plot.png")
                                          )
                           )
# 发送请求,如果地址和内容没有问题
# 在群中机器人就会发出对应的消息
# POST(webhook, body = image_message_body, encode = "json")

图文类型

{
    "msgtype": "news",
    "news": {
       "articles" : [
           {
               "title" : "中秋节礼品领取",
               "description" : "今年中秋节公司有豪礼相送",
               "url" : "URL",
               "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"
           }
        ]
    }
}
# 图文消息
news_message_body <- list(msgtype = "news", 
     news = list(
       articles = list(
         list(
           title = "中秋节礼品领取", 
           description = "今年中秋节公司有豪礼相送",
           url = "www.qq.com",
           picurl = "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"
             )
         )))
# 发送请求,如果地址和内容没有问题
# 在群中机器人就会发出对应的消息
# POST(webhook, body = news_message_body, encode = "json")

文件类型

文件类型,并不在官方网站的介绍中,只在企业微信群机器人配置提示窗口内,其文档描述得不够详尽。

该文件类型的消息发送要分两个步骤:

首先,要通过另一个API将文件上传到企业微信服务器上,并从返回结果中,获得该媒体文件成功上传的唯一标识;

之后,才是发送请求消息,包含文件标识,告诉机器人显示哪个具体的文件。

# 先将数据保持为媒体文件
write.csv(iris, file = "iris.csv")

uppload_file_api <- glue("https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={key}&type=file")


# 上传文件并接返回结果存储在 res 中
res <- POST(uppload_file_api, 
            body = list(media = upload_file("iris.csv")), 
            encode = "multipart") # 这里的 encode 需要用 multipart

# 获得上传后的文件ID, 拼接成文件消息主体
file_message_body <- list(msgtype = "file", file = list(media_id = content(res)$media_id))


# 推送消息(文件)
# POST(webhook, body = file_message_body, encode = "json")