开源:可热更新的客户端爬虫框架JsCrawler
Posted 苦逼程序员_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开源:可热更新的客户端爬虫框架JsCrawler相关的知识,希望对你有一定的参考价值。
最近在研究爬虫和客户端抓取网页的相关内容,想要做一个android客户端抓取博客内容的应用,思考了一段时间需求,发现常规的实现方案非常容易出现一些意外问题,再次思考了一段时间,最后做了一个简单易用可热更新的爬虫抓取方案。
相信做过网页抓取的都了解抓取的基本步骤:
1. 选取一个需要抓取的URL
2. 通过网络请求获取完整的html文档
3. 通过jsoup等工具解析html获取需要的内容
4. 重新整理内容,整合成实体类
5. 进行存储或展示
这几个步骤,必须要全部正常执行,才能最终正确的展示抓取的页面内容。
这其中,第4第5个步骤相对比较稳定,而前三个步骤,是比较容易出问题的,一旦有其中一个步骤出错,就会导致应用不可用。
下面是几种较容易出现问题的点:
1. 源数据服务器宕机了,直接导致抓取页面无响应,这种问题基本无解,会导致抓取的第2个步骤出错。
2. 源数据网页小幅度更新了,某些核心内容的布局发生了改变,这会导致抓取的第3个步骤出错,在解析html的过程中无法获得正确的数据。
3. 源数据网站改版,整个访问架构都发生了改变,这种情况有可能会使第1个步骤确定的抓取URL都变得无效,应用变得完全不可用。
在常规的客户端抓取做法下,一旦出现上述问题,开发者只能修改整个客户端,把客户端重新往各大应用商店扔,然后再提示用户需要更新新版本。这整个流程都走下来,需要的时间可能会比较长,而且容易流失用户,毕竟一些用户都不习惯或不喜欢去更新应用(例如我)。
那么有没有办法解决这些问题?
当然有,下面几种方法都能在一定程度上解决这些问题:
1. 通过服务器中转,客户端的请求通过服务端做代理请求,服务端请求源数据网页后整理并返回给客户端(优点: 抓取逻辑放在服务端,随时都可以修改,出了问题修复起来也快,只需要重新部署一次服务器。 缺点: 当用户量增加的时候,服务器的压力也会成倍增加,而且多人访问同一个页面的时候会造成冗余请求,耗费资源。)
2. 服务端定时开启爬虫爬取源数据页面的内容,更新到数据库,所有客户端请求的数据实际上只是获取我们自己服务器的数据。(优点: 节省了第一种方式频繁发生中转请求的资源,即使源数据网页改版或宕机,也不会影响现有客户端的使用。 缺点: 增加了服务端开发的工作量,维护成本几乎翻倍,客户端获取的数据有可能不是最新的,出现数据更新延迟的问题。)
3. 结合第一第二种方法,客户端第一次请求A页面的时候,服务端进行中转请求,并缓存这个页面,在下一次请求同一个页面的时候,服务端可以直接从缓存中返回相应的A页面数据给客户端,不需要再次中转请求。(优点: 解决了冗余请求和服务器中转压力增加的问题。 缺点: 维护服务端的成本以及数据更新延迟依旧存在。)
4. 引入客户端HotFix热修复等框架,抓取内容交给客户端自己处理,出现问题的时候可以很方便的更新抓取逻辑,在用户弱感知的情况下就能完成客户端的更新。(优点: 可快速动态修复,减轻了服务端工作量,降低维护成本,客户端抓取不会对服务器造成压力。 缺点: 热修复学习成本较高,应用复杂度增加。)
把几种方案罗列出来对比后,需求也都变得清晰多了,第1、2、3种方案,在脱离了服务端之后都不能正常工作,这并不符合我的预期目标。相比之下,第4种方案是最合适的,可以脱离服务端运行,也有很强的热更新热修复能力,但热修复框架并不算简单易用。
为了解决这些问题,最后实现了一套更简易的热更新框架。
这套框架工具的核心在于把抓取的1、2、3、4步骤,都抽取到脚本文件中完成了。这个脚本文件,当然就是开发者必备语言javascript了。把核心url,抓取请求,解析数据并重新包装这些步骤都放到脚本文件,一旦遇到源数据结构更新,网站改版等问题,完全不用担心,只需要在服务器更新js脚本和版本号,让客户端下载一份新的抓取脚本即可修复问题。
除了修复问题外,使用这套框架还可以很轻易的对相同类型网站的抓取展示进行拓展。例如,博客类的网站,CSDN的博客,或者技术小黑屋等个人博客,它们都是同一种类型的网站,有几乎相同的展示方式。使用框架可以在后台中更新js脚本动态增加支持展示的博客,也可以做多套爬取脚本,供用户自行选择下载订阅哪些博客的脚本。
JsCrawler基本使用
下面来介绍一下这个框架的使用方式:
- 在application中初始化JsCrawler
public class MyApplication extends Application
@Override
public void onCreate()
super.onCreate();
JsCrawler.initialize(this);
// 获取JsCrawler实例
JsCrawler jsCrawler = JsCrawler.getInstance();
// 设置是否开启使用JQuery
jsCrawler.setJQueryEnabled(true);
@Override
public void onTerminate()
super.onTerminate();
JsCrawler.release();
- 在Activity获取JsCrawler的实例,加载js脚本并调用getBlogList()函数。PS: jsCrawler.callFunction执行js是异步执行不会阻塞UI线程的,而返回的result是在UI线程上执行的,请务必在UI线程中调用callFunction()方法。
public class MainActivity extends Activity
private JsCrawler jsCrawler;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取JsCrawler实例
jsCrawler = JsCrawler.getInstance();
final String js = loadJs();
jsCrawler.callFunction(js, new JsCallback()
@Override
public void onResult(String result)
Log.d(TAG, "onResult: " + result);
// js与java之间的通信只能使用基本类型
// 对于复杂对象,使用json即可
Gson gson = new Gson();
MyModel model = gson.fromJson(result, MyModel.class);
// do something
@Override
public void onError(String errorMessage)
Log.d(TAG, "onError: " + errorMessage);
, "getBlogList");
public String loadJs()
String path = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/Download/crawler.js";
try
File file = new File(path);
InputStream inputStream = new FileInputStream(file);
Scanner scanner = new Scanner(inputStream, "UTF-8");
return scanner.useDelimiter("\\\\A").next();
catch (final IOException e)
e.printStackTrace();
return null;
- 对于需要传递参数的js函数,调用方式如下:
jsCrawler.callFunction("function myFunction(a, b, c, a) return 'result'; ",
new JsCallback()
@Override
public void onResult(String result)
// 处理JavaScript返回结果
@Override
public void onError(String errorMessage)
// 处理JavaScript调用错误信息
, "myFunction", "parameter 1", "parameter 2", 912, 101.3);
- 下面是js脚本的内容,在脚本中定义关键url,执行请求,用JQuery的方式解析内容,整个过程真的是非常顺手。
function getBlogList()
// 定义抓取url
var url = "http://droidyue.com";
// 通过RequestBuilder构造请求
var request = new RequestBuilder()
.url(url).method("GET")
.timeout(10000).build();
// 调用RequestEngine.executeByRequest()传入构造好的request对象
var response = RequestEngine.executeByRequest(request);
// 得到response对象的json字符串,格式如下:
// "code":"200", "message":"OK", "body":"请求获取的内容"
// "code":"404", "message":"NOT FOUND", "body":"请求获取的内容"
// "code":"-1", "message":"Request Exception", "body":""
// 通过eval函数, 转成js对象
response = eval("("+response+")");
// 处理异常的请求返回码
if(response.code != 200)
return "response error";
// 得到正确内容后, 获取相应的body并通过JQuery对内容进行处理
var body = response.body;
var articleEles = $(body).find(".blog-index article");
var articleList = new Array();
// 处理元素数组
$.each(articleEles, function(index, element)
var article = new Object();
element = $(element);
var entry = element.find(".entry-title a").first();
article.title = entry.text();
article.url = url + entry.attr("href");
article.describe = element.find(".entry-content").text().trim();
articleList.push(article);
);
// 把js数组对象转成json字符串返回
return JSON.stringify(articleList);
上面这段脚本的关键点在RequestBuilder构造请求,以及RequestEngine根据构造的请求参数执行请求。看到这里可能有人会发出疑问,既然都可以执行JQuery了,为什么不用ajax直接发起请求,方便简单同时对会JQuery的开发者完全零学习成本,再封装一个请求引擎不是多此一举吗?最初封装的时候,确实有打算直接使用ajax做请求,后来还是重新封装了。为什么?上面这段脚本只使用了一些基本的请求参数,但有些api接口,只设置url设置method是不够的,还需要同时设置一些特殊的header请求头。而不用ajax的原因是因为,ajax对请求头的设置支持不全面,例如User-Agent
,Referer
,Cookie
等,这些请求头都非常重要,像cnBeta的api接口,如果不设置Referer
的话不会返回任何数据。下面列举RequestBuilder的请求设置:
// 创建builder对象,支持链式调用
var builder = new RequestBuilder();
// 设置请求url
builder.url("http://api.kejie.tk");
// 设置method,只支持POST和GET两种请求
builder.method("POST");
builder.method("GET");
// 添加header,有两种方式,addHeader或者setHeaders
builder.addHeader("User-Agent", "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N)")
.addHeader("Referer", "http://api.kejie.tk");
var headers =
"User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N)",
"Referer": "http://api.kejie.tk"
builder.setHeaders(headers);
// 添加Cookie方式跟Header类似,也有两种方式
builder.addCookie("uid", "1170120F8E53899BC88B236FA6A731FC");
var cookies =
"uid": "1170120F8E53899BC88B236FA6A731FC",
"type": "1"
builder.setCookies(cookies);
// 设置data,同上有两种设置方式
// 对于GET请求,data数据会以query方式追加到url
// 对于POST请求,data数据会以form-data方式设置到请求体中
builder.addData("wd", "testData");
var data =
"wd": "testData",
"qid": "59"
builder.setData(data);
// 设置请求body内容字符串
// 有些api是以application/json的方式发起请求
// 并把请求内容以json字符串的方式设置到body中
// 注意,设置了body,上面的form-data形式参数将会失效,两者不能同时使用
// Content-type将自动设为application/json
builder.body('"username":"kejie","pwd":"d8j3kduui461p"');
// 设置请求超时时间,单位毫秒
builder.timeout(10000);
// 生成request对象
var request = builder.build();
生成了request请求对象后,通过js全局对象RequestEngine.executeByRequest(request)发起请求,并获取返回的json数据。返回的内容包含响应的状态码、状态码信息、响应body。在js中通过eval函数把json转成js对象即可自由操作。需要注意的是当请求发生异常例如无网络或者请求超时,状态码将返回-1
,用户可自行处理。
var response = RequestEngine.executeByRequest(request);
// "code":"200", "message":"OK", "body":"请求获取的内容"
// "code":"404", "message":"NOT FOUND", "body":"请求获取的内容"
// "code":"-1", "message":"Request Exception", "body":""
response = eval("("+response+")");
// 使用console.log可以打印字符串到android log中,无法打印js对象,虽然不太方便,但聊胜于无
console.log(response.code);
console.log(response.message);
console.log(response.body);
在框架内封装了两个请求引擎,一个是Jsoup,一个是OkHttp,默认使用Jsoup。如果想要切换成OkHttp只需要调用一句代码。
jsCrawler.setRequestEngine(new OkHttpEngine());
拓展请求引擎
对于内置的请求引擎,可配置的请求参数能够对大多数请求适用,但对于一些更特殊的请求,可能不够用。开发者可以自行进行拓展内置的请求引擎,或继承RequestEngine抽象类实现一个新的完整的请求引擎。下面以拓展JsoupEngine为例,介绍拓展代理请求proxy的方法。
1.继承RequestModel,拓展proxy属性。
public class MyRequestModel extends RequestModel
private String proxy;
public String getProxy()
return proxy;
public void setProxy(String proxy)
this.proxy = proxy;
2.继承JsoupEngine,重写process方法,加入processProxy(),重写jsonToModel方法,把json转成新的MyRequestModel。
public class MyJsoupEngine extends JsoupEngine
protected void processProxy(MyRequestModel model)
if (model.getProxy() != null)
String[] proxy = model.getProxy().split(":");
if (proxy.length > 1)
// connection是jsoup请求的关键对象
connection.proxy(proxy[0], Integer.parseInt(proxy[1]));
@Override
protected void process(RequestModel model)
super.process(model);
processProxy((MyRequestModel) model);
@Override
protected RequestModel jsonToModel(String request)
return gson.fromJson(request, MyRequestModel.class);
3.在脚本文件头部拓展RequestBuilder的proxy()方法以及build()方法,使构造的request中加入proxy字段,即可链式调用proxy并传入代理相关参数。其他调用方式不变。
RequestBuilder.prototype.proxy = function(host)
this.mProxy = host;
return this;
RequestBuilder.prototype.build = function()
var request = new Request(this);
request.proxy = this.mProxy;
return JSON.stringify(request);
function getBlogList()
// your js code ...
var request = new RequestBuilder()
.url(url).method("GET").proxy("127.0.0.1:8088")
.timeout(10000).build();
var response = RequestEngine.executeByRequest(request);
// your js code ...
4.在Application初始化时增加一句代码修改JsCrawler的请求引擎。
public class MyApplication extends Application
@Override
public void onCreate()
super.onCreate();
JsCrawler.initialize(this);
// 获取JsCrawler实例
JsCrawler jsCrawler = JsCrawler.getInstance();
// 设置是否开启使用JQuery
jsCrawler.setJQueryEnabled(true);
// 修改JsCrawler请求引擎
jsCrawler.setRequestEngine(new MyJsoupEngine());
@Override
public void onTerminate()
super.onTerminate();
JsCrawler.release();
github地址: https://github.com/YuanKJ-/JsCrawler
以上是关于开源:可热更新的客户端爬虫框架JsCrawler的主要内容,如果未能解决你的问题,请参考以下文章