美文网首页Kotlin 实战爬虫专题
从API到DSL —— 使用 Kotlin 特性为爬虫框架进一步

从API到DSL —— 使用 Kotlin 特性为爬虫框架进一步

作者: fengzhizi715 | 来源:发表于2018-09-23 23:53 被阅读358次
    奇思妙想的女孩.jpg

    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?=null
        var password: String?=null
    
        override 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.GET
    
                spiderName = "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.dsl
    
    import com.cv4j.netdiscovery.core.domain.Request
    import 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? = null
    
        var spiderName: String? = null
    
        var httpMethod: HttpMethod = HttpMethod.GET
    
        fun header(init: HeaderContext.() -> Unit) {
    
            headerContext.init()
        }
    
        fun extras(init: ExtrasContext.() -> Unit) {
    
            extrasContext.init()
        }
    
        internal fun getHeaderContext() = headerContext
    
        internal 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 = 7070
    
                addSpider {
    
                    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.CHROME
    
                    addAction {
                        action = BrowserAction()
                    }
    
                    addAction {
                        action = SearchAction()
                    }
    
                    addAction {
                        action = SortAction()
                    }
                }
    
                parser = PriceParser()
    
                pipelines = listOf(PricePipeline())
    
            }.run()
    

    这里,主要是对 SeleniumDownloader 的封装。Selenium 模块可以适配多款浏览器,而 Downloader 是爬虫框架的下载器组件,实现具体网络请求的功能。这里的 DSL 需要封装所使用的浏览器、浏览器驱动地址、各个模拟浏览器动作(Action)等。

    package com.cv4j.netdiscovery.dsl
    
    import com.cv4j.netdiscovery.selenium.Browser
    import com.cv4j.netdiscovery.selenium.action.SeleniumAction
    import com.cv4j.netdiscovery.selenium.downloader.SeleniumDownloader
    import com.cv4j.netdiscovery.selenium.pool.WebDriverPool
    import com.cv4j.netdiscovery.selenium.pool.WebDriverPoolConfig
    
    /**
     * Created by tony on 2018/9/14.
     */
    class SeleniumWrapper {
    
        var path: String? = null
    
        var browser: Browser? = null
    
        private 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{
    
        @Override
        public 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.jd
    
    import com.cv4j.netdiscovery.dsl.elementByXpath
    import com.cv4j.netdiscovery.selenium.action.SeleniumAction
    import 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 的风格。

    相关文章

      网友评论

      本文标题:从API到DSL —— 使用 Kotlin 特性为爬虫框架进一步

      本文链接:https://www.haomeiwen.com/subject/qjlfoftx.html