用Kotlin进行网页抓取:一步步指南

Kotlin是进行网页抓取的强大工具。在本指南中,学习如何有效地使用它,涵盖设置、技术以及高效和道德抓取的最佳实践。
7 min read
如何使用Kotlin进行抓取:分步指南

本教程将教你如何构建一个Kotlin网页抓取脚本。具体来说,你将学习:

  • 为什么Kotlin是抓取网站的一个很好的语言。
  • 最好的Kotlin抓取库有哪些。
  • 如何从头开始构建一个Kotlin抓取器。

让我们开始吧!

Kotlin是进行网页抓取的可行选择吗?

简短回答:是的,它是!而且可能比Java更好!

Kotlin是一种静态类型的、跨平台的通用编程语言,其标准库依赖于Java类库。Kotlin的特别之处在于其简洁和有趣的编码方式。谷歌推荐Kotlin,并选择其作为Android开发的首选语言。

由于Kotlin与JVM的互操作性,它支持所有的Java抓取库。因此,你可以利用Java库的庞大生态系统,但语法更加简洁和直观。这是一个双赢的局面!

此外,Kotlin还自带一些原生库,其中包括HTML解析器和浏览器自动化库,这些库简化了数据提取。探索一些最受欢迎的库吧!

最佳的Kotlin网页抓取库

以下是一些最好的Kotlin网页抓取库列表:

  • skrape{it}:一个基于Kotlin的HTML/XML测试和网页抓取库,用于解析和解释HTML。它包括几个数据抓取器,使skrape{it}既可以作为传统的HTML解析器,也可以作为无头浏览器用于客户端DOM渲染。
  • chrome-reactive-kotlin:一个用Kotlin编写的低级DevTools协议客户端,用于编程控制基于Chromium的浏览器。
  • ksoup:一个受Jsoup启发的轻量级Kotlin库。Ksoup提供了解析HTML、提取HTML标签、属性和文本以及编码和解码HTML实体的方法。

不要忘记Kotlin与Java互操作。这意味着你可以使用任何其他Java中的网页抓取库。Jsoup是其中一个最流行的HTML解析器。更多内容请参考我们的使用Jsoup进行网页抓取指南。

前提条件

按照以下说明设置你的Kotlin网页抓取环境。

设置环境

要在你的机器上编写和运行Kotlin应用程序,你需要本地安装JDK(Java开发工具包)。从Oracle网站下载最新的LTS版本JDK,执行安装程序,并按照安装向导进行操作。截至撰写本文时,它是Java 21。

然后,你需要一个工具来管理依赖关系并构建你的Kotlin应用程序。Gradle和Maven都是很好的选择,所以你可以自由选择你喜欢的Java构建工具。由于Gradle支持Kotlin作为DSL(领域特定语言)语言,我们将选择Gradle。请注意,即使你是Maven用户,也可以轻松跟随本教程。

下载Maven或Gradle并安装。Gradle对Java版本特别敏感,所以一定要下载正确的包。Java 21的工作Gradle版本是8.5或更高版本。

最后,你需要一个Kotlin IDE。带有Kotlin语言扩展的Visual Studio CodeIntelliJ IDEA社区版都是不错的免费选择。

完成了!你现在有一个Kotlin就绪的环境!

创建一个Kotlin项目

为你的Kotlin网页抓取项目创建一个项目文件夹,并在终端中进入该文件夹:

mkdir KotlinWebScraper
cd KotlinWebScraper

我们将目录命名为KotlinWebScraper,但你可以随意命名。

接下来,在项目文件夹中启动以下命令以创建一个Gradle应用程序:

gradle init --type kotlin-application

在此过程中,你会被问到几个问题。你应该选择“Kotlin”作为构建脚本DSL,并给你的应用程序一个合适的包名,如com.kotlin.scraper。对于其他问题,默认答案应该是可以的。

初始化过程结束时,你将看到以下内容:

Select build script DSL:

  1: Kotlin

  2: Groovy

Enter selection (default: Kotlin) [1..2] 1

Project name (default: KotlinWebScraper):

Source package (default: kotlinwebscraper): com.kotlin.scraper

Enter target version of Java (min. 8) (default: 21):

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]

> Task :init

To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.5/samples/sample_building_kotlin_applications.html

BUILD SUCCESSFUL in 2m 10s

2 actionable tasks: 2 executed

太棒了!KotlinWebScraper文件夹现在将包含一个Gradle项目。

在你的Kotlin IDE中打开该文件夹,等待所需的后台任务完成,然后查看com.kotlin.scraper包中的主要App.kt文件。它应该包含以下内容:

/*

 * This Kotlin source file was generated by the Gradle 'init' task.

 */

package com.kotlin.scraping.demo

class App {

    val greeting: String

        get() {

            return "Hello World!"

        }

}

fun main() {

    println(App().greeting)

}

这是一个简单的Kotlin脚本,它会在终端中打印“Hello World!”

要验证它是否工作,使用以下Gradle命令启动脚本:

./gradlew run

等待项目构建和运行,你将看到:

> Task :app:run

Hello World!

BUILD SUCCESSFUL in 3s

3 actionable tasks: 2 executed, 1 up-to-date

你可以忽略Gradle日志消息。请关注“Hello World!”消息,这是脚本预期的输出。换句话说,你的Kotlin设置正常工作。

现在是时候使用Kotlin进行网页抓取了!

构建一个网页抓取Kotlin脚本

在这个一步步的部分,你将看到如何在Kotlin中构建一个网页抓取器。特别是,你将学习如何定义一个自动化脚本,从Quotes抓取沙箱站点提取数据。

在高层次上,你将要编写的Kotlin网页抓取脚 本将:

  1. 连接到目标页面。
  2. 选择页面上的quote HTML元素。
  3. 从中提取所需的数据。
  4. 对网站上的所有引用重复此操作,访问每个分页页面。
  5. 将收集到的数据导出为CSV格式。

以下是目标站点的样子:

来自Quotes to Scrape网站的示例

按照以下步骤,了解如何在Kotlin中进行网页抓取!

步骤1:安装抓取库

首先要做的是弄清楚哪些Kotlin网页抓取库最适合你的目标。为此,你必须检查目标站点。

因此,在浏览器中访问Quotes To Scrape沙箱站点。右键单击空白部分,选择“检查”选项以打开DevTools。进入“网络”选项卡,重新加载页面,并探索“Fetch/XHR”部分。

你应该看到以下内容:

Chrome开发者工具中的网络选项卡

没有AJAX请求!换句话说,目标页面不会通过JavaScript动态检索数据。这意味着服务器将包含所有感兴趣数据的页面返回给客户端。

因此,一个HTML解析库就足够了。你仍然可以使用浏览器自动化工具,但在浏览器中加载和渲染页面只会引入性能开销,而没有实际的好处。

因此,skrape{it}将是实现网页抓取目标的绝佳选择。在你的build.gradle.kts文件的dependencies对象中添加这一行:

implementation("it.skrape:skrapeit:1.2.2")

否则,如果你是Maven用户,请将这些行添加到pom.xml中的<dependencies>标签中:

<dependency>

    <groupId>it.skrape</groupId>

    <artifactId>skrapeit</artifactId>

    <version>1.2.2</version>

</dependency>

如果你使用IntelliJ IDEA,IDE会显示一个按钮以重新加载项目的依赖关系并安装新库。单击它以安装skrape{it}。

同样,你可以手动使用以下Gradle命令安装新依赖项:

./gradlew build --refresh-dependencies

安装过程可能需要一些时间,请耐心等待。

接下来,通过在App.kt脚本中添加以下导入来准备使用skrape{it}:

import it.skrape.core.*

import it.skrape.fetcher.*

不要忘记,skrape{it}带有许多数据抓取器。在这里,为了简化,我们导入了所有数据抓取器。同时,你只需要HttpFetcher,一个经典的HTTP客户端,它向给定URL发送HTTP请求并返回解析后的响应。

太棒了!你现在拥有使用Kotlin进行网页抓取所需的一切!

步骤2:下载目标页面并解析其HTML

在App.kt中,删除App类,并在main()函数中添加以下几行代码以使用skrape{it}连接到目标页面:

skrape(HttpFetcher) {

// make an HTTP GET request to the specified URL

   request {

url = "https://quotes.toscrape.com/"

   }

}

skrape{it}将使用前面提到的HttpFetcher类向给定的URL发起同步HTTP GET请求。

如果你想确保脚本按预期工作,请在skrape(HttpFetcher)定义中添加以下部分:

response {

   // get the HTML source code and print it

   htmlDocument {

       print(html)

   }

}

这告诉skrape{it}如何处理服务器响应。具体来说,它访问解析后的响应,然后打印页面的HTML代码。

你的App.kt Kotlin抓取脚本现在应包含:

package com.kotlin.scraper

import it.skrape.core.*

import it.skrape.fetcher.*

fun main() {

    skrape(HttpFetcher) {

// make an HTTP GET request to the specified URL

        request {

url = "https://quotes.toscrape.com/"

        }

response {

// get the HTML source code and print it

            htmlDocument {

print(html)

            }

        }

    }

}

执行脚本,它将打印:

<!doctype html>

<html lang="en">

 <head>

  <meta charset="UTF-8">

  <title>Quotes to Scrape</title>

  <link rel="stylesheet" href="/static/bootstrap.min.css">

  <link rel="stylesheet" href="/static/main.css">

 </head>

 <body>

 <!-- omitted for brevity... -->

这正是目标页面的HTML代码。干得好!

步骤3:检查页面内容

下一步是定义抓取逻辑。但在不知道如何选择页面元素的情况下,你无法做到这一点。这就是为什么需要额外的一步来检查目标页面的结构。

再次在浏览器中打开Quotes To Scrape。右键单击一个引用元素,选择“检查”以打开如下所示的DevTools:

检查特定引用的元素

在这里,你可以注意到每个引用卡片都是一个.quote HTML元素,其中包含:

  1. 一个带有引用文本的.text元素。
  2. 一个带有作者名字的.author元素。
  3. 多个.tag元素,每个显示一个标签。

注意,并非所有引用都有标签部分:

由Ayn Rand特定引用的示例

上述CSS选择器将帮助你选择页面中的所需DOM元素以从中提取数据。你还需要一个类来存储这些数据。因此,在你的网页抓取Kotlin脚本的顶部添加以下Quote类定义:

class Quote(var text: String, var author: String, tags: List<String>?) {

    var tags: MutableList<String> = ArrayList()

    init {

        if (tags != null) {

            this.tags.addAll(tags)

        }

    }

}

由于页面包含多个引用,请在main()中实例化一个Quote对象的列表:

val quotes: MutableList<Quote> = ArrayList()

在脚本结束时,quotes将包含从站点收集的所有引用。

使用你在这里理解和定义的内容,在下一步中实现抓取逻辑!

步骤4:实现抓取逻辑

skrape{it}有一种选择页面上HTML节点的独特方式。要在页面上应用CSS选择器,你需要在htmlDocument中定义一个与CSS选择器同名的部分:

skrape(HttpFetcher) {

    // request section...

    response {

        htmlDocument {

            // select all ".quote" HTML elements on the page

            ".quote" {

                // scraping logic...

            }

        }

    }

}

在“.quote”部分中,你可以定义一个findAll部分。它将包含应用于每个使用指定CSS选择器选择的quote HTML节点的逻辑。而findFirst将只获取第一个选择的元素。

在幕后,所有这些部分都不过是Kotlin lambda函数。因为这个,你可以在findAll中的forEach部分中使用it访问单个DOM元素。如果你对此不熟悉,它是lambda中单个参数的隐式名称。

it遵循类似的逻辑,但基于方法和属性。然后,你可以实现抓取逻辑,从每个引用中提取所需数据,实例化一个Quote对象,并将其添加到quotes列表中,如下所示:

".quote" {

findAll {

forEach {

// scraping logic on a single quote element

            val text = it.findFirst(".text").text

            val author = it.findFirst(".author").text

            val tags = try {

                it.findAll(".tag").map { tag -> tag.text }

            } catch(e: ElementNotFoundException) {

                null

            }

// create a Quote object and add it to the list

            val quote = Quote(

                text = text,

                author = author,

                tags = tags

            )

            quotes.add(quote)

        }

    }

}

通过text属性,你可以检索HTML元素的内部文本。由于并非所有quote HTML元素都包含标签,你需要处理ElementNotFoundException异常。当给定CSS选择器未匹配页面上的任何节点时,findAll将引发此异常。

使用以下代码导入ElementNotFoundException:

import it.skrape.selects.ElementNotFoundException

Put all the snippets together and log the data contained in the quotes array:

package com.kotlin.scraper

import it.skrape.core.*

import it.skrape.fetcher.*

import it.skrape.selects.ElementNotFoundException

// define a class to represent the scraped data in Kotlin

class Quote(var text: String, var author: String, tags: List<String>?) {

    var tags: MutableList<String> = ArrayList()

    init {

        if (tags != null) {

            this.tags.addAll(tags)

        }

    }

}

fun main() {

    // where to store the scraped data

    val quotes: MutableList<Quote> = ArrayList()

    skrape(HttpFetcher) {

// make an HTTP GET request to the specified URL

        request {

url = "https://quotes.toscrape.com/"

        }

response {

htmlDocument {

// select all ".quote" HTML elements on the page

                ".quote" {

findAll {

forEach {

// scraping logic on a single quote element

                            val text = it.findFirst(".text").text

                            val author = it.findFirst(".author").text

                            val tags = try {

                                it.findAll(".tag").map { tag -> tag.text }

                            } catch(e: ElementNotFoundException) {

                                null

                            }

// create a Quote object and add it to the list

                            val quote = Quote(

                                text = text,

                                author = author,

                                tags = tags

                            )

                            quotes.add(quote)

                        }

                    }

                }

            }

        }

    }

// log the scraped data

    for (quote in quotes) {

        println("Text: ${quote.text}")

        println("Author: ${quote.author}")

        println("Tags: ${quote.tags.joinToString("; ")}")

        println()

    }

}

注意使用joingToString()将tags列表合并为一个逗号分隔的字符串。

如果你执行脚本,现在将得到:

Text: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”

Author: Albert Einstein

Tags: change; deep-thoughts; thinking; world

# omitted for brevity...

Text: “A day without sunshine is like, you know, night.”

Author: Steve Martin

Tags: humor; obvious; simile

哇!你刚刚学会了如何使用Kotlin进行网页抓取!

步骤5:添加爬取逻辑

你刚刚从一个页面抓取了数据,但引用列表分布在多个页面上。如果你滚动页面到底部,你会注意到一个带有“下一页”链接的按钮:

下一页文本在一个标签内

这对所有页面都适用,除了最后一个页面:

检查最后一页的代码

要在Kotlin中进行网页爬取并抓取网站上的每个引用,你需要:

  1. 从当前页面抓取所有引用。
  2. 选择“下一页”元素(如果存在),并从中提取下一页的URL。
  3. 在新页面上重复第一步。

根据上述算法进行实现:

脚本现在不再是抓取单个页面然后停止,而是依赖于while循环。它将继续迭代,直到没有更多页面可抓取。当.next a CSS选择器引发ElementNotFoundException异常时,这意味着页面上没有“下一页”按钮,因此你在网站的最后一个分页页面。

请注意,htmlDocument部分可以包含多个CSS选择器部分。每个部分将按指定顺序运行。如果你再次启动网页抓取Kotlin脚本,quotes现在将存储网站上的所有100个引用。

太棒了!Kotlin网页抓取和爬取逻辑已经准备就绪。只需删除记录代码并添加数据导出逻辑即可。

步骤7:将抓取的数据导出为CSV

收集到的数据目前存储在一个Quote对象的列表中。打印到终端是有用的,但导出为CSV是最好的方式,使团队中的其他成员能够过滤、阅读和分析这些数据。

Kotlin为你提供了创建和填充CSV文件所需的一切,但使用库会使一切变得更容易。一个流行的Kotlin原生库,用于读取和写入CSV文件,是kotlin-csv

在build.gradle.kts中将其添加到项目的依赖项中:

implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.9.3")

或者,如果你是Maven用户:

<dependency>

    <groupId>com.github.doyaaaaaken</groupId>

    <artifactId>kotlin-csv-jvm</artifactId>

    <version>1.9.3</version>

</dependency>

安装库并在App.kt文件中导入:

import com.github.doyaaaaaken.kotlincsv.dsl.*

现在,你可以用几行代码将quotes导出为CSV文件:

val header = listOf("quote", "author", "tags")

val csvContent: List<List<String>> = quotes.map { quote ->

listOf(

        quote.text,

        quote.author,

        quote.tags.joinToString("; ")

        )

}

csvWriter().open("quotes.csv") {

writeRow(header)

    writeRows(csvContent)

}

注意List<String>是kotlin-csv中CSV记录的表示方式。首先,定义标题行的记录。然后,将quotes转换为所需数据。接下来,初始化一个CSV写入器,创建quotes.csv文件,并使用writeRow()和writeRows()填充它。

我们出发了!现在只剩下查看你的Kotlin网页抓取脚本的最终代码。

步骤8:将所有部分放在一起

以下是你的Kotlin抓取器的最终代码:

package com.kotlin.scraper

import it.skrape.core.*

import it.skrape.fetcher.*

import it.skrape.selects.ElementNotFoundException

import com.github.doyaaaaaken.kotlincsv.dsl.*

// define a class to represent the scraped data in Kotlin

class Quote(var text: String, var author: String, tags: List<String>?) {

    var tags: MutableList<String> = ArrayList()

    init {

        if (tags != null) {

            this.tags.addAll(tags)

        }

    }

}

fun main() {

    // where to store the scraped data

    val quotes: MutableList<Quote> = ArrayList()

    // the URL of the next page to visit

    var nextUrl: String? = "https://quotes.toscrape.com/"

    // until there is a page to visit

    while (nextUrl != null) {

        skrape(HttpFetcher) {

            // make an HTTP GET request to the specified URL

            request {

                url = nextUrl!!

            }

            response {

                htmlDocument {

                    // select all ".quote" HTML elements on the page

                    ".quote" {

                        findAll {

                            forEach {

                                // scraping logic on a single quote element

                                val text = it.findFirst(".text").text

                                val author = it.findFirst(".author").text

                                val tags = try {

                                    it.findAll(".tag").map { tag -> tag.text }

                                } catch (e: ElementNotFoundException) {

                                    null

                                }

                                // create a Quote object and add it to the list

                                val quote = Quote(

                                    text = text,

                                    author = author,

                                    tags = tags

                                )

                                quotes.add(quote)

                            }

                        }

                    }

                    // crawling logic

                    try {

                        ".next a" {

                            findFirst {

                                nextUrl = "https://quotes.toscrape.com" + attribute("href")

                            }

                        }

                    } catch (e: ElementNotFoundException) {

                        nextUrl = null

                    }

                }

            }

        }

    }

    // create a "quotes.csv" file and populate it

    // with the scraped data

    val header = listOf("quote", "author", "tags")

    val csvContent: List<List<String>> = quotes.map { quote ->

        listOf(

            quote.text,

            quote.author,

            quote.tags.joinToString("; ")

            )

    }

    csvWriter().open("quotes.csv") {

        writeRow(header)

        writeRows(csvContent)

    }

}

难以置信吗?得益于skrape{it},你可以在不到100行代码中从整个站点检索数据!

使用以下命令运行你的网页抓取Kotlin脚本:

./gradlew run

在抓取器遍历目标站点上的每个页面时要有耐心。完成后,项目根目录中会出现一个quotes.csv文件。打开它,你应该看到以下数据:

抓取的引用列表

就这样!你从在线页面中获得了非结构化数据,现在将其转换为易于探索的CSV文件!

使用代理在Kotlin中避免IP封禁

使用Kotlin进行网页抓取时,最大挑战之一是被反爬虫技术阻止。这些系统可以检测到你的脚本的自动化性质,并封禁你的IP。这样,它们会阻止你的抓取操作。

如何避免这种情况?使用网页代理!

按照以下步骤,学习如何将Bright Data代理集成到Kotlin中。

在Bright Data中设置代理

Bright Data是市场上最好的代理服务器,监控全球成千上万的代理服务器。当涉及到IP轮换时,最好的代理类型是住宅代理。

要开始,如果你已有账户,请登录Bright Data。否则,免费创建一个账户。你将访问以下用户控制面板:

Bright Data控制面板主页面

点击以下“查看代理产品”按钮:

点击查看代理产品

你将被重定向到以下“代理和抓取基础设施”页面:

Bright Data控制面板上的代理和抓取基础设施服务列表

向下滚动,找到“住宅代理”卡片,然后点击“开始”按钮:

开始使用住宅代理网络

你将进入住宅代理配置仪表板。根据你的需要,按照向导设置代理服务。如果你对如何配置代理有任何疑问,请随时联系24/7支持。

配置住宅代理设置

转到“访问参数”选项卡,并检索你的代理的主机、端口、用户名和密码,如下所示:

复制住宅代理的访问参数

注意,“主机”字段已经包含了端口。

这就是你需要构建代理URL并在skrape{it}中使用它的全部内容。将所有信息放在一起,并使用以下语法构建URL:

<Username>:<Password>@<Host>

例如,在这种情况下,它将是:

brd-customer-hl_4hgu8dwd-zone-residential:[email protected]:XXXXX

切换到“活动代理”,按照最后的指示操作,你就准备好了!

在所有配置完成后激活代理

在Kotlin中集成代理

在skrape{it}中集成Bright Data的代码片段如下所示:

skrape(HttpFetcher) {

    request {

url = "https://quotes.toscrape.com/"

        proxy = proxyBuilder {

type = Proxy.Type.HTTP

            host = "brd.superproxy.io"

            port = XXXXX

        }

        authentication = basic {

            username = "brd-customer-hl_4hgu8dwd-zone-residential"

            password = "ZZZZZZZZZZ"

        }

    }

    // ...

}

如你所见,一切归结于使用代理和认证请求选项。从现在开始,skrape{it}将通过Bright Data代理向指定的URL发起请求。告别IP封禁!

保持你的Kotlin网页抓取操作道德和尊重

抓取网页是收集各种用途有用数据的有效方式。请记住,最终目标是检索数据,而不是损害目标站点。因此,你必须以正确的预防措施来进行此任务。

遵循以下提示,以负责任的方式进行Kotlin网页抓取:

  • 仅抓取公开可用的信息:专注于检索站点上公开访问的数据。相反,避免抓取受登录凭据或其他授权形式保护的页面。在未经适当许可的情况下抓取私人或敏感数据是不道德的,可能会导致法律后果。
  • 遵守robots.txt文件:每个站点都有一个robots.txt文件,定义了自动爬虫如何访问其页面的规则。为了保持道德抓取实践,你必须遵守这些准则。了解更多内容,请参考我们的robots.txt网页抓取指南
  • 限制请求频率:在短时间内发起太多请求会导致服务器过载,影响所有用户的站点性能。这也可能触发速率限制措施并导致你被封禁。因此,通过在请求之间添加随机延迟,避免向目标服务器发送大量请求。
  • 检查并遵守站点的服务条款:在抓取站点之前,请审查其服务条款。这些条款可能包含有关版权、知识产权以及如何和何时使用其数据的指南。
  • 依靠可信赖和最新的抓取工具:选择信誉良好的提供商,并选择维护良好且定期更新的工具和库。只有这样,你才能确保它们符合最新的道德Kotlin抓取原则。如果你有任何疑问,请查阅我们的文章如何选择最佳网页抓取服务

结论

在本指南中,你了解了为什么Kotlin是网页抓取的一个很好的语言,尤其是与Java相比。你还了解了一些最好的Kotlin抓取库。然后,你学习了如何使用skrape{it}构建一个抓取器,从一个真实世界的网站的多个页面中提取数据。正如你在这里所体验的,使用Kotlin进行网页抓取是简单的,只需要几行代码。

你的抓取操作面临的主要挑战是反爬虫解决方案。网站采用这些系统来保护其数据不被自动脚本访问,在脚本访问其页面之前封禁它们。绕过所有这些措施并不容易,需要高级工具。幸运的是,Bright Data为你提供了帮助!

这些是Bright Data提供的一些抓取产品:

  • 网页抓取API:易于使用的API,用于程序化访问来自几十个热门域的结构化网页数据。
  • 抓取浏览器:一个基于云的可控制浏览器,提供JavaScript渲染能力,同时处理浏览器指纹、验证码、自动重试等。它与最流行的自动化浏览器库集成,如Playwright和Puppeteer。
  • 网页解锁器:一个解锁API,可以无缝返回任何页面的原始HTML,绕过任何反抓取措施。

不想处理网页抓取但仍对在线数据感兴趣?探索Bright Data的现成数据集!