博客搬遷至https://blog.wangjiegulu.com
RSS訂閱:https://blog.wangjiegulu.com/feed.xml
原文鏈接:https://blog.wangjiegulu.com/2018/04/03/huginn_douban_high_score_movies_and_slack/
Huginn實現自動通過slack推送豆瓣高分電影
如果尚未安裝 Huginn,可以參考這里
想象下以下場景:每當有正在上映的電影在豆瓣上的評分超過7.8分,則 huginn 自動編輯一條信息並通過 Slack
(當然也可以用 telegram 等app)通知到我電腦或者手機上。收到信息后,點擊不喜歡忽略,或者點擊購票按鈕直接進入到購票頁面。甚至 Huginn 可以結合 Google Calendar
查詢你這幾天的行程安排,推送高分電影信息的同時給你選擇一個比較合適觀看電影的時間點,購買好電影票后,huginn 又自動幫你把日程事件寫入到 Google Calendar
中,並設置提醒。是不是很酷?!
Huginn 就如你的貼心管家,按照你的意願自動幫你完成很多事情。
我們先來實現 每當有正在上映的電影在豆瓣上的評分超過7.8分,則給我推送 Slack 信息
這一部分需求。
最后達到的效果如下:

手機端效果

PC端效果
創建 Agents
首先進入 Huginn 首頁(默認localhost:3000
),左上角進入 Scenarios
:

我的理解:Scenario 代表一種場景,一般會包含多個 agent,一個 agent 表示進行一次事件的處理或者變換。拿我們現在的例子來說,自動通過slack推送豆瓣高分電影 這一整個就是一個 Scenario
,但是這個 Scenario
會有很多的 agent
s 組成,比如:
- 有一個 agent 是用來從豆瓣網頁獲取當前上映中的所有電影和它們的分數等信息;
- 一個 agent 是用來從第一個 agent 里面拿到的所有電影進行過濾,過濾的標准就是
score > 7.8
, - 還有一個 agent 是用來把過濾后的電影通過 slack 推送到我們手機上。
看着跟 RxJava
的觀察者模式是不是很像?第一個從豆瓣頁面拉取數據的過程就像是 Observable
,然后其它的 agent 就像很多的 operator
用來把數據進行轉換和變化,最終通知到 subscriber
,這里的 subscriber
就是我們自己。我們通過 huginn 訂閱了 豆瓣高分電影
,就是這么簡單。
點擊左下角的 New Scenario
創建一個名為 douban_high_score_movie
的 Scenario。
創建獲取數據 agent
第一個 agent 用來從豆瓣官網獲取所有正在上映的電影
在 douban_high_score_movie
的 Scenario 中點擊 + New Agent
來創建第一個 Agent。

如上圖,你需要去決定你要創建的 agent 的類型(這里是目前 Huginn 支持的所有的類型)。
我們通過輸入 "web" 來進行過濾選擇 Website Agent
。

上圖,左邊是我們需要去配置的地方;右邊是每個設置對應的說明。
- Name:給這個 agent 取個名字,我們這里取名為
step1_get_douban_playing_movies
,表示這個 agent 是douban_high_score_movie
這個 Scenario 的第一步,是用來從豆瓣獲取當前正在上映的所有電影。 - Schedule:表示調度周期,表示在什么時候自動執行這個 agent,比如
Every 1d
表示每一天執行一次、Every 2h
表示每2小時執行一次、8pm
表示每天下午8點執行等等;這里我們選擇3pm
,每天下午3點執行一次。 - Keep events:表示事件保留的時間;比如我們從豆瓣上獲取到所有上映的電影,每一部電影信息都是一個 event,Huginn會把這些 event 保留在本地,你可以通過這個參數來設置這些 events 在本地保留多少時間,超過這個時間,Huginn會把數據清除。我們這里設置1小時(為什么只設置為1小時,下面我們會再討論)。
- Sources:表示這個 agent 處理的數據來源是哪個 agent。我們現在創建的 agent 是第一個 agent,是從豆瓣網站上獲取正在上映的所有電影,所以不需要從其他 agent 傳遞數據(也就是上面說的 events)過來,所以這個留空。
- Receivers:表示這個 agent 處理完數據之后把這些數據傳入到哪個 agent。還是用
RxJava
做類比,因為每個 agnet 都有可能只是整個觀察者模式中的一個操作符,用來轉化數據,數據轉化完之后,可能還需要其他 agent 把這些數據做進一步的轉化。 - Scenarios:表示這個 agent 是數據哪個 Scenario 的。
- Options:這個非常關鍵,就是通過這個配置文件(JSON)來進行網絡請求和豆瓣電影數據解析相關的操作的,這個我們重點講下。
注意:以上沒提到的配置可以留空
Options 配置
Options 配置其實就是一個 JSON 文件。Website Agent 的 Options 主要的元素有如下:

- url:網站地址,表示我需要從哪個網站獲取數據,現在我們是從豆瓣,所以需要輸入豆瓣正在上映的網址,這里我們輸入
https://movie.douban.com/cinema/nowplaying/hangzhou/
,當然最后一個地點可以根據你的常駐地點做相應的修改。 - type:數據解析的類型,支持的類型有
xml
、html
、json
、text
四種,當前豆瓣網址返回的當然是 html 了,所以這里我們填寫html
。如果其他場景,比如 調用第三方開放的 api,返回的類型可能就是json
或者xml
了。 - mode:表示獲取數據的模式,我們這里選擇
on_change
。- on_change:在數據有更改時才會獲取作為 events。
- merge:把新數據和輸入的數據進行合並。
- all:獲取所有數據。
- extract:用來配置(JSON)從這個網站解析出真正我們想要的數據。如果
type
是html
,則每個數據通過css
選擇器或者xpath
來解析出真正的數據。
注意:
on_change
這個設置在我們現在的場景下其實用處不大,這個下面我們會再討論。
最后的 options 如下:
{
"expected_update_period_in_days": "2",
"url": "https://movie.douban.com/cinema/nowplaying/hangzhou/",
"type": "html",
"mode": "on_change",
"extract": {
"title": {
"css": "li[@data-category='nowplaying']",
"value": "@data-title"
},
"score": {
"css": "li[@data-category='nowplaying']",
"value": "@data-score"
},
"star": {
"css": "li[@data-category='nowplaying']",
"value": "@data-star"
},
"release": {
"css": "li[@data-category='nowplaying']",
"value": "@data-release"
},
"region": {
"css": "li[@data-category='nowplaying']",
"value": "@data-region"
},
"actors": {
"css": "li[@data-category='nowplaying']",
"value": "@data-actors"
},
"director": {
"css": "li[@data-category='nowplaying']",
"value": "@data-director"
},
"detail_url": {
"css": "li[@data-category='nowplaying']/ul/li/a[@data-psource='poster']",
"value": "@href"
},
"image_url": {
"css": "li[@data-category='nowplaying']/ul/li/a[@data-psource='poster']/img",
"value": "@src"
}
}
}
以上可以看出,我們從豆瓣的每部電影中獲取了以下信息:
- title:電影名字
- score:電影分數,滿分10分
- star:電影分數,滿分50分
- release:上映日期
- region:地區
- actors:演員
- director:導演
- detail_url:詳細 url
- image_url:電影封面
注意:獲取具體 xpath 比較簡單的方法:通過 chrome 右鍵的
inspect
來復制拿到。
以上配置完畢后,點擊下面的 Dry Run
,應該就會出現以下頁面

最后進行保存。第一個 agent 就創建完畢了。
同時,這個 agent 在運行的過程中會生成以下 events:

創建過濾 agnet
step1_get_douban_playing_movies
把所有正在上映的電影數據從豆瓣上拉取下來並解析好,生成一個個 events。然后我們第二個 agent 就需要從這些 events 里面進行過濾篩選出所有分數大於 7.8
(具體的標准可以自己定) 的電影。相當於 RxJava 的 filter 操作符吧。
同樣創建 agent,選擇為 TriggerAgent
,名字為 step2_pick_high_score_movies
。這是把 Sources 填寫為第一個 agent 的名字,即 step1_get_douban_playing_movies
,表示我要創建的 agent 處理的數據(events)是從 step1_get_douban_playing_movies
來的。
然后重點還是在 Options 中
- keep_event:表示是否把我從
step1_get_douban_playing_movies
這個 agent 收到的 events 原封不動地再傳給下一個 agent(下一個 agent 我們還沒創建),我們設置為 true。因為下一個 agent 我們是用來把數據通過 slack 發送到給我們自己的,那肯定需要第一個 agent 中獲取到的例如電影名字、分數等信息。 - rules:表示我們過濾的規則,可以多個,具體下面說。
- must_match:表示 rules 中我必須要滿足幾個規則,如果是1,則意味着 rules 中所有的規則是或關系(只要滿足 rules 中的1個規則即可);默認不填寫的話是必須要滿足 rules 中所有的規則。,因為我們這里只需要滿足一個分數大於7.8就可以,所以可以不填寫。
最后 Options 的配置如下:
{
"expected_receive_period_in_days": "2",
"keep_event": "true",
"rules": [
{
"type": "field>=value",
"value": "7.8",
"path": "$.score"
}
],
"message": "Looks like your pattern matched in '{{value}}'!"
}
如上,在 rules 中添加一個規則,type 表示匹配規則,field>=value
:
- field: 通過下面 path 從 events 匹配出來的數據,這里是
$.score
,所以表示的是電影的分數; - value:表示下面 json 的
value
字段的值,這里為7.8
。
通過簡單的表達式 field>=value
來設定匹配規則:電影分數 >= 7.8分。
至此,第二個 agent 創建完畢。
你同樣可以通過下面的 Dry Run
來進行測試,測試時因為有 Sources
,需要你構造一些假數據作為輸入來運行。
創建去重 agnet
step2_pick_high_score_movies
用來把 step1_get_douban_playing_movies
中從豆瓣官網獲取的電影信息進行高分的過濾(分數>=7.8)。
我們還需要創建一個去重的 agent,來避免重復給我們自己推送高分電影(因為我們現在獲取的頻率是每天進行獲取檢測,但是電影總不可能是每部電影只上映一天吧,第二天獲取的時候肯定有第一天獲取的數據)。
這里大家可能會有個問題,因為我們在配置第一個 agent 的時候,已經把 mode
已經設置為 on_change
了,為什么還是會有重復數據呢?因為這里的電影信息中,有諸如 分數
這類的數據,這些數據是隨時可能會有變化的,雖然是同一個電影,但是分數從 8.1
上升到 8.2
,那 Huginn 也會認為滿足了 on_change
條件,所以會造成重復推送。所以,我們還需要單獨做去重處理。
創建 agent,type 選擇 DeDuplicationAgent
,名字取為 step2_1_deduplication_high_score_movies
,Sources 填寫為上一個 agent 的名字,即 step2_pick_high_score_movies
。
注意:這里 keep_event 設置了90天,因為一旦經過我們這個 agent 去重后,events 假設保留1小時,那下一天我再去獲取所有上映的電影並高分過濾后,因為昨天的數據(events)已經被清空了,所以就沒辦法做比較去重了,所以會導致重復數據。所以這里保存時間應該要>=電影上映的時長,所以這里設置為90天,即3個月左右。
DeDuplicationAgent 的 Options 填寫就比較簡單了

- Property:填寫你要去重依據的字段,我們這里根據電影名字來去重,也就是
title
。 - Lookback:表示去重的時候跟之前的多少條歷史 events 做比較,同一時期一起上映的電影應該不會超過100部,所以設置為100了。
創建 slack 通知的 agent
Huginn 自帶有一個 SlackAgent
,用來發送 slack 消息。

它使用了 incoming-webhooks 來實現消息的發送。
但是為了有更多的可玩性,我們這里選擇,自己創建一個 slack app
,然后通過它的 open api 實現。
因此,我們需要創建一個 PostAgent
。但是在此之前我們先來配置好 Slack 環境。
配置 Slack 環境
安裝 Slack:https://slack.com
- Google Play for Android:https://play.google.com/store/apps/details?id=com.Slack
創建自己的 workspace(單獨創建一個自己私有的,注意不要使用公司、團隊的 workspace),比如我的是 https://wangjie.slack.com
。
在自己私有的 workspace 中創建一個私有的 channel:#huginn-movie

這個 channel 就是用來接收高分電影的數據了,當然你也可以使用 #general
。
然后我們創建一個自己的 app,用來發送電影信息。進入 https://api.slack.com/

點擊 Start Building
,

- App Name:可以隨意填寫
- Development Slack Workspace:選擇你剛剛創建的私有的 workspace
在 Add features and functionality
中點擊 Permissions
進入權限配置。
在 Scope
中添加如下權限:

添加完以上所有權限后,點擊保存,然后重新打開 Permissions
,點擊下面按鈕安裝我們的這個 app 到 slack。


然后,我們就可以使用我們的 token 來訪問 slack 的 open api 了,具體文檔在這里:https://api.slack.com/web。
我們需要的發送消息到 #huginn-movie
channel 的接口文檔:
https://api.slack.com/methods/chat.postMessage
有了 api 文檔,有了 token,一切就好辦了。

由上述文檔,我們可以通過 post 請求,把我們要發送的電影信息封裝到 attachments
參數中執行請求即可。
而且 attachments
參數可以參考文檔 https://api.slack.com/docs/message-attachments 來封裝信息。
Slack 環境一切就緒,接下來,回到 Huginn。
創建 Agent 發送 Slack 消息
創建 PostAgent
(注意,不是 SlackAgent
),取名為 step3_high_score_movies_to_slack_post
。Sources 填寫為 step2_1_deduplication_high_score_movies
,因為這個 agent 需要把去重后的電影信息通過 slack 發送給我們。
最終的 Options 配置如下:
{
"post_url": "{% credential slack_huginn_url_post_message %}",
"expected_receive_period_in_days": "1",
"content_type": "json",
"method": "post",
"payload": {
"channel": "huginn-movie",
"username": "Douban Movie",
"icon_url": "https://img3.doubanio.com/pics/douban-icons/favicon_48x48.png",
"attachments": [
{
"fallback": "Required plain-text summary of the attachment.",
"mrkdwn_in": [
"text",
"pretext"
],
"color": "#36a64f",
"pretext": "Hi~ <@{% credential slack_at_user_id %}>, There is *high score* movie.",
"author_name": "{{director}}",
"author_link": "{{detail_url}}",
"author_icon": "",
"title": "《{{title}}》",
"title_link": "{{detail_url}}",
"text": "*Actors*: {{actors}}",
"fields": [
{
"title": "Score",
"value": "{{score}}",
"short": true
},
{
"title": "Star",
"value": "{{star}}",
"short": true
},
{
"title": "Region",
"value": "{{region}}",
"short": true
},
{
"title": "Release",
"value": "{{release}}",
"short": true
}
],
"image_url": "",
"thumb_url": "{{image_url}}",
"footer": "Slack",
"footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
"ts": "{{\"now\" | date: \"%s\"}}"
}
]
},
"headers": {
"Content-Type": "application/json",
"Authorization": "{% credential slack_huginn_token %}"
},
"emit_events": "false",
"no_merge": "false",
"output_mode": "clean"
}
需要注意的是:
{\% credential slack_huginn_url_post_message %\}
:此類的表達式為 Liquid-interpolated,具體的值配置在Credentials
中,可以理解為全局定義,在Credentials
中配置好key-value
之后,可以在其它地方以諸如{\% credential key \%}
的方式來使用,這里不做過多介紹了。

保存該 Agent,至此,所需的所有的 Agent 都已經創建完畢了。
總結
整個 Scenario 的事件流程圖如下:

Huginn 還支持公開你創建的 Scenario,提供給其它人使用,以上的代碼也已經公開:
http://h.wangjiegulu.com/scenarios/8/export.json
大家可以直接下載使用,不過需要在 Credentials
中配置如下參數:
- slack_huginn_token:你創建的 Slack App 的 OAuth Access Token,具體方式可以參考這里
- slack_at_user_id:你需要 @ 的 slack 用戶 ID,填寫你自己的,拿到你 ID 的方式可以參考這里
- slack_huginn_url_post_message:填寫
https://slack.com/api/chat.postMessage
即可。
除了以上例子,Huginn 還可以完成更多奇思妙想,限制你的只有你的想象力。