
NetDiscovery (https://github.com/fengzhizi715/NetDiscovery) 是一款基于 Vert.x、RxJava 2 等框架实现的爬虫框架。
一. 如何创建 DSL
领域特定语言(英语:domain-specific language、DSL)指的是专注于某个应用程序领域的计算机语言。又译作领域专用语言。DSL 能够简化程序设计过程,提高生产效率的技术,同时也让非编程领域专家的人直接描述逻辑成为可能。
NetDiscovery 本身提供了很多功能的 API,然而它的 DSL 模块是为了让使用者拥有更多的选择。
本文讨论的 DSL 是内部 DSL。
内部 DSL:通用语言的特定语法,用内部DSL写成的脚本是一段合法的程序,但是它具有特定的风格,而且仅仅用到了语言的一部分特性,用于处理整个系统一个小方面的问题。
NetDiscovery 的 DSL 主要是结合 Kotlin 带接收者的 Lambda、运算符重载、中缀表达式等 Kotlin 语法特性来编写。
运算符重载、中缀表达式其实很多语言都有,那么我们着重介绍一下带接收者的 Lambda。
在介绍 Kotlin 带接收者的 Lambda 之前,先介绍一下带接收者的函数类型。
带接收者的函数类型,例如 A.(B) -> C,其中 A 是接收者类型,B是参数类型,C是返回类型。
例如:
val sum: Int.(Int) -> Int = {this + it}
sum 是带接收者的函数类型,它在使用上类似于扩展函数。在函数内部,可以使用this指代传给调用的接收者对象。
而带接收者的 Lambda 典型代表是 Kotlin 标准库的扩展函数:with 和 apply。
看一下 apply 的源码:
public inline fun <T> T.apply(block: T.() -> Unit): T {contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)}block()return this}
在 apply 函数中,参数 block 是一个带有接收者的函数类型的参数。
对于 apply 函数的使用,先定义一个 User 对象:
class User{var name:String?=nullvar password: String?=nulloverride fun toString(): String {return "name:$name,password=$password"}}
然后,使用 apply 函数对 User 的属性进行赋值:
fun main(args: Array<String>) {val user = User().apply {name = "Tony"password = "123456"}println(user)}
二. Request 的 DSL 封装
Request 请求包含了爬虫网络请求 Request 的封装,例如:url、userAgent、httpMethod、header、proxy 等等。当然,还包含了请求发生之前、之后做的一些事情,类似于AOP。
那么,我们来看一下使用 DSL 来编写Request:
val request = request {url = "https://www.baidu.com/"httpMethod = HttpMethod.GETspiderName = "tony"header {"111" to "2222""333" to "44444"}extras {"tt" to "qqq"}}Spider.create().name("tony").request(request).pipeline(DebugPipeline()).run()
可以看到,Request 使用 DSL 封装之后,非常简单明了。
下面的代码是具体的实现,主要是使用带接收者的 Lambda、中缀表达式。
package com.cv4j.netdiscovery.dslimport com.cv4j.netdiscovery.core.domain.Requestimport io.vertx.core.http.HttpMethod/*** Created by tony on 2018/9/18.*/class RequestWrapper {private val headerContext = HeaderContext()private val extrasContext = ExtrasContext()var url: String? = nullvar spiderName: String? = nullvar httpMethod: HttpMethod = HttpMethod.GETfun header(init: HeaderContext.() -> Unit) {headerContext.init()}fun extras(init: ExtrasContext.() -> Unit) {extrasContext.init()}internal fun getHeaderContext() = headerContextinternal fun getExtrasContext() = extrasContext}class HeaderContext {private val map: MutableMap<String, String> = mutableMapOf()infix fun String.to(v: String) {map[this] = v}internal fun forEach(action: (k: String, v: String) -> Unit) = map.forEach(action)}class ExtrasContext {private val map: MutableMap<String, Any> = mutableMapOf()infix fun String.to(v: Any) {map[this] = v}internal fun forEach(action: (k: String, v: Any) -> Unit) = map.forEach(action)}fun request(init: RequestWrapper.() -> Unit): Request {val wrap = RequestWrapper()wrap.init()return configRequest(wrap)}private fun configRequest(wrap: RequestWrapper): Request {val request = Request(wrap.url).spiderName(wrap.spiderName).httpMethod(wrap.httpMethod)wrap.getHeaderContext().forEach { k, v ->request.header(k,v)}wrap.getExtrasContext().forEach { k, v ->request.putExtra(k,v)}return request}
三. SpiderEngine的 DSL 封装
SpiderEngine 可以管理引擎中的爬虫,包括爬虫的生命周期。
下面的例子展示了创建一个 SpiderEngine,并往 SpiderEngine 中添加2个爬虫(Spider)。其中一个爬虫是定时地去请求网页。
val spiderEngine = spiderEngine {port = 7070addSpider {name = "tony1"}addSpider {name = "tony2"urls = listOf("https://www.baidu.com")}}val spider = spiderEngine.getSpider("tony1")spider.repeatRequest(10000,"https://github.com/fengzhizi715").initialDelay(10000)spiderEngine.runWithRepeat()
四. Selenium 模块的 DSL 封装
在我之前的文章为爬虫框架构建Selenium模块、DSL模块(Kotlin实现) 中,曾举例使用 NetDiscovery 的 Selenium 模块实现:在京东上搜索我的新书《RxJava 2.x 实战》,并按照销量进行排序,然后获取前十个商品的信息。
这次,使用 DSL 来实现这个功能:
spider {name = "jd"urls = listOf("https://search.jd.com/")downloader = seleniumDownloader {path = "example/chromedriver"browser = Browser.CHROMEaddAction {action = BrowserAction()}addAction {action = SearchAction()}addAction {action = SortAction()}}parser = PriceParser()pipelines = listOf(PricePipeline())}.run()
这里,主要是对 SeleniumDownloader 的封装。Selenium 模块可以适配多款浏览器,而 Downloader 是爬虫框架的下载器组件,实现具体网络请求的功能。这里的 DSL 需要封装所使用的浏览器、浏览器驱动地址、各个模拟浏览器动作(Action)等。
package com.cv4j.netdiscovery.dslimport com.cv4j.netdiscovery.selenium.Browserimport com.cv4j.netdiscovery.selenium.action.SeleniumActionimport com.cv4j.netdiscovery.selenium.downloader.SeleniumDownloaderimport com.cv4j.netdiscovery.selenium.pool.WebDriverPoolimport com.cv4j.netdiscovery.selenium.pool.WebDriverPoolConfig/*** Created by tony on 2018/9/14.*/class SeleniumWrapper {var path: String? = nullvar browser: Browser? = nullprivate val actions = mutableListOf<SeleniumAction>()fun addAction(block: ActionWrapper.() -> Unit) {val actionWrapper = ActionWrapper()actionWrapper.block()actionWrapper?.action?.let {actions.add(it)}}internal fun getActions() = actions}class ActionWrapper{var action:SeleniumAction?=null}fun seleniumDownloader(init: SeleniumWrapper.() -> Unit): SeleniumDownloader {val wrap = SeleniumWrapper()wrap.init()return configSeleniumDownloader(wrap)}private fun configSeleniumDownloader(wrap: SeleniumWrapper): SeleniumDownloader {val config = WebDriverPoolConfig(wrap.path, wrap.browser)WebDriverPool.init(config)return SeleniumDownloader(wrap.getActions())}
除此之外,还对 WebDriver 添加了一些常用的扩展函数。例如:
fun WebDriver.elementByXpath(xpath: String, init: WebElement.() -> Unit) = findElement(By.xpath(xpath)).init()
这样的好处是简化WebElement的操作,例如下面的 BrowserAction :打开浏览器输入关键字
package com.cv4j.netdiscovery.example.jd;import com.cv4j.netdiscovery.selenium.Utils;import com.cv4j.netdiscovery.selenium.action.SeleniumAction;import org.openqa.selenium.WebDriver;import org.openqa.selenium.WebElement;/*** Created by tony on 2018/6/12.*/public class BrowserAction extends SeleniumAction{@Overridepublic SeleniumAction perform(WebDriver driver) {try {String searchText = "RxJava 2.x 实战";String searchInput = "//*[@id=\"keyword\"]";WebElement userInput = Utils.getWebElementByXpath(driver, searchInput);userInput.sendKeys(searchText);Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}return null;}}
而使用了 WebDriver 的扩展函数之后,上述代码等价于下面的代码:
package com.cv4j.netdiscovery.example.jdimport com.cv4j.netdiscovery.dsl.elementByXpathimport com.cv4j.netdiscovery.selenium.action.SeleniumActionimport org.openqa.selenium.WebDriver/*** Created by tony on 2018/9/23.*/class BrowserAction2 : SeleniumAction() {override fun perform(driver: WebDriver): SeleniumAction? {try {val searchText = "RxJava 2.x 实战"val searchInput = "//*[@id=\"keyword\"]"driver.elementByXpath(searchInput){this.sendKeys(searchText)}Thread.sleep(3000)} catch (e: InterruptedException) {e.printStackTrace()}return null}}
五. 总结
爬虫框架github地址:https://github.com/fengzhizi715/NetDiscovery
这里使用的 DSL 很多情况是对链式调用的进一步封装。当然,有人会更喜欢链式调用,也有人会更喜欢 DSL。但是从 API 到 DSL,个人明细更加喜欢 DSL 的风格。
关注【Java与Android技术栈】
更多精彩内容请关注扫码:

本文探讨如何使用 Kotlin 的特性,如带接收者的 Lambda、运算符重载和中缀表达式,来为爬虫框架 NetDiscovery 创建内部 DSL,提升代码的可读性和易用性。通过示例展示了 Request、SpiderEngine 和 Selenium 模块的 DSL 封装,简化了爬虫配置和操作,使得非编程专家也能更好地理解和使用爬虫框架。

被折叠的 条评论
为什么被折叠?



