“silver iMac on brown wooden desk” by Roman Bozhko on Unsplash

自动化工具 Huginn 入门指南

Huginn 是一个类似 IFTTT 的工具,可以按照要求自动完成任务,如监视状态更新、获取网页内容、设置条件触发等。具体的介绍可以看 这里

我们有时候可能会想监视一个网站的更新,例如字幕组更新或是学校教务系统通知,有的网站可能会有 RSS、官方微博等提醒更新的渠道,但如果没有,或是我们想要更迅速地,更清晰地,按照自己的要求输出更新条目的话,我们就可以用上 Huginn 这款工具了。它能够抓取网页、按指定的格式输出更新、去重、筛选、推送到 Telegram 或 Slack 等,配合其他 Agents 和 Scenarios 还能实现更多的自动化任务。

安装 Huginn

安装这些工具总是一件麻烦的事,但有了 Docker,就可以省下很多事,不必再跟配置环境时的各种出错打交道。这次我使用 Docker 安装 Huginn。

P.S. 使用基于 Docker 的服务器管理应用 — — HyperApp 来安装更加便捷,可以参照 此处 完成。

首先要在服务器上安装 Docker,安装方法请见 此处

Huginn 的使用依赖 MySQL 数据库,可以接入 MySQL 的 Docker 容器,也可以使用 Huginn 镜像的内置数据库。使用独立的 MySQL 容器的话有利于在主机内共享 MySQL 服务,不必启动多个 MySQL 占用资源。

接入 MySQL 容器首先需要有一个 MySQL 容器:

docker run -d --name mysql \
-e MYSQL_DATABASE=数据库名 \
-e MYSQL_USER=数据库用户名 \
-e MYSQL_PASSWORD=数据库密码 \
-e MYSQL_ROOT_PASSWORD=数据库Root密码 \
-v /data/mysql:/var/lib/mysql \
mysql:5.7

Huginn 似乎不兼容更高版本号的 MySQL,所以这里选用的是 5.7 版本。数据库会被备份至服务器里的 /data/mysql 目录里。

然后安装并启动 Huginn:

docker run -d --name huginn \
--link mysql:mysql \
-p 3000:3000 \
-e HUGINN_DATABASE_NAME=数据库名 \
-e HUGINN_DATABASE_USERNAME=数据库用户名 \
-e HUGINN_DATABASE_PASSWORD=数据库密码 \
-e TIMEZONE="Beijing" \
huginn/huginn

上面还配置了时区,其他的环境配置项可在 此处 查阅。稍等几分钟之后访问 服务器域名或 IP:3000 ,即可看见登录界面。登录名 admin ,密码 password ,登入后可更改密码。

也可以使用内置数据库,省去单独安装 MySQL。建议挂载一个目录来备份内置数据库,如下例将备份至 /data/mysql 路径:

docker run -d --name huginn -p 3000:3000 -v /data/mysql:/var/lib/mysql -e TIMEZONE="Beijing" huginn/huginn

使用方法

在 Huginn 里有几个概念:Agents 是执行任务的一个个环节,它们分别是不同的功能,完成一项任务中的一环。我们需要通过配置 Agents,排列 Agents 来完成一项任务。

Events 是每个 Agent 之间传递的信息,例如网页抓取 Agent 抓出来的更新就会成为一个 Event,传送给下一个 Agent 例如格式化 Agent,通过对网页内容的格式化输出一个 Event,再传送给 Telegram Agent,就能完成一项抓取网页更新后推送给 Telegram 的任务。

Scenarios 是 Agents 的集合,我们自己可以选择 Agents 添加成为一个 Scenario,一般一个 Scenario 是完成一项任务的 Agents 的集合。通过对 Scenario 的管理实现对 Agents 的分组批量管理。

Credentials 里面可以保存常量,在 Agents 中调用的时候使用 {% credential name %} 即可调出保存的 value

常用 Agents

登陆后点击顶部导航栏的 Agents — New Agent,即可开始新建 Agent。不同的 Agent 配置也不同,对 Agent 和其配置项的介绍在配置页面的右边栏里有详细说明。

Schedule 指定该 Agent 的自动执行频率,一般一系列 Agents 里只需要指定第一个 Agent 的 Schedule,后面的 Agent 就能跟着动了。也可以不设定 Schedule,利用另一个 Schedule Agent 来控制自启动。

Controllers 是控制这个 Agent 的启动、执行、停用等的另一个 Agent,一般不用填,当你配置了另一个 Agent 时会自动填上的。

Keep events 指定 Event 的保存时长,对于网页抓取 Agent 的话保存的 Events 可以帮助它检测网页是否更新。如果后面会配置去重 Agent 的话这里就不必设置很长的时间。

Sources 是上一个 Agent,也就是会把 Event 传过来的 Agent。Receivers 则是下一个 Agent,即本 Agent 所产生的 Event 的接受者。在还没有建立别的 Agent 的时候这两项都可以留空,以后会自动填上的。

Propagate immediately 勾上的话,在 Event 传入后会即时执行 Agent。否则 Event 传入后要等上一点时间再和其他 Agent 凑一起执行。开启这个选项会使资源占用增加,对于去重 Agent 建议不要开启,作者说这样会有 Bug。

解析网页:Website Agent

Options 里进行配置,配置项的写法需要遵循 JSON 语法。

url 里填入要抓取的网页,一般是最新文章的页面。把 url 改成 url_from_event 可以填入传入的 Event 内的项目,如 {{url}}

mode 里填 on_change 则只输出更新的条目,all 会输出所有条目,merge 则会跟传入的 Event 合并。type 指定文档类型,分别是 xml html json 或者 text ,不同类型抓取写法不一样,示例可以看配置页面右边栏。

接下来以某个网页为例子,示例如何对网页的结构进行分析。打开要监视的网页,可以看到一个文章列表。现在要做的就是发现文章列表内每条链接的结构,并使用 XPath 语法表达出来。

右击文章列表内的链接,查看元素,发现每个 a 外面都包裹着一个 idlineu9_xli,虽然看起来很有规律,但是我还没找到这样的规律要怎么用 XPath 表达出来。

所以要换一条路走,再观察一下,发现所有链接其实都在 classsubstance_rtr 里面,有这个唯一的 class 就好办了。我可以让 Website Agent 寻找这个页面内 classsubstance_rtr 里面的 li 里面的所有 a。写成 XPath 就会像这样:

//td[@class='substance_r']//li/a
  • td[@class='substance_r'] 表示选取拥有 substance_rclass 属性的 td
  • //td[@class='substance_r']指选取所有匹配的td,不管是不是根元素;
  • //td[@class='substance_r']//li 指选取该 td 下的所有 li,不管 li 是不是根元素;
  • //td[@class='substance_r']//li/a 表示选取该 li 下的 a,并且两者之间没有其他元素包裹。

回到新建 Agent 的页面,点击 Options 旁边的 Toggle View,在 "extract" 下的 "url""css" 里,把"css" 改成 "xpath" ,填上刚才写好的路径,用 "value": "@href" 指出网址在 href 里。

同理,在下面的 "title" 中,由于文章链接的标题和地址都在同一个 a 里,所以直接用上面的路径,但是 "value" 的值 "@href" 要改成 "@title"

然后把原本模板里的 "hovertext" 修改为 "date",以获取文章更新时间。回到网页里,看到更新时间是在与 a 同级的 span 里,所以把 XPath 路径里的 a 修改成 span。但是网页里的时间是显示成 (01-17) 的样子,需要把它转为 01/17 的形式。这里可以用到 XPath 字符串函数 中的 transfer('abc','b','d'),可以把字符串 abc 中的 b 替换成 d

P.S. 其实在 "css" 里还可以写成 CSS 选择器的形式,并不强制使用 XPath。

完成之后可以点击 Dry Run 看看效果,满意就点击 Save 保存。

格式化输出:Event Formatting Agent

从网页 Agent 里生成的 Agent 一般都是 JSON,要格式化的话就要用到这个 Agent。在 Options 里进行配置。

modemerge 会把新生成的 JSON 和旧 Event 合并成为新 Event,选 clean 则不合并。

instructions 里描述如何格式化。支持 Liquid 模板语言,具体用法请点 此处

我常用的有:{{date|date:\”(%-m/%-d/%-H:%M)\”}},格式化日期;{% line_break %} ,输出一个换行;{{text| regex_replace: ‘\\n.*’,’’}} ,正则替换;{{text | remove: ‘1234’}} ,删掉匹配内容;{{\”now\”|date:\”(%-m/%-d/%-H:%M)\”}} ,输出当前时间。

判断过滤:Trigger Agent

可以对 Event 里的某一项进行判断,结果为假则被过滤不会进入下一个 Agent。

path 里填被判断的项目,如 titlekeep_event 为真则保留传入的 Event 内容; message 里可以填入 Liquid 表达式,可以一并被输出到新 Event 里。

type 可以是regex !regex field<value field<=value field==value field!=value field>=value field>valuenot in ;在 value 里填正则式,或用来对比的数值,或者 not in 的范围。

去重:De Duplication Agent

可以存储一段时间的 Event,当新 Event 进入时判断与旧 Event 有无重复,重复则不输出。对这个 Agent 的 Keep events 可以设置得稍长。

Options 里的 property 指定要去重的旧 Event 里的哪一项,一般会是 {{url}}lookback 指定要对多久前的 Events 进行比对,设定为 0 则对存储的所有 Events 进行比对。

生成 RSS:Data Output Agent

Source 里选择一个 Agent 作为 Event 的来源,如上面的网页 Agent。在 Options 里填写:

{
"secrets": [
"a-secret-key"
],
"expected_receive_period_in_days": 2,
"template": {
"title": "通知公告 - 本科生院",
"description": "通知公告 - 本科生院",
"link": "http://uc.edu.cn/tzgg.htm",
"item": {
"title": "{{title}}",
"link": "http://uc.edu.cn/{{url}}",
"pubDate": "{{date}}"
}
},
"ns_media": "true"
}

默认的模板里没有 "link""pubDate",所以要自己增加。如果获得的 url 里面没有网站的域名,在这里的 {{url}} 之前可以把域名加进去。

保存好以后,当上游 Agent 推送 Event 到这里,点击 Agent 列表里这个 Agent 的名字,就可以看到输出的两个链接了。

复制 .xml 的链接,直接添加到 RSS 阅读器里,或是托管到 FeedBurner 后添加进 RSS 阅读器里,就获取到这个网站的更新了。

Telegram 推送:Telegram Agent

使用此 Agent,可以将接收到的 Event 通过 Bot 发送到某个频道、群组或个人。Agent 会把传入的 Event 的 text 里的内容发送出去,建议上游建立一个格式化 Agent 以控制推送信息的格式。

使用前需要申请一个 Telegram Bot,申请方法可以参考 此处。配置好 Options 就可以用了。

自动执行:Scheduler Agent

通过 Cron 表达式编排时间表,自动对某个 Agent 执行启用、禁用、运行操作。Control targets 里选择被控制的 Agent。 Optionsaction 指定操作, schedule 里写时间表。

参考资料

让所有网页变成 RSS — — Huginn