奇思妙想的女孩. jpg
https://GitHub.com/fengzhizi715/NetDiscovery 是一款基于 Vert.x,RxJava 2 等框架实现的爬虫框架.
一. 如何创建 DSL
领域特定语言 (英语: domain-specific language,DSL) 指的是专注于某个应用程序领域的计算机语言. 又译作领域专用语言. DSL 能够简化程序设计过程, 提高生产效率的技术, 同时也让非编程领域专家的人直接描述逻辑成为可能.
https://GitHub.com/fengzhizi715/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 实现) 中, 曾举例使用 https://GitHub.com/fengzhizi715/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 的风格.
来源: http://www.jianshu.com/p/d1cac558d1d0