最近在做微信訂閱號爬蟲的時候,突然感覺可以搞這樣一個報警系統:如果解析的內容出現了錯誤,通過『瀑布 IM』發送消息給我。
有這樣聰明懂事的爬蟲,絕對省心不少。
功能嘛很簡單,就是爬蟲解析網頁的時候,如果發現解析的內容和期待的內容格式不相符(比如正則沒匹配上),則調用報警接口,預計應該是 pubu.error('extract item failed')
這樣的調用方式。
我們先分析一下接口需要哪些數據,瀑布的文檔裡是這樣描述的:
{ "text": "文本", "attachments": [{ "title": "標題", "description": "描述", "url": "鏈接", "color": "warning|info|primary|error|muted|success" }], "displayUser": { "name": "機器人名稱", "avatarUrl": "頭像地址" } }
大概是需要:消息的內容,附件的標題、描述、鏈接、類型,發送者的名稱、頭像。
於是我們很快可以寫出一個報警函數:
function sendPubuMessage(type, sender, title, description, url) { const attachment = { title: title, description: description, url: url, color: type, } request.post('https://hooks.pubu.im/services/xxxxxxx', { json: { text: moment().format('GGGG-MM-DD HH:mm'), attachments: [attachment], displayUser: { name: sender, }, }, }, (err, response) => { if (err || response.statusCode !== 200) { console.error('網絡異常!提交瀑布失敗:' + err) // eslint-disable-line } }) }
然後調用方法如下:
sendPubuMessage('error', '微信爬蟲', 'Extract key failed!', 'I do xxxx xxxx and failed', 'http://my.url/for/this/error')
測試一下,木問題:
然而,現在這個調用方法用起來還是不方便:
每次需要手動輸入消息的級別,比如 error
這種,容易手誤
每次需要手動輸入發送者的機器人名字,不易管理
消息發送的頻道接口寫死在了函數裡,不方便定制
於是乎,需要把 sender
和 type
分離出來。
先用 buildType
來組裝 type
,生成各種消息類型,主要是定義 color
屬性,用於在消息中顯示不同級別的顏色:
function buildType(color) { return { color: color, } } const info = buildType('info') const warning = buildType('warning') const error = buildType('error') const success = buildType('success')
再用 buildSender
來組裝 sender
,生成各種發送者,主要是定義 name
和 url
屬性,即發送者的名稱和需要發送的頻道地址:
function buildSender(name, url) { return { name: name, url: url, } } const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111') const sogou = buildSender('搜狗爬蟲', 'https://hooks.pubu.im/services/222222222') const log = buildSender('系統日志', 'https://hooks.pubu.im/services/333333333')
最後函數稍作調整,變成了這樣:
function sendPubuMessage(type, sender, title, description, url) { const attachment = { title: title, description: description, url: url, color: type.color, } request.post(sender.url, { json: { text: moment().format('GGGG-MM-DD HH:mm'), attachments: [attachment], displayUser: { name: sender.name, avatarUrl: sender.avatar, }, }, }, (err, response) => { if (err || response.statusCode !== 200) { console.error('網絡異常!提交瀑布失敗:' + err) // eslint-disable-line } }) }
調用的地方成了這樣:
// 由 微信爬蟲 發送一條 error 消息 sendPubuMessage(error, wechat, 'failed!', 'I xx and failed', 'http://my.url/for/this/error') // 由 搜狗爬蟲 發送給一條 warning 消息 sendPubuMessage(warn, sogou, 'failed!', 'I xx and failed', 'http://my.url/for/this/error')
封裝接口
函數基本是確定了,但是這樣的函數外部對象需要使用的時候,只能:
const pubu = require('./lib/pubu') pubu.sendPubuMessage(pubu.error, pubu.wechat, 'failed!')
這真是太丑了。我希望能夠這樣調用:
const pubu = require('./lib/pubu') pubu.wechat.error('failed!')
我們需要改造!我們希望能直接通過 sender
對象發送消息,所以需要改寫一下 sender
的 builder
函數:
function buildSender(name, url) { return { name: name, url: url, info: function(title, description, url) { sendPubuMessage(info, this, title, description, url) }, warn: function(title, description, url) { sendPubuMessage(warning, this, title, description, url) }, error: function(title, description, url) { sendPubuMessage(error, this, title, description, url) }, success: function(title, description, url) { sendPubuMessage(success, this, title, description, url) }, } } const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111')
修改過後我們就可以這樣調用啦:
wechat.info('info test') wechat.warn('warn test') wechat.error('error test') wechat.success('success test')
測試結果看起來還不錯:
然而,這部分代碼看得我總是慌得很:
function buildSender(name, url) { return { name: name, url: url, info: function(title, description, url) { sendPubuMessage(info, this, title, description, url) }, warn: function(title, description, url) { sendPubuMessage(warning, this, title, description, url) }, error: function(title, description, url) { sendPubuMessage(error, this, title, description, url) }, success: function(title, description, url) { sendPubuMessage(success, this, title, description, url) }, } }
為什麼這個世界上充滿了重復。
為什麼?為什麼?為什麼?為什麼?
是的,重復了四遍。
是的,上面那句是個雙關。
仔細想想,其實我們要做的就是封裝 sendPubuMessage
以便外部調用。這個函數接受三類參數:
type,消息類型,不同類型的消息有不用的顏色區分
sender,發送者,包括發送者名稱和發送到的頻道地址
message,後面三個參數都是消息的內容,統一歸為一類,title
是必須的, description
和 url
是可選的
每傳入一個參數,其實這個函數就完善了一點點。
比如我傳入了 error
,那後面不管傳入什麼,這都是個發送 error
消息的函數。
比如我再傳入了 wechat
,那後面不管傳入什麼消息,這都是個發送微信爬蟲的 error
消息的函數。
感覺有點眼熟,這不是柯裡化的思路嗎?不妨用柯裡化函數試試。
找了一個 JS 的柯裡化的庫:curry,柯裡化後的調用是這樣的:
const curry = require('curry') const curreidSend = curry(sendPubuMessage) function buildSender(name, url) { const sender = { name: name, url: url, } sender.info = curreidSend(info)(sender) sender.warn = curreidSend(warning)(sender) sender.error = curreidSend(error)(sender) sender.success = curreidSend(success)(sender) return sender } const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111')
由於不再是 function
了,所以 this
失效,只能通過這種『聲明外賦值』的方式來實現。(JS 學藝不精,應該有更好的方法,歡迎指點)
看起來似乎是簡潔了一些,然而,在測試的時候發現,wechat.info
這個函數如果接受了少於3個參數就不會執行了。
比如這樣的時候:
wechat.info('info test')
仔細一想,柯裡化之後的函數應該是期待五個參數輸入,而此時我才輸入了三個參數: type
、sender
、title
。講道理的話,此時的執行結果,應該是一個期待輸入兩個參數的參數。我們打印一下,果然:
console.log(wechat.info('info test').length) // 2
這就有點辣手了啊,柯裡化之後把我本來的可選參數給搞沒了,而大部分情況下其實我只傳個 title
就結束了,剩下來兩個參數是不會傳的。
換句話說為了省幾個字母的內部實現,現在每次外部調用都需要傳入兩個額外的參數。
你知道什麼時候我會覺得我是個天才嗎?
當我發現我以前原來是一個傻逼的時候。
整理一下思緒,柯裡化顯然需要把所有的參數都假設成需要輸入的參數,然後再做局部應用,要不然一個 ()
人家怎麼知道是該直接調用返回運算結果,還是該局部調用返回一個新的函數呢?
那我可以在柯裡化的結果外面包一層啊,根據傳入參數的數量來決定生成的柯裡化的結果是該有幾個入參,比如這樣:
const buildCurreidSend = (type) => { return () => { const args = [].slice.call(arguments) const curriedSend = curry.to(2 + args.length, sendPubuMessage) return curriedSend(type)(sender) } }
然而這方法並沒有調用,雖然通過 arguments
知道了參數的數量,但是並沒有將參數傳入並調用函數。
如果要調用,我需要自己對這個生成的函數傳入參數,而不是像現在這樣直接返回一個函數。
『傳入參數』之後才能『生成新函數』,『生成新函數』之後需要傳入『傳入的參數』來調用函數,那我為什麼不直接把參數組裝一下給這個函數呢?
想到這裡的時候我的內心是崩潰的。
但是也是光明的:是啊,為什麼我一定要柯裡化呢?
這種參數不確定的場景,其實並不適合柯裡化,個人感覺。
一開始的思路是:需要局部調用函數,生成一個新的函數供外部調用。
其實也就是:提供部分參數,然後將參數補全並調用。那我為何不用 apply
方法呢,將外部傳入的參數把持住,然後在前面插上 type
和 sender
,然後作為參數傳給那個函數就可以了。
而且由於我可以自己組裝函數,this
指針也重新起了作用:
function buildSendMessage(type) { return () => { const args = [].slice.call(arguments) args.unshift(type, this) sendPubuMessage.apply(this, args) } } function buildSender(name, url) { const sender = { name: name, url: url, info: buildSendMessage(info), warning: buildSendMessage(warning), error: buildSendMessage(error), success: buildSendMessage(success), } return sender }
最後的完整代碼是這樣的:
const request = require('request') const moment = require('moment') // ---------------------------------------------------------------------------- // 消息類型 // ---------------------------------------------------------------------------- function buildType(color) { return { color: color, } } const info = buildType('info') const warning = buildType('warning') const error = buildType('error') const success = buildType('success') // ---------------------------------------------------------------------------- // 發消息的函數定義 // ---------------------------------------------------------------------------- function sendPubuMessage(type, sender, title, description, url) { const attachment = { title: title, description: (typeof description === 'object') ? JSON.stringify(description) : description, url: url, color: type.color, } request.post(sender.url, { json: { text: moment().format('GGGG-MM-DD HH:mm'), attachments: [attachment], displayUser: { name: sender.name, avatarUrl: sender.avatar, }, }, }, (err, response) => { if (err || response.statusCode !== 200) { console.error('網絡異常!提交瀑布失敗' + err) // eslint-disable-line } }) } // ---------------------------------------------------------------------------- // 消息的發送者 // ---------------------------------------------------------------------------- function buildSendMessage(type) { return () => { const args = [].slice.call(arguments) args.unshift(type, this) sendPubuMessage.apply(this, args) } } function buildSender(name, url) { const sender = { name: name, url: url, info: buildSendMessage(info), warning: buildSendMessage(warning), error: buildSendMessage(error), success: buildSendMessage(success), } return sender } module.exports.wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111') module.exports.sogou = buildSender('搜狗爬蟲', 'https://hooks.pubu.im/services/111111111111111') module.exports.log = buildSender('系統日志', 'https://hooks.pubu.im/services/222222222222222')
終於可以這樣調用接口了:
pubu.log.warning('Test Warning') pubu.log.error('Test Error') pubu.log.success('Test Success')
小結
經過一通蝦折騰,花了半天的時間。
JS 還是有待深入學習,感覺一旦遇到一些稍微深入一點的話題,自己的知識儲備就顯得乏力了。比如 this
比如 apply
比如 call
比如 bind
各種。
回想起來,學習 Swift 的過程中了解過一段時間的 FRP 並且整理了一些文章。雖然粗淺地看了一些理論知識,但是並沒有什麼真槍實彈的經驗。今天終於在項目裡實驗了一次,雖然結果以失敗告終,但是內心是
崩潰的。
相關文章:
Functional Reactive Programming in Swift
Swift 中的柯裡化