本教程将教你如何构建一个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 Code和IntelliJ 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网页抓取脚 本将:
- 连接到目标页面。
- 选择页面上的quote HTML元素。
- 从中提取所需的数据。
- 对网站上的所有引用重复此操作,访问每个分页页面。
- 将收集到的数据导出为CSV格式。
以下是目标站点的样子:
按照以下步骤,了解如何在Kotlin中进行网页抓取!
步骤1:安装抓取库
首先要做的是弄清楚哪些Kotlin网页抓取库最适合你的目标。为此,你必须检查目标站点。
因此,在浏览器中访问Quotes To Scrape沙箱站点。右键单击空白部分,选择“检查”选项以打开DevTools。进入“网络”选项卡,重新加载页面,并探索“Fetch/XHR”部分。
你应该看到以下内容:
没有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元素,其中包含:
- 一个带有引用文本的.text元素。
- 一个带有作者名字的.author元素。
- 多个.tag元素,每个显示一个标签。
注意,并非所有引用都有标签部分:
上述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中进行网页爬取并抓取网站上的每个引用,你需要:
- 从当前页面抓取所有引用。
- 选择“下一页”元素(如果存在),并从中提取下一页的URL。
- 在新页面上重复第一步。
根据上述算法进行实现:
脚本现在不再是抓取单个页面然后停止,而是依赖于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。否则,免费创建一个账户。你将访问以下用户控制面板:
点击以下“查看代理产品”按钮:
你将被重定向到以下“代理和抓取基础设施”页面:
向下滚动,找到“住宅代理”卡片,然后点击“开始”按钮:
你将进入住宅代理配置仪表板。根据你的需要,按照向导设置代理服务。如果你对如何配置代理有任何疑问,请随时联系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的现成数据集!