利用 huginn 收集最新的猫眼电影上线咨询并推送 slack

安装 huginn

网上关于如何安装 huginn 的文章太多了, 这里不做介绍, 我自己使用的是 huginn 的 docker 安装方式, docker-compose.yml 如下

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
version: '2'
services:
mysql:
image: mysql:5.7
volumes:
- /data/docker/huginn/mysql:/var/lib/mysql
ports:
- 3310:3306
environment:
MYSQL_ROOT_PASSWORD: <mysql_root_password>
MYSQL_DATABASE: huginn
MYSQL_USER: huginn
MYSQL_PASSWORD: <mysql_password>
restart: always
huginn:
image: huginn/huginn
restart: always
environment:
HUGINN_DATABASE_NAME: huginn
HUGINN_DATABASE_USERNAME: huginn
HUGINN_DATABASE_PASSWORD: <mysql_password>
INTENTIONALLY_SLEEP: 10
PORT: 3000
MYSQL_PORT_3306_TCP_ADDR: mysql
MYSQL_PORT_3306_TCP_PORT: 3306
ports:
- 3000:3000
links:
- mysql

启动后, 配置 nginx 使用 ssl 时发现, 每次登录总是跳转到首页, 变为未登录状态, 解决方式是参考 nginx-ssl, 添加必要的 proxy_set_header:

1
2
3
4
5
6
proxy_set_header  Host                $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;

安装完成后, 通过默认用户名密码 admin / password 即可登录.

配置 huginn

huginn 中的几个关键术语解释一下:

  • Agent

代理, 简单理解就是一个操作, 比如发送消息, 解析网页等等.

  • Scenarios

场景, 场景由多个 agent 组成, 通过 agent 构建一个有向图.

  • Event

事件, 即 agent 的执行的中间结果, 前一步的 agent 将处理后的信息以 event 的形式抛出去, 由下一步的 agent 去接着处理. 这里就涉及到 agent 中的两个概念, sources 和 receivers, 分别定义的是当前 agent 的前置和后置 agent.

以当前我们的任务为例. 我们要收集信息的页面为 https://maoyan.com/films:

可以看到, 我们能从这个页面获取到最新电影的 中文名称/缩略图/分数, 我们第一步就是要解析这个页面, 获取最基本的信息, 第二步去每部电影的详情页去获取电影的详细信息(介绍, 时间等等), 第三步将获取的信息格式化为 slack message 进行发送.

这里, 我们先创建一个名为 movie 的 Scenarios.

step 1

通过查看页面元素, 我们不难去定位每一个字段

我们新建一个 agent, 类型为 Website Agent, 这个 agent 用于处理页面元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"expected_update_period_in_days": "2",
"url": "https://maoyan.com/films?showType=1&sortId=1",
"type": "html",
"mode": "on_change",
"extract": {
"url": {
"xpath": "//div[@class=\"movie-item\"]/a/@href",
"value": "."
},
"poster": {
"xpath": "//div[@class=\"movie-poster\"]/img[1]/@src",
"value": "."
},
"title": {
"xpath": "//div[@class=\"channel-detail movie-item-title\"]/@title",
"value": "."
},
"score": {
"xpath": "//div[@class=\"channel-detail channel-detail-orange\"]",
"value": "string(.)"
}
}
}

通过点击 Dry Run 可以进行测试. 我们看到可以拉取到的数据如下:

1
2
3
4
5
6
7
8
[
{
"url": "/films/1217651",
"poster": "//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png",
"title": "小公主艾薇拉与神秘王国",
"score": "4.8"
}
]

这个数组中的每个 {} 都将是一个 event 向外发布.

step 2

这里我们再创建一个 agent, url 使用上一步给出的 url 进行拼接.

另外还有一个细节是, 这里的 mode 为 merge, 意思是, 将上一步的 event 带过了的字段和当前 event 进行合并, 往下一步传.

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
{
"expected_update_period_in_days": "2",
"url": "https://maoyan.com{{url}}",
"type": "html",
"mode": "merge",
"extract": {
"titleCn": {
"css": ".movie-brief-container .name",
"value": "normalize-space(.)"
},
"titleEn": {
"css": ".movie-brief-container .ename",
"value": "normalize-space(.)"
},
"cover": {
"css": ".avatar-shadow .avatar",
"value": "@src"
},
"ellipsis": {
"css": ".movie-brief-container ul",
"value": "normalize-space(.)"
},
"description": {
"css": ".tab-desc .dra",
"value": "normalize-space(.)"
}
}
}

点击 Dry Run, 在 Event to Send 中填入一个上一步的 event. 如:

1
2
3
4
5
6
{
"url": "/films/1213193",
"poster": "//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png",
"title": "猫与桃花源",
"score": "8.5"
}

可以看到我们获取到了更详细的影片信息

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"url": "/films/1213193",
"poster": "//ms0.meituan.net/mywww/image/loading_2.e3d934bf.png",
"title": "猫与桃花源",
"score": "8.5",
"titleCn": "猫与桃花源",
"titleEn": "Cats and Peachtopia",
"cover": "http://p0.meituan.net/movie/aadd3c954b697c16455e56f35f12c4be336103.jpg@464w_644h_1e_1c",
"ellipsis": "动画,冒险,家庭 中国大陆 / 105分钟 2018-04-05大陆上映",
"description": "一只常常在窗台发呆的家猫毯子(李宇峰 配音),一直以来和儿子斗篷(杨砚铎 配音)安逸地生活在城市的一座高楼公寓中。有一天毯子不得不和斗篷分别踏上冒险旅程,去寻找传说中的猫的桃花源。与此同时,毯子必须面对心中一段不愿提起的往事,或许这也是毯子害怕外面的世界,不愿意离开家的重要原因......"
}
]

step 3

先要为 slack 创建一个 app, 用于接受消息. Create New App.

为这个 app 创建一个 incoming_webhook. 格式应该是

https://hooks.slack.com/services/XXXXXXXXXX/xxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxx

关于发送 slack, 虽然 huginn 提供了一个 slack agent, 但使用起来仅仅发送最简单的 message 可以使用, 无法构建复杂的格式. 我们改用 post agent.

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
40
41
42
43
44
45
46
47
48
49
50
51
{
"post_url": "<slack incoming webhook>",
"expected_receive_period_in_days": "1",
"content_type": "json",
"method": "post",
"payload": {
"username": "猫眼电影",
"icon_url": "https://img3.doubanio.com/pics/douban-icons/favicon_48x48.png",
"attachments": [
{
"fallback": "{{ titleCn }} is online NOW!!",
"mrkdwn_in": [
"text",
"pretext"
],
"color": "#36a64f",
"pretext": "Hi~ Dan. There is a new movie.",
"title": "{{titleCn}}",
"title_link": "https://maoyan.com{{url}}",
"text": "{{ ellipsis }}",
"fields": [
{
"title": "Alias",
"value": "{{ titleEn }} ",
"short": true
},
{
"title": "Score",
"value": "{{ score }}",
"short": true
},
{
"title": "Description",
"value": "{{ description }}",
"short": false
}
],
"image_url": "{{ cover }}",
"thumb_url": "{{ poster }}",
"footer": "猫眼电影",
"footer_icon": "http://www.xz7.com/dir/UploadPic/2014-11/2014111011382236405.jpg"
}
]
},
"headers": {
"Content-Type": "application/json"
},
"emit_events": "false",
"no_merge": "false",
"output_mode": "clean"
}

显示效果如下:

关于 slack 的消息格式, 不是本文的重点, 具体可以参考这里 An introduction to messages.