开源:可热更新的客户端爬虫框架JsCrawler
来源:互联网 发布:win10笔记本优化教程 编辑:程序博客网 时间:2024/06/11 10:16
最近在研究爬虫和客户端抓取网页的相关内容,想要做一个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();// 设置请求urlbuilder.url("http://api.kejie.tk");// 设置method,只支持POST和GET两种请求builder.method("POST");builder.method("GET");// 添加header,有两种方式,addHeader或者setHeadersbuilder.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/jsonbuilder.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
- 客户端python热更新
- JSPatch 热更新框架的个人评估
- Android热更新框架Nuwa的使用
- android 热更新框架
- Facebook Android客户端热更新
- Android热更新框架NuWa
- Android常用的热更新技术框架调研
- 热更新之Bugly框架的详细集成
- Android热更新框架Tinker无法更新?
- 基于DevTools协议+Chromium headless的客户端爬虫框架
- Erlang的热更新
- HBuilder的热更新
- ulua+PureMVC框架简单热更新使用
- Unity dll 热更新 基础框架
- Unity3D热更新<三> Me-slua 框架
- android 热更新技术框架地址
- 《江湖X》开发笔谈 - 热更新框架
- virtualbox下centos7的桥接模式下的联网配置
- JavaScript常用函数
- 关于前端性能优化
- redis启动
- ps学习笔记(二)
- 开源:可热更新的客户端爬虫框架JsCrawler
- bzoj1910: [Ctsc2002] Award 颁奖典礼
- NP问题证明 8.22
- LCT模板
- 初识java连接Phoenix遇到的坑
- int类型和String类型的相互转换
- 【剑指Offer学习】【所有面试题汇总】
- Windows编程之旅(六)
- 连续属性的决策树算法实现--基于西瓜3.0数据