使用 Kotlin DSL 编写网络爬虫

这篇具有很好参考价值的文章主要介绍了使用 Kotlin DSL 编写网络爬虫。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本博文将会通过一个网络爬虫的例子,向你介绍 Kotlin 的基本用法和其简洁有力的 DSL。

关于DSL

按照维基百科的说法,DSL(domain-specific language) 是一种专注于某一特定应用领域的计算机语言。和我们常用的通用目的型语言(类如 C,Java,Python 等)相反,DSL 并不承诺可用来解决一切可计算性问题。DSL 设计者聚焦于某一特定的场景,通过对 DSL 的精心设计,让使用者在这一场景下能够用该 DSL 简洁高效地表达出自己的想法。例如在数据库领域,SQL 就是一种被用作“查询”的 DSL;在 Web 开发领域,用 HTML 这种 DSL 来描述一张网页的布局结构。而本文介绍的 Kotlin DSL,它是 Kotlin 提供的一种创建 DSL 的能力。我们可以很容易借助该能力创建我们自己的 DSL,例如,Jetpack Compose,Gradle’s Kotlin DSL。

Kotlin DSL

Kotlin DSL 的能力主要来自于 Kotlin 的如下几个语法特性:

  • Lambda表达式,包括
    • 高阶函数
    • 函数的最后一个参数是函数时,可以将函数提取到括号的外面
    • 单参数函数用 it 作为参数的默认名字,可不用声明
  • 操作符重载
  • 中缀符
  • 扩展函数

快速开始

我们首先设计爬虫程序的 API,即 DSL 的语法。以爬取本博客站点的全部博文为例,我们希望爬虫程序完成后,使用者可以这么去调用:

val spider = Spider("https://www.cnblogs.com/dongkuo") {
    html {
        // 文章详情页
        follow(".postTitle2:eq(0)") {
            val article = htmlExtract<Article> {
                it.url = this@follow.request.url.toString()
                it.title = css("#cb_post_title_url")?.text()
            }
            // 下载文章
            download("./blogs/${article.title}.html")
        }
        // 下一页
        follow("#nav_next_page a")
        follow("#homepage_bottom_pager a:containsOwn(下一页)")
    }
}
spider.start()

data class Article(var url: String? = null, var title: String? = null)

以上代码的大致逻辑是:首先通过调用 Spider 构造方法创建一只爬虫,并指定一个初始待爬取的 url,然后启动。通过调用 html 方法或 htmlExtract 方法,可将请求的响应体解析成 html 文档,接着可以调用 follow 方法“跟随”某些 html 标签的链接(继续爬取这些链接),也可以调用 download 方法下载响应内容到文件中。

下面按各个类去介绍如何实现上述 DSL。

Spider 类

Spider 类代表爬虫,调用其构造函数时可以指定初始的 url 和爬虫的配置信息;Spider 构造函数的最后一个参数是一个函数,用于处理请求初始 url 的响应或作为提交 url 时未指定 handler 的缺省 handler。其接收者,即该函数作用域内的 this 为 Response 对象。利用函数的最后一个参数是函数时的便利写法,我们可以把该函数的函数体提到参数括号的外面。因此,原本的 Spider("https://www.cnblogs.com/dongkuo", defaultHandler = {}) 变为 Spider("https://www.cnblogs.com/dongkuo"){}

Spider 类提供 addUrls 方法,用于向爬虫提交需要爬取的网页:

class Spider(
    vararg startUrls: String,
    private val options: Options = Options(),
    private val defaultHandler: Handler<Response>
) {
    
     private val taskChannel: Channel<Task> = Channel(Channel.UNLIMITED)
    
    suspend fun addUrls(vararg urls: String, handler: Handler<Response> = defaultHandler) {
    	urls.forEach {
      	  log.debug("add url: $it")
      	  taskChannel.send(Task(it, handler))
    	}
    }
}

typealias Handler<T> = suspend (T).() -> Unit
typealias ExtraHandler<T, E> = suspend (T).(E) -> Unit
data class Task(val url: String, val handler: Handler<Response>)

Spider 的 start 方法会创建若干 Fetcher 去爬取网页,此过程用协程执行:

@OptIn(ExperimentalCoroutinesApi::class)
fun start(stopAfterFinishing: Boolean = true) {
    updateState(State.NEW, State.RUNNING) {
        // launch fetcher
        val fetchers = List(options.fetcherNumber) { Fetcher(this) }
        for (fetcher in fetchers) {
            launch {
                fetcher.start()
            }
        }
        // wait all fetcher idle and task channel is empty
        runBlocking {
            var allIdleCount = 0
            while (true) {
                val isAllIdle = fetchers.all { it.isIdle }
                if (isAllIdle && taskChannel.isEmpty) {
                    allIdleCount++
                } else {
                    allIdleCount = 0
                }
                if (allIdleCount == 2) {
                    fetchers.forEach { it.stop() }
                    return@runBlocking
                }
                delay(1000)
            }
        }
    }
}

Fetcher 类

Fetcher 类用于从 channel 中取出请求任务并执行,最后调用 handler 方法处理请求响应:

private class Fetcher(val spider: Spider) {
    var isIdle = true
        private set

    private var job: Job? = null

    suspend fun start() = withContext(spider.coroutineContext) {
        job = launch(CoroutineName("${spider.options.spiderName}-fetcher")) {
            while (true) {
                isIdle = true
                val task = spider.taskChannel.receive()
                isIdle = false
                spider.log.debug("fetch ${task.url}")
                val httpStatement = spider.httpClient.prepareGet(task.url) {
                    timeout {
                        connectTimeoutMillis = spider.options.connectTimeoutMillis
                        requestTimeoutMillis = spider.options.requestTimeoutMillis
                        socketTimeoutMillis = spider.options.socketTimeoutMillis
                    }
                }
                httpStatement.execute {
                    val request = Request(URI.create(task.url).toURL(), "GET")
                    task.handler.invoke(Response(request, it, spider))
                }
            }
        }
    }

    fun stop() {
        job?.cancel()
    }
}

Response 类

Response 类代表请求的响应,它有获取响应码、响应头的方法。

fun statusCode(): Int {
    TODO()
}

fun header(name: String): String? {
    TODO()
}
// ...

除此之外,我们还需要一些解析响应体的方法来方便使用者处理响应。因此提供

  • text 方法:将响应体编码成字符串;
  • html 方法:将响应体解析成 html 文档(见 Document 类);
  • htmlExtra 方法:将响应体解析成 html 文档,并自动创建通过泛型指定的数据类返回。它的末尾参数是一个函数,其作用域内,it 指向自动创建(通过反射创建)的数据对象,this 指向 Document 对象。
  • stream 方法:获取响应体的输入流;
  • download 方法:保存响应体数据到文件;

具体实现代码可在文末给出的仓库中找到。

Selectable 与 Extractable 接口

Selectable 接口表示“可选择”元素的,定义了若干选择元素的方法:

interface Selectable {
    fun css(selector: String): Element?
    fun cssAll(selector: String): List<Element>
    fun xpath(selector: String): Element?
    fun xpathAll(selector: String): List<Element>
    fun firstChild(): Element?
    fun lastChild(): Element?
    fun nthChild(index: Int): Element?
    fun children(): List<Element>
}

Extractable 接口表示“可提取”信息的,定义了若干提取信息的方法:

interface Extractable {
    fun tag(): String?
    fun html(onlyInner: Boolean = false): String?
    fun text(onlyOwn: Boolean = false): String?
    fun attribute(name: String, absoluteUrl: Boolean = true): String
}

为了方便使用,还定义一个函数类型的别名 Extractor

typealias Extractor = (Extractable?) -> String?

并提供一些便利地创建 Extractor 函数的函数(高阶函数):

fun tag(): Extractor = { it?.tag() }
fun html(): Extractor = { it?.html() }
fun attribute(name: String): Extractor = { it?.attribute(name) }
fun text(): Extractor = { it?.text() }

Document 类

Document 类代表 HTML 文档。它实现了 Selectable 接口:

class Document(
    html: String,
    baseUrl: String,
    private val spider: Spider
) : Selectable {
    fun title(): String {
        TODO()
    }

    override fun css(selector: String): Element? {
        TODO()
    }

    // ...
}

除此以外,Document 类还提供 follow 方法,便于使用者能快速跟随页面中的链接:

suspend fun follow(
    css: String? = null,
    xpath: String? = null,
    extractor: Extractor = attribute("href"),
    handler: Handler<Response>? = null
) {
    if (css != null) {
        follow(cssAll(css), extractor, handler)
    }
    if (xpath != null) {
        follow(xpathAll(xpath), extractor, handler)
    }
}

suspend fun follow(
    extractableList: List<Extractable>,
    extractor: Extractor = attribute("href"),
    responseHandler: Handler<Response>? = null
) {
    extractableList.forEach { follow(it, extractor, responseHandler) }
}

suspend fun follow(
    extractable: Extractable?,
    extractor: Extractor = attribute("href"),
    handler: Handler<Response>? = null
) {
    val url = extractable.let(extractor) ?: return
    if (handler == null) {
        spider.addUrls(url)
    } else {
        spider.addUrls(url, handler = handler)
    }
}

Element 类

Element 类代表 DOM 中的元素。它除了具有和 Document 类一样的读取 DOM 的方法外(实现 Selectable接口),还实现了Extractable 接口:

class Element(private val innerElement: InnerElement) : Selectable, Extractable {
    // ...
}

总结

本文试图通过一个简单的爬虫程序向读者展示 Kotlin 以及 其 DSL 的魅力。作为一门 JVM 语言,Kotlin 在遵守 JVM 平台规范的基础上,吸取了众多优秀的语法特性,值得大家尝试。

本文完整代码可在 kspider 仓库中找到。文章来源地址https://www.toymoban.com/news/detail-843561.html

到了这里,关于使用 Kotlin DSL 编写网络爬虫的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Gradle Kotlin DSL 打包带上依赖

    Gradle version Gradle 8.2.1 build.gradle.kts 参考 https://blog.csdn.net/setlilei/article/details/123173339

    2024年02月15日
    浏览(37)
  • 切底掌握Android中的Kotlin DSL

    在这篇文章中,我们将学习如何在您的 Android 项目中编写 Kotlin DSL。 这个文章会很长,所以花点时间,让我们一起来写你的 DSL。我们将讨论以下主题, 什么是简单英语中的 DSL? 您使用任何 DSL 吗? 为什么我们使用 DSL? 我们如何编写自己的 DSL 基本示例说明。 那么让我们开

    2024年02月10日
    浏览(47)
  • 【28】Kotlin语法进阶——使用协程编写高效的并发程序

    提示:此文章仅作为本人记录日常学习使用,若有存在错误或者不严谨得地方欢迎指正。 协程是Kotlin语言中很有代表性的一种并发设计模式,用于简化异步执行的代码。 协程和线程有点类似,可以简单地将它理解成一种轻量级的线程 。我们前面学习的线程是属于重量级的,

    2024年02月03日
    浏览(53)
  • python 爬虫热身篇 使用 requests 库通过 HTTP 读取网络数据,使用 pandas 读取网页上的表格,使用 Selenium 模拟浏览器操作

    在过去,收集数据是一项繁琐的工作,有时非常昂贵。机器学习项目不能没有数据。幸运的是,我们现在在网络上有很多数据可供我们使用。我们可以从 Web 复制数据来创建数据集。我们可以手动下载文件并将其保存到磁盘。但是,我们可以通过自动化数据收集来更有效地做

    2023年04月08日
    浏览(74)
  • kotlin 编写一个简单的天气预报app (七)使用material design

    对之前的天气预报的app进行了优化,原先的天气预报程序逻辑是这样的。 使用text和button组合了一个输入城市,并请求openweathermap对应数据,并显示的功能。 但是搜索城市的时候,可能会有错误,比如大小写,比如拼写之类的,所以打算给他升级一下。 目标: 在搜索的时候需

    2024年04月27日
    浏览(46)
  • kotlin 编写一个简单的天气预报app(六)使用recyclerView显示forecast内容

    要使用RecyclerView显示天气预报的内容 先在grandle里添加recyclerView的引用 创建一个RecyclerView控件:在布局文件中,添加一个RecyclerView控件,用于显示天气预报的列表。 这是一个包含三个TextView的LinearLayout布局,用于显示天气相关的数据。每个TextView都有一个唯一的id,可用于在代

    2024年02月13日
    浏览(86)
  • 【Kotlin】DSL 领域特定语言 ( apply 标准库函数分析 | 普通匿名函数 | 扩展匿名函数 | 泛型扩展匿名函数 )

    本章总结 : 读懂 apply 标准库函数 核心是其 block: T.() - Unit 参数 , 这是 泛型扩展匿名函数 ; 泛型扩展匿名函数 T.() - Unit 演变路径 : 普通匿名函数 : () - Unit , 这个函数 参数 和 返回值 都为空 ; 扩展匿名函数 : String.() - Unit , 这个函数 是 为 具体的 String 类型定义的扩展函数 ; 泛型

    2023年04月09日
    浏览(39)
  • 使用Selenium和Java编写爬虫程序

    以下是一个使用Selenium和Java编写的音频爬虫程序,该程序使用了proxy的代码。请注意,这个示例需要在IDE中运行,并且可能需要根据您的系统和需求进行调整。 这个示例代码使用了Selenium的ChromeDriver,并设置了一个用户。它首先访问,然后查找并下载页面上的音频文件。请注

    2024年02月03日
    浏览(52)
  • 使用Selenium模块编写自动化爬虫程序

    使用Selenium模块编写自动化爬虫程序可以实现更复杂的爬取操作,模拟浏览器的行为。以下是关于使用Selenium模块编写自动化爬虫程序的总结: 模拟浏览器行为:Selenium模块可以模拟浏览器的行为,如点击按钮、填写表单、滚动页面等。这使得爬虫可以处理需要交互操作或动态

    2024年02月07日
    浏览(47)
  • 使用 native-image 编译 Kotlin / Java 编写的小工具成 Windows 下 .exe 可执行文件

    使用 Kotlin 写了个小工具,想要发给不懂编程的朋友用,便想到将其先编译成 .exe 文件,搜了下应该是可以使用 Kotlin-native 或者 GraalVM 的 native-image 来编译。这篇文章里使用 IDEA 将程序打包成 jar 再使用 native-image 将 jar 编译成 .exe 文件。 先使用 IDEA 将程序打包成 jar, 再使用

    2024年02月22日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包