基于Casperjs的网站内容采集器设计

摘要:

本文记录了使用 Casperjs 进行网站内容采集的设计与实现过程,主要是对大概一个星期左右的边学边编程进行记录,总结其中设计的知识点以及设计思路,以备未来升级回顾使用。

需求描述

现在国内有若干产品可以实现网站内容的采集,比如火车头,章鱼等,通过试用发现免费版的功能极其有限(比如火车头会限制导出的方式,免费版貌似只能连接 access 导出,章鱼的导出是需要积分的,虽然初始注册给了积分够导出大概2万条数据,但是这个限制对于使用十分不方便),并且都只有 Windows 平台的客户端。所以对于一个几乎完全摆脱 Windows 的人来说,这些工具统统难以入法眼。

另一方面,这些采集器大多数是对页面进行解析,很难进行更深入的扩展,比如动态获取 POSTResponse 这种需求很难完成。 所以我毅然选择自己写一个爬虫来完成这个工作。

下面我简要描述一下我的需求,这里我们不特定某个网站,但是我需要采集的内容有一定的特性(目前是定制,未来考虑将程序做的更加通用)。

某视频资源网站 A,其网站基于不同类别的视频为分页结构,比如电影页面,每一页有若干个电影的简要信息,其中包含链接跳转到电影详情页面,在详情页面又包含有播放链接,同样该播放链接也是新页面,用来加载视频资源。其中视频资源的加载比较复杂,是通过内嵌 JS 来发送请求动态获取的资源地址。

我们的目标就是将所有的视频信息及播放地址进行格式化输出,一般为 JSON 格式,然后将这个数据动态上传至数据库或其他存储。

依赖技术简介

SlimerJS & PhantomJS

对于爬虫技术,可能现在主流的技术是使用 PythonJavascript 来做,Python 因为有很多开源的程序,但是发现对于上面的需求,可能还是使用 Javascript 来做比较合适,因为视频资源的获取最简单的方法还是模拟浏览器访问来获取。

一开始尝试 PhantomJS,因为这个是基于彻底的 Headless Browser,也就是说用 PhantomJS 模拟访问页面完全看不到任何界面。但是通过测试发现,难以获取 POSTResponse 的具体内容,所以后来选择了 SlimerJS,它使用了 Gecko,这样在运行的时候会弹出一个小的窗口来运行,经过测试可以完美获取,所以最终选择使用 SlimerJS 作为爬虫的网页加载引擎。

CasperJS

为了让程序更加通用,我选择使用 CasperJS 来编写爬虫,实际上 CapserJS 是进一步封装,其目的就是简化自动化测试和网站抓取的代码。所以用这个来写 JS 然后使用 SlimerJS 作为引擎,应该算是一个比较好的选择,以后万一 PhantomJS 更加强大了,可以无缝切换。

1
casperjs xxx.js --engine=slimerjs > result.txt

Firebase

Firebase 的实时数据库目前看来是最好的选择,免费有1G 存储空间,作为自用足够了,而且 SDK 完善,还有 Restful API,可以说非常容易集成到任何的代码中。(不过他没有 Java Client 的 SDK)

Tor Proxy

现在大部分网站都有访问限制策略,也就是说如果你的爬虫大量的访问资源,很容易被检测并且被封锁。这是用 Tor 可以给予一定的帮助,至少自己的 IP 地址不会被封锁,通过测试方法,仍然会有访问一段时间会短暂的无法访问,但是总体来讲比直接用自己的 IP 要好很多。

具体实现过程

概要

主要是使用递归方法,通过设置三个级别和三个独立的 URL 数组来控制递归的 URL,三个级别分别是 集合类(Level 1),详情类(Level 2)和视频资源类(Level 3)。Level 1,表示该 URL 的页面是一个集合,包含若干的详情链接;Level 2,表示这个链接的页面是包含资源具体详情信息的。Level 3,表示该页面会加载视频资源。分成三个级别是因为不同级别页面的处理是不同的

Level1:主要是 HTML 解析,获取链接
Level2:主要是 HTML 解析,获取语义信息
Level3:主要是 POST/Response 解析,获取视频资源

程序的逻辑

三个链接数组

  • 集合页面链接 collectionUrl
  • 视频详情链接 initialDetailUrl
  • 视频播放链接 detailChainUrl
  1. 配置参数,集合页面的基础 URL,分页的参数,以及搜索的页码
  2. 开始加载第一个集合页面,设为 level1, 然后采集页面的所有视频详情的 URL,并且加入到 initialDetailUrl 数组中。
  3. 通过判断 initialDetailUrl 不为空,设置 level2,并且开始解析 initialDetailUrl 中的 url,在解析的过程中,获取影片播放的地址,存入 detailChainUrl
  4. 通过判断 detailChainUrl 不为空,设置 level3, 开始获取视频播放详细地址,如果获取成功,那么由于 detailChainUrl 为空,则开始继续分析下一个视频详情地址,直到视频详情地址为空为止,则开始分析下一个集合页面。

如何正确处理多于的请求返回

由于在抓取中个,通过监听 resource.received 来获取资源,但是这个过程中会有其他的请求资源返回,我们如何尽可能的减少这些资源的干扰呢?

首先我们通过资源返回的特征来判断是否是我们需要的资源:

level1: if (resource.url === currentUrl && resource.body !== "") {}
level2: if (resource.url === currentUrl && resource.body !== "") {}
level3: if (!hasGotVideoUrl && JSON.stringify(resource).indexOf(subString) !== -1) {} 这个请求比较特殊,我们希望拿到视频链接后就不再进行任何处理,所以加入一个标志来判断是否已经获取到视频链接。

然后我们还需要一个标志标明我们已经完成了本次链接分析工作已经完成,所以在每次收到资源返回时将var finishTheUrlWork = false;,这样如果没有得到匹配的资源,就直接结束处理,如果得到匹配的资源,则进入对应的资源处理流程,处理完后需要将 finishTheUrlWork = true;

最后,只有finishTheUrlWork = true;的时候,才会进行下一个链接的处理

程序的配置逻辑

为了应对不同任务:

  1. 更新采集
  2. 历史采集

我们设定了如下的配置来灵活定制采集的内容

  1. 采集页码范围:表示我们只采集范围内的页码资源
  2. 是否设定起始视频详情链接:如果我们需要继续上一次采集,这样可能一个页面上的部分视频资源已经采集过来,为了不重复,通过设定一个起始链接并且,资源本身是顺序的,所以可以通过这个标志链接来决定是否开始进行采集。
  3. 起始视频详情链接:上一个配置为 true 的时候,需要配置这个。
  4. 结束视频链接:如果是更新的视频资源,可能我们只需采集一个页面上的最前面的若干个视频资源,所以我们设定一个终止链接,也就是上一次采集的第一个链接,然后一旦采集遇到该链接就会停止进行采集。

关于应对网站频繁请求资源屏蔽的解决方法

  1. 在每次进行页面采集的时候进行等待若干分钟来模拟个人操作行为
  2. 如果请求失败,将会无法进行递归,我们通过判断链接数组是否还有数据来判断是否是异常退出了,如果是异常退出,等待若干分钟,然后再进行重试。

Javascript 知识点备忘

HTML 字符串 DOM 查找

最开始我是用正则表达式匹配,但是发现这种匹配放到一个内容很多 HTML 页面中时,很难设计唯一匹配规则,这样一旦匹配失败将无法获取信息。后来发现既然 HTML 是一个结构化的页面,那么我还是通过 DOM 来定位信息更加方便也更加准确。一些介绍如何操作:

1
2
3
4
5
6
7
var htmlString = resource.body;
// parse the html string to DOM
var html = document.createElement('html');
html.innerHTML = htmlString;
// get the specific DOM by CSS selector
// this element is contain 25 movie DOM
var detailDom = html.querySelector("div.className ul");

基本思路就是将 String 转化为 Element,然后再用 ElementquerySelector 来定位 DOM,这个 selector 就是 CSS 标准的选择器,大部分情况可以准确定位到目标资源。

另外还可以定位一组 DOM,然后再循环遍历

1
2
3
4
5
var anchors = detailDom.querySelectorAll("div.className a");
var href;
for (var i = 0; i < anchors.length; ++i) {
//.....
}

Casper 定时等待

1
2
3
4
5
6
7
8
9
10
11
if(detailChainUrls.length===0 && initialDetailUrls.length===0 && collectionUrls.length===0){
console.log("finish the casper right");
}else{
//wait for several time
console.log("not right, need to wait and start again");
this.wait(config.waitingAfterRequestNumber, function() {
//try to continue
console.log("try to continue");
spider(currentUrl);
});
}

程序使用手册

一般情况下,只需要修改

config.pageStart = 12;
config.pageEnd = 12;

然后如果不是从头开始,或者一直到结尾,可以通过设置

config.startDetailUrl = "xxxx.html"
config.enableStartCheck = true
config.stopDetailUrl  = "xxxxx"

如果是从头开始,

config.enableStartCheck = false

总结

通过这个程序,学习了一些知识,包括 Javascript,CasperJS,SlimerJS 等等,还包括代理 Tor 的知识。最后列出一些参考资料以备回顾:

How to prevent getting blacklisted while scraping
use both Tor and Privoxy with Mac OSX
Tor-ifying CasperJS