本博文将会通过一个网络爬虫的例子,向你介绍 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 ComposeGradle’s Kotlin DSL

Kotlin DSL

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

快速开始

我们首先设计爬虫程序的 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 仓库中找到。

使用 Kotlin DSL 编写网络爬虫的更多相关文章

  1. python3编写网络爬虫20-pyspider框架的使用

    二.pyspider框架的使用 简介 pyspider是由国人binux 编写的强大的网络爬虫系统 github地址 : https://github.com/binux/pyspider 官方文档 ...

  2. 为编写网络爬虫程序安装Python3.5

    1. 下载Python3.5.1安装包1.1 进入python官网,点击menu->downloads,网址:https://www.python.org/downloads/ 1.2 根据系统 ...

  3. 利用Python编写网络爬虫下载文章

    #coding: utf-8 #title..href... str0='blabla<a title="<论电影的七个元素>——关于我对电影的一些看法以及<后会无期 ...

  4. 吴裕雄--天生自然python学习笔记:编写网络爬虫代码获取指定网站的图片

    我们经常会在网上搜索井下载图片,然而一张一张地下载就太麻烦了,本案例 就是通过网络爬虫技术, 一次性下载该网站所有的图片并保存 . 网站图片下载并保存 将指定网站的 .jpg 和 .png 格式的图片 ...

  5. python3编写网络爬虫18-代理池的维护

    一.代理池的维护 上面我们利用代理可以解决目标网站封IP的问题 在网上有大量公开的免费代理 或者我们也可以购买付费的代理IP但是无论是免费的还是付费的,都不能保证都是可用的 因为可能此IP被其他人使用 ...

  6. 用python语言编写网络爬虫

    本文主要用到python3自带的urllib模块编写轻量级的简单爬虫.至于怎么定位一个网页中具体元素的url可自行百度火狐浏览器的firebug插件或者谷歌浏览器的自带方法. 1.访问一个网址 re= ...

  7. python3编写网络爬虫21-scrapy框架的使用

    一.scrapy框架的使用 前面我们讲了pyspider 它可以快速的完成爬虫的编写 不过pyspider也有一些缺点 例如可配置化不高 异常处理能力有限对于一些反爬虫程度非常强的网站 爬取显得力不从 ...

  8. python3编写网络爬虫19-app爬取

    一.app爬取 前面都是介绍爬取Web网页的内容,随着移动互联网的发展,越来越多的企业并没有提供Web页面端的服务,而是直接开发了App,更多信息都是通过App展示的 App爬取相比Web端更加容易 ...

  9. Python3编写网络爬虫12-数据存储方式五-非关系型数据库存储

    非关系型数据库存储 NoSQL 全称 Not Only SQL 意为非SQL 泛指非关系型数据库.基于键值对 不需要经过SQL层解析 数据之间没有耦合性 性能非常高. 非关系型数据库可细分如下: 键值 ...

  10. 用 Python 编写网络爬虫 笔记

    Chapter I 简介 为什么要写爬虫? 每个网站都应该提供 API,然而这是不可能的 即使提供了 API,往往也会限速,不如自己找接口 注意已知条件(robots.txt 和 sitemap.xm ...

随机推荐

  1. NC22596 Rinne Loves Data Structure

    题目链接 题目 题目描述 Rinne 喜欢 OI.在 9102 年的 PION 中,她在初赛遇到了这样一道题目: 阅读下列代码,然后回答问题. 补充:建树过程中会更新lc和rc,这实质上是一个二叉查找 ...

  2. 蔚来杯2022牛客暑期多校训练营3 AC

    比赛链接 A 题解 知识点:LCA. 队友写的,俺不会qwq.预处理出关键点序列的在树A B上的前缀LCA和后缀LCA,枚举去掉的关键节点并使用前后缀LCA算出剩余节点的LCA比较权值即可. 时间复杂 ...

  3. Working with Dates in PL/SQL(PL/SQL中使用日期)

    Working with Dates in PL/SQL By Steven Feuerstein  史蒂芬.佛伊尔斯坦 The previous articles in this introduct ...

  4. Java Enumeration接口详解

    二话不说,来看官方文档: public interface Enumeration<E> An object that implements the Enumeration interfa ...

  5. SSL/TLS 资料整理

    1. HTTPS详解二:SSL / TLS 工作原理和详细握手过程 看到另外几篇介绍不错的文章,再次分享一下 园内大佬写的, 通过一个小故事,理解 HTTPS 工作原理 这篇博文已经把 SSL 的工作 ...

  6. 项目实战:Qt+Arm+Fpga医疗肾镜(又名内窥镜)(实时影像、冻结、拍照、白平衡、九宫格、录像、背光调整、硬件光源调整、光源手动自动调整、物理按键)

    若该文为原创文章,转载请注明原文出处本文章博客地址:https://blog.csdn.net/qq21497936/article/details/111241205长期持续带来更多项目与技术分享, ...

  7. docker 发布.net core 项目(linux)

    一.准备阶段:前提:一台linux系统,安装好了Docker并启动 1.上传.netcore项目压缩文件 2.解压 注:若没有解压软件,先下载rar解压软件再安装:需注意系统是64位还是32   (下 ...

  8. 【LeetCode哈希表#1】有效的字母异位词+赎金信(数组)

    有效的字母异位词 力扣题目链接(opens new window) 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词. 示例 1: 输入: s = "anagr ...

  9. 教程|在矩池云使用 Stable Diffusion web UI v1.5 模型和 ControlNet 插件

    今天给大家介绍下如何在矩池云使用 Stable Diffusion web UI v1.5 模型和 Stable Diffusion ControlNet 插件. 租用机器 租用机器需要选择内存大于8 ...

  10. 图数据库 Nebula Graph 的代码变更测试覆盖率实践

    对于一个持续开发的大型工程而言,足够的测试是保证软件行为符合预期的有效手段,而不是仅仅依靠 code review 或者开发者自己的技术素质.测试的编写理想情况下应该完全定义软件的行为,但是通常情况都 ...