使用Kotlin Coroutines简单改造原有的爬虫框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SLFq6OF5O7aH/article/details/81571525

640?wx_fmt=jpeg


NetDiscover 是一款基于 Vert.x、RxJava2 实现的爬虫框架。因为我最近正好在学习 Kotlin 的 Coroutines,在学习过程中尝试改造一下自己的爬虫框架。所以,我为它新添加了一个模块:coroutines 模块。

一. 爬虫框架的基本原理:

对于单个爬虫而言,从消息队列 queue 中获取 request,然后通过下载器 downloader 完成网络请求并获得 html 的内容,通过解析器 parser 解析 html 的内容,然后由多个 pipeline 按照顺序执行操作。其中,downloader、queue、parser、pipeline 这些组件都是接口,爬虫框架里内置了它们很多实现。开发者可以根据自身情况来选择使用或者自己开发全新的实现。

640?wx_fmt=png


下面响应式风格的代码反映了上图爬虫框架的基本原理:

 
  
  1.                    // 从消息队列中取出request

  2.                    final Request request = queue.poll(name);

  3.                    ......

  4.                    // request正在处理

  5.                    downloader.download(request)

  6.                            .map(new Function<Response, Page>() {

  7.                                @Override

  8.                                public Page apply(Response response) throws Exception {

  9.                                    Page page = new Page();

  10.                                    page.setRequest(request);

  11.                                    page.setUrl(request.getUrl());

  12.                                    page.setStatusCode(response.getStatusCode());

  13.                                    if (Utils.isTextType(response.getContentType())) { // text/html

  14.                                        page.setHtml(new Html(response.getContent()));

  15.                                        return page;

  16.                                    } else if (Utils.isApplicationJSONType(response.getContentType())) { // application/json

  17.                                        // 将json字符串转化成Json对象,放入Page的"RESPONSE_JSON"字段。之所以转换成Json对象,是因为Json提供了toObject(),可以转换成具体的class。

  18.                                        page.putField(Constant.RESPONSE_JSON,new Json(new String(response.getContent())));

  19.                                        return page;

  20.                                    } else if (Utils.isApplicationJSONPType(response.getContentType())) { // application/javascript

  21.                                        // 转换成字符串,放入Page的"RESPONSE_JSONP"字段。

  22.                                        // 由于是jsonp,需要开发者在Pipeline中自行去掉字符串前后的内容,这样就可以变成json字符串了。

  23.                                        page.putField(Constant.RESPONSE_JSONP,new String(response.getContent()));

  24.                                        return page;

  25.                                    } else {

  26.                                        page.putField(Constant.RESPONSE_RAW,response.getIs()); // 默认情况,保存InputStream

  27.                                        return page;

  28.                                    }

  29.                                }

  30.                            })

  31.                            .map(new Function<Page, Page>() {

  32.                                @Override

  33.                                public Page apply(Page page) throws Exception {

  34.                                    if (parser != null) {

  35.                                        parser.process(page);

  36.                                    }

  37.                                    return page;

  38.                                }

  39.                            })

  40.                            .map(new Function<Page, Page>() {

  41.                                @Override

  42.                                public Page apply(Page page) throws Exception {

  43.                                    if (Preconditions.isNotBlank(pipelines)) {

  44.                                        pipelines.stream()

  45.                                                .forEach(pipeline -> pipeline.process(page.getResultItems()));

  46.                                    }

  47.                                    return page;

  48.                                }

  49.                            })

  50.                            .observeOn(Schedulers.io())

  51.                            .subscribe(new Consumer<Page>() {

  52.                                @Override

  53.                                public void accept(Page page) throws Exception {

  54.                                    log.info(page.getUrl());

  55.                                    if (request.getAfterRequest()!=null) {

  56.                                        request.getAfterRequest().process(page);

  57.                                    }

  58.                                }

  59.                            }, new Consumer<Throwable>() {

  60.                                @Override

  61.                                public void accept(Throwable throwable) throws Exception {

  62.                                    log.error(throwable.getMessage());

  63.                                }

  64.                            });

其中,Downloader的download方法会返回一个Maybe。

 
  
  1. import com.cv4j.netdiscovery.core.domain.Request;

  2. import com.cv4j.netdiscovery.core.domain.Response;

  3. import io.reactivex.Maybe;

  4. import java.io.Closeable;

  5. /**

  6. * Created by tony on 2017/12/23.

  7. */

  8. public interface Downloader extends Closeable {

  9.    Maybe<Response> download(Request request);

  10. }

正是因为这个 Maybe对象,后续的一系列的链式调用才显得非常自然。比如将Response转换成Page对象,再对Page对象进行解析,Page解析完毕之后做一系列的pipeline操作。

当然,在爬虫框架里还有 SpiderEngine 可以管理 Spider。

二. 使用协程改造

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

由于 Kotlin Coroutines 仍然是试验的API,所以我不打算在爬虫框架原有的 core 模块上进行改动。于是,新增一个模块。

在新模块里,将之前的响应式风格的代码,改造成协程的方式。

Kotlin Coroutines 为各种基于 reactive streams 规范的库提供了工具类。可以在下面的github地址找到。

https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive

我在build.gradle中添加了

 
  
  1.    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.23.0'

  2.    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:0.23.0'

注意,协程的版本号必须跟 Kotlin 的版本要相符和。我所使用的 Kotlin 的版本是1.2.41

下面是修改之后的 Kotlin 代码,原有的各种组件接口依然可以使用。

 
  
  1.                       // 从消息队列中取出request

  2.                       final Request request = queue.poll(name);

  3.                        ......

  4.                        // request正在处理

  5.                        val download = downloader.download(request).await()

  6.                        download?.run {

  7.                            val page = Page()

  8.                            page.request = request

  9.                            page.url = request.url

  10.                            page.statusCode = statusCode

  11.                            if (Utils.isTextType(contentType)) { // text/html

  12.                                page.html = Html(content)

  13.                            } else if (Utils.isApplicationJSONType(contentType)) { // application/json

  14.                                // 将json字符串转化成Json对象,放入Page的"RESPONSE_JSON"字段。之所以转换成Json对象,是因为Json提供了toObject(),可以转换成具体的class。

  15.                                page.putField(Constant.RESPONSE_JSON, Json(String(content)))

  16.                            } else if (Utils.isApplicationJSONPType(contentType)) { // application/javascript

  17.                                // 转换成字符串,放入Page的"RESPONSE_JSONP"字段。

  18.                                // 由于是jsonp,需要开发者在Pipeline中自行去掉字符串前后的内容,这样就可以变成json字符串了。

  19.                                page.putField(Constant.RESPONSE_JSONP, String(content))

  20.                            } else {

  21.                                page.putField(Constant.RESPONSE_RAW, `is`) // 默认情况,保存InputStream

  22.                            }

  23.                            page

  24.                        }?.apply {

  25.                            if (parser != null) {

  26.                                parser!!.process(this)

  27.                            }

  28.                        }?.apply {

  29.                            if (Preconditions.isNotBlank(pipelines)) {

  30.                                pipelines.stream()

  31.                                        .forEach { pipeline -> pipeline.process(resultItems) }

  32.                            }

  33.                        }?.apply {

  34.                            println(url)

  35.                            if (request.afterRequest != null) {

  36.                                request.afterRequest.process(this)

  37.                            }

  38.                        }

其中,download 变量返回了 Maybe的结果。之后, run、apply 等 Kotlin 标准库的扩展函数替代了原先的 RxJava 的 map 操作。

Kotlin 的协程是无阻塞的异步编程方式。上面看似同步的代码,其实是异步实现的。

await() 方法是 Maybe 的扩展函数:

 
  
  1. /**

  2. * Awaits for completion of the maybe without blocking a thread.

  3. * Returns the resulting value, null if no value was produced or throws the corresponding exception if this

  4. * maybe had produced error.

  5. *

  6. * This suspending function is cancellable.

  7. * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function

  8. * immediately resumes with [CancellationException].

  9. */

  10. @Suppress("UNCHECKED_CAST")

  11. public suspend fun <T> MaybeSource<T>.await(): T? = (this as MaybeSource<T?>).awaitOrDefault(null)

由于 await() 方法是 suspend修饰的,所以在上述代码的最外层还得加上一段代码,来创建协程。

 
  
  1.        runBlocking(CommonPool) {

  2.                    ......

  3.        }

到此,完成了最初的改造,感兴趣的同学可以查看我的爬虫框架。 github地址:https://github.com/fengzhizi715/NetDiscovery

三. 小结

随着 Kotlin Coroutines 未来的正式发布,爬虫框架的 coroutines 模块也会考虑合并到 core 模块中。以及随着个人对 Kotlin Coroutines 的进一步认识和理解,也会考虑在更多的地方使用 Coroutines ,例如 Vert.x 和 Kotlin Coroutines 相结合。



关注【Java与Android技术栈】

新增了关键词回复,赶紧来调戏本公众号吧~


更多精彩内容请关注扫码

640?wx_fmt=jpeg



猜你喜欢

转载自blog.csdn.net/SLFq6OF5O7aH/article/details/81571525