Alfred Workflow of BYR BBS Top 10

引子

如何撰写一个Alfred Workflow? 这个不是个问题。

如何撰写一个Node.js爬虫?这也不是个问题。

但如何撰写一个基于Node.js的Alfred Workflow, 这个问题就比较不好回答了,因为谷歌不到(大雾

所以这篇文章,就是为了回答这个问题来的。(此处应有掌声╮( ̄▽ ̄)╭)

使用

开发过程

先写一个node.js爬虫

这个成套的解决方案一堆堆,但我们并不需要真正去爬很多内容,所以也就不涉及并发统计、数据库存储等问题,核心其实总结成就是一句话:发送请求,获取响应,解析响应,组成返回结果展示。
于是经过一番折腾,代码就有了。这里使用的是基于request、cheerio、iconv-lite的解决方案。

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
var request = require('request'),
cheerio = require("cheerio"),
iconv = require('iconv-lite');
var bbsurl = 'https://bbs.byr.cn/rss/topten';
request({
url: bbsurl,
encoding: null
}, function(err, res, body) {
if (err) {
console.log('err happens');
return;
}
var gbkHtml = iconv.decode(body, 'gb2312')
var $ = cheerio.load(gbkHtml, {
decodeEntities: false,
xmlMode: true
});
$('item').each(function(idx, element) {
var el = $(element);
var title = el.find("title").text(),
link_url = el.find("link").text(),
author = el.find("author").text(),
index = idx + 1;
console.log("-----------------");
console.log("title : " + title);
console.log("author : " + author);
});
});

与Alfred Workflow集成

这块是本文的核心。首先我们需要解决如何在alfred下run node.js的问题。庆幸的是有个简单的hack在alfred 2风靡的时候就已经有了:

1
2
3
/usr/local/bin/node <<-'CODE'
require("./index")("{query}");
CODE

然而这并非我们需要的,显然我们并不需要任何参数,因为展示十大是个没法选择的行为。所以我们先选择好触发的指令——bbs。然后选择好模块Script Filter,在里面选择好使用bash或zsh即可。

1
/usr/local/bin/node server.js

因此我们的核心就变成了撰写server.js。根据上文的爬虫,我们需要解决两个问题:

  • 用alfred展示九条结果(其实是10条,然而这个是alfred自身限制,那么就九大吧,第十条通过↓键来解决。)
  • 选中对应十大之后可以回车打开浏览器查看详情(当然必须先有登录的session,否则就先登录再看吧)

第一个问题费了一点儿劲儿,因为这个之前并不了解。经过查询,发觉Alfred使用的是xml形式的结构数据,如果console输出满足这个xml的要求,就能给你show出来。详情可以查看这里。那么就需要修改我们的console.log部分,让最终的结果组成如下的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item uid="top1" arg="https://bbs.byr.cn/article/Friends/1771753" valid="yes" autocomplete="【代征】温油室友诚意征蓝票">
<title>【代征】温油室友诚意征蓝票</title>
<subtitle>lovelysee</subtitle>
<icon>icon.png</icon>
</item>
...省略部分内容
<item uid="top10" arg="https://bbs.byr.cn/article/WorkLife/1056892" valid="yes" autocomplete="马上入职百度inf,如何才能快速升职加薪,达到70万,很缺钱的那">
<title>马上入职百度inf,如何才能快速升职加薪,达到70万,很缺钱的那</title>
<subtitle>xiaozhangxue</subtitle>
<icon>icon.png</icon>
</item>
</items>

这实际就是个拼接string的事情。代码就需要反复测试再提交了。

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
var request = require('request'),
cheerio = require("cheerio"),
iconv = require('iconv-lite');
var bbsurl = 'https://bbs.byr.cn/rss/topten';
var tmp = "<?xml version=\"1.0\"?><items>";
request({
url: bbsurl,
encoding: null
}, function(err, res, body) {
if (err) {
console.log('err happens');
return;
}
var gbkHtml = iconv.decode(body, 'gb2312')
var $ = cheerio.load(gbkHtml, {
decodeEntities: false,
xmlMode: true
});
$('item').each(function(idx, element) {
var el = $(element);
var title = el.find("title").text(),
link_url = el.find("link").text(),
author = el.find("author").text(),
index = idx + 1;
tmp += "<item uid=\"top" + index + "\" arg=\"" + link_url + "\" valid=\"yes\" autocomplete=\"" + title + "\"><title>" + title + "</title><subtitle>" + author + "</subtitle><icon>icon.png</icon></item>";
});
tmp = tmp + "</items>";
console.log(tmp);
});

上述代码同时解决了如何选中后回车能打开URL的问题。在我们的workflow的第一个script filter模块后面加上一个open url的模块,然后在xml组成中将arg参数赋成对应的url即可。Alfred就会根据arg参数来作为url参数去打开,这些可以在Open URL的模块中找到说明。

综上,事情搞定了!剩下就是删掉多余代码,发布Alfred Workflow到github。

结语

其实思路有了之后,用什么语言实现只是个途径不同而已。

希望这个workflow能陪伴大家品悦每天的十大。

做了一点微小的工作,谢谢大家。( ̄▽ ̄)”


2017.1.16 更新

支持其他版面

这个其实从code角度比较好做,真正的代码就几行。

1
2
3
4
5
6
7
var boardName = process.argv[2];
var bbsurl = "";
if (boardName === "") {
bbsurl = "https://bbs.byr.cn/rss/topten";
} else {
bbsurl = "https://bbs.byr.cn/rss/board-" + boardName;
}

这里用node.js内部的process.argv把对应的变量取出来。然后做个简单的判断就可以了。

真正的难点,其实也不能算难点,主要是debug的时候花费了很多时间。第一个是Alfred Workflow的第一步配置那里需要选择加上Arguments的选项,其次在执行脚本的时候,选择以'{query}'的形式加载参数到执行脚本的那一行。执行脚本那一行改为:

1
/usr/local/bin/node server.js '{query}'

其他还需要配置的地方就是Run Behavior那里,考虑到要输入版面名称,不能立即执行脚本,而是让Alfred稍等一会儿。

Debug的坑

这里简单说下debug Alfred Workflow的时候的坑。因为Alfred是以程序输出满足一定的格式才能输出到它的UI上面的,所以一定要保持最后的log里的输出满足这个格式。但是为了调试程序,我一直加了很多console.log(variables)在程序里,导致一直报错,自己却查不出来原因。后来突然意识到自己有多蠢,终于把程序搞定了。

所以如果debug觉得程序输出可以,那就应该立即把一些不必要的输出语句干掉。切记切记。