
最近在做微信訂閱號爬蟲的時候,突然感覺可以搞這樣一個報警系統:如果解析的內容出現了錯誤,通過『瀑布 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 中的柯裡化