如果你突然想到了下一个大项目的想法:”我利用X公司提供的数据,为它建立一个前端如何?”你开始编码,发现X公司并没有为他们的数据提供API。他们的网站是他们数据的唯一来源。
现在是时候求助于良好的老式网页爬取,即从网站的HTML源代码中解析和提取数据的自动化过程。
jsoup 是一个实现 WHATWG HTML5规范的Java库,可以用来解析HTML文档,从HTML文档中寻找和提取数据,并操作HTML元素。它是一个简单的网络爬取的优秀库,因为它的性质很简单,而且它能够以浏览器的方式解析HTML,这样你就可以使用常见的CSS选择器。
在这篇文章中,你将学习如何使用jsoup在Java中进行网页爬取。
安装jsoup
本文使用 Maven 作为构建系统,因此要确保其已 安装 。注意,你也可以在没有Maven的情况下使用jsoup。你可以在 jsoup的下载页面 找到相关说明。
你将在本文中构建的应用程序可以在 GitHub 中找到,如果你想克隆它并跟着做,或者你可以按照说明从头开始构建该应用程序。
首先生成一个Maven项目:
mvn archetype:generate -DgroupId=com.example.jsoupexample -DartifactId=jsoup-example -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
名为
jsoup-example
的目录将存放项目文件。在文章的其余部分,你将在这个目录下工作。
编辑
pom.xml
文件,在
依赖关系
部分添加jsoup作为依赖关系:
本文使用的是
1.14.3
版本,这是在写作时的最新版本。当你阅读本文时,请到
jsoup下载页面
查看哪个版本是最新的。
在
plugins
部分,添加
Exec Maven Plugin
。
使用jsoup
网络爬取应该总是从人的角度出发。在直接跳入编码之前,你应该首先熟悉目标网站的情况。花一些时间研究网站的结构,弄清楚你想爬取什么数据,并查看HTML源代码以了解数据的位置和结构。
在这篇文章中,你将爬取ScrapingBee的博客,并收集有关发表的博客的信息:标题、链接等。这是很基本的,但它将帮助你开始你的网络爬取之旅。
让我们开始探索这个网站。在浏览器中打开
https://www.scrapingbee.com/blog
,点击
Ctrl+Shift+I
,打开开发工具。点击控制台左上角的小光标图标。如果你在启用该功能时将鼠标悬停在网页上的某个元素上,它将在HTML代码中定位该元素。它使你不必手动浏览HTML文件以确定哪些代码对应于哪些元素。这个工具将是你在整个爬取过程中的朋友。
将鼠标悬停在第一篇博文上。你会看到它在控制台中被突出显示,它是一个具有
p-10 md:p-28 flex
类的
div
。它旁边的其他博客都是一个具有
w-full sm:w-1/2 p-10 md:p-28 flex
类的
div
。
现在你已经对网页中的目标数据的大体结构有了一个大致的了解,现在是时候进行一些编码了。打开
src/main/java/com/example/jsoupexample/App.java
文件,删除自动生成的代码,并粘贴以下模板代码:
package com.example.jsoupexample; public class App public static void main( String[] args ) }
HTML解析
jsoup的工作原理是解析网页的HTML并将其转换为
Document
对象。可以把这个对象看作是DOM的一个程序化表示。为了创建这个
Document
,jsoup提供了一个有多个重载的
parse
方法,可以接受不同的输入类型。
一些值得注意的有以下几点。
-
parse(File file, @Nullable String charsetName)
: 解析一个HTML文件(也支持gzipped文件)。 -
parse(InputStream in, @Nullable String charsetName, String baseUri)
: 读取一个InputStream
并解析它。 -
parse(String html
): 解析一个HTML字符串。
所有这些方法都返回解析后的
Document
对象。
让我们看看最后一个的操作。首先,导入所需的类:
import org.jsoup.Jsoup; import org.jsoup.nodes.Document;
在
main
方法中,写下以下代码:
正如你所看到的,一个HTML字符串被直接传递到
解析
方法中。
Document
对象的
title
方法返回网页的标题。使用命令
mvn exec:java
运行该应用程序,它应该打印出
Web Scraping
。
把一个硬编码的HTML字符串放在
解析
方法中是可以的,但它并不真正有用。你如何从一个网页中获取HTML并解析它?
一种方法是使用
HttpURLConnection
类来向网站发出请求,并将响应的
InputStream
传递给
解析
方法。这里有一些示例代码可以做到这一点:
package com.example.jsoupexample; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; public class App { public static void main(String[] args) { URL url; try { url = new URL("https://www.scrapingbee.com/blog"); HttpURLConnection connection; try { connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty("accept", "application/json"); try { InputStream responseStream = connection.getInputStream(); Document document = Jsoup.parse(responseStream, "UTF-8", "https://www.scrapingbee.com/blog"); System.out.println(document.title()); } catch (IOException e) { e.printStackTrace(); } catch (IOException e1) { e1.printStackTrace(); } catch (MalformedURLException e1) { e1.printStackTrace(); }
请注意,仅仅为了从网页上获取HTML,就需要大量的模板代码。值得庆幸的是,jsoup提供了一个更方便的
连接
方法,可以连接到一个网页,获取HTML,并将其解析为一个
Document
,一气呵成。
只要把URL传给
connect
方法,它就会返回一个
Connection
对象:
Jsoup.connect("https://example.com")
你可以用这个对象来修改请求属性,比如用
data
方法添加参数,用
header
方法添加头信息,用
cookie
方法设置
cookie
,等等。每个方法都返回一个
Connection
对象,所以它们可以被链起来。
Jsoup.connect("https://example.com") .data("key", "value") .header("header", "value") .timeout(3000)
当你准备好进行请求时,调用
Connection
对象的
get
或
post
方法。这将返回解析后的
Document
对象。写下以下代码:
import java.io.IOException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; public class App { public static void main(String[] args) { try { Document document = Jsoup.connect("https://www.scrapingbee.com/blog") .timeout(5000) .get(); System.out.println(document.title()); } catch (IOException e) { e.printStackTrace(); }
这段代码被包裹在一个try-catch块中,因为
get
方法会抛出一个
IOException
。当你运行这段代码时,你应该看到网页的标题被打印出来。
getElementById
和
getElementsByClass
现在我们来解析一下博客。
文档
对象提供了许多方法来选择你想要的节点。如果你了解JavaScript,你会发现这些方法与你所习惯的非常相似。
第一个重要的方法是
getElementById
。它类似于JavaScript中的
getElementById
。它接受一个ID并返回具有该ID的唯一
元素
。我们的目标网页有一个ID为
content
的
div
。让我们用
getElementById
()来探测它。
... import org.jsoup.nodes.Element; // add this import Document document = Jsoup.connect("https://www.scrapingbee.com/blog") .timeout(5000) .get(); Element content = document.getElementById("content"); System.out.println(content.childrenSize());
注意
childrenSize
方法的使用,该方法返回
元素
的子代数。运行这段代码会打印出
2
,而这个
div
确实有两个孩子。
在这个特定的例子中,
getElementById
不是很有用,因为没有多少元素带有ID,所以让我们关注下一个重要方法:
getElementsByClass
。同样,它与JavaScript中的
getElementsByClassName
方法类似。请注意,这个方法返回一个
Elements
对象,而不是一个
Element
。这是因为可以有多个具有相同类的元素。
如果你记得,所有的博客都有一个
p-10
类,所以你可以用这个方法来掌握它们:
... import org.jsoup.select.Elements; // Add this import Elements blogs = document.getElementsByClass("p-10");
现在你可以用一个
for
循环来迭代这个列表:
for (Element blog : blogs) { System.out.println(blog.text()); }
text
方法返回该元素的文本,类似于JavaScript中的
innerText
。当你运行这段代码时,你会看到博客的文本被打印出来。
select
现在我们来解析每个博客的标题。你将使用
select
方法来完成这个任务。类似于JavaScript中的
querySelectorAll
,它接受一个CSS选择器并返回一个与该选择器相匹配的元素列表。在本例中,标题是博客中的一个
h4
元素。
因此,下面的代码将选择标题并从中获取文本:
for (Element blog : blogs) { String title = blog.select("h4").text(); System.out.println(title); }
下一步是获取博客的链接。为此,你将使用
blog.select("a")
来选择blog元素中的所有
a
标签。为了得到链接,使用
attr
方法,它返回第一个匹配元素的指定属性。由于每个博客只包含一个
a
标签,你将从
href
属性中获得链接。
String link = blog.select("a").attr("href"); System.out.println(link);
运行它可以打印出博客的链接:
Are Product Hunt's featured products still online today? /blog/producthunt-cemetery/ C# HTML parsers /blog/csharp-html-parser/ Using Python and wget to Download Web Pages and Files /blog/python-wget/ Using the Cheerio NPM Package for Web Scraping /blog/cheerio-npm/ ...
first
and
selectFirst
让我们看看是否能得到博客的标题图片。这次
blog.select("img")
将不起作用,因为有两张图片:头像和作者的头像。相反,你可以使用
first
方法来获得第一个匹配的元素,或者使用
selectFirst
。
String headerImage = blog.select("img").first().attr("src"); // Or String headerImage = blog.selectFirst("img").attr("src"); System.out.println(headerImage);
带有
select
功能的高级CSS选择器
现在我们来试试作者的头像。如果你看一下其中一个头像的URL,你会发现它包含 “作者 “这个词。因此,我们可以使用一个
属性选择器
来选择所有
src
属性包含 “author “的图片。相应的选择器是
img[src*=authors]
。
String authorImage = blog.select("img[src*=authors]").attr("src"); System.out.println(authorImage);
分页
最后,让我们看看如何处理分页问题。点击第
2页
的按钮会带你到
https://www.scrapingbee.com/blog/page/2/。
由此,你可以推断出你可以将页码附加到
https://www.scrapingbee.com/blog/page/
,以获得该页。由于每一页都是一个独立的网页,你可以把所有的东西都包在一个
for
循环中,并改变URL来迭代每一页:
for(int i = 1; i <= 4; ++i) { try { String url = (i==1) ? "https://www.scrapingbee.com/blog" : "https://www.scrapingbee.com/blog/page/" + i; Document document = Jsoup.connect(url) .timeout(5000) .get(); } catch (IOException e) { e.printStackTrace(); }
i = 1
的情况是例外的,因为第一页是在
https://www.scrapingbee.com/blog
,而不是
https://www.scrapingbee.com/blog/page/1。
最终代码
这是最后的代码,增加了一些空格和文字,使其更容易阅读:
package com.example.jsoupexample; import java.io.IOException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; public class App { public static void main(String[] args) { for(int i = 1; i <= 4; ++i) { System.out.println("PAGE " + i); try { String url = (i==1) ? "https://www.scrapingbee.com/blog" : "https://www.scrapingbee.com/blog/page/" + i; Document document = Jsoup.connect(url) .timeout(5000) .get(); Elements blogs = document.getElementsByClass("p-10"); for (Element blog : blogs) { String title = blog.select("h4").text(); System.out.println("TITLE: " + title); String link = blog.select("a").attr("href"); System.out.println("LINK: " + link); String headerImage = blog.selectFirst("img").attr("src"); System.out.println("HEADER IMAGE: " + headerImage); String authorImage = blog.select("img[src*=authors]").attr("src"); System.out.println("AUTHOR IMAGE:" + authorImage); System.out.println(); } catch (IOException e) { e.printStackTrace(); }
下面是输出的样本:
PAGE 1 TITLE: Are Product Hunt's featured products still online today? LINK: /blog/producthunt-cemetery/ HEADER IMAGE: https://d33wubrfki0l68.cloudfront.net/9ac2840bae6fc4d4ef3ddfa68da1c7dd3af3c331/8f56d/blog/producthunt-cemetery/cover.png AUTHOR IMAGE:https://d33wubrfki0l68.cloudfront.net/7e902696518771fbb06ab09243e5b4ceb2cba796/23431/images/authors/ian.jpg TITLE: C# HTML parsers LINK: /blog/csharp-html-parser/ HEADER IMAGE: https://d33wubrfki0l68.cloudfront.net/205f9e447558a0f654bcbb27dd6ebdd164293bf8/0adcc/blog/csharp-html-parser/cover.png AUTHOR IMAGE:https://d33wubrfki0l68.cloudfront.net/897083664781fa7d43534d11166e8a9a5b7f002a/115e0/images/authors/agustinus.jpg TITLE: Using Python and wget to Download Web Pages and Files LINK: /blog/python-wget/ HEADER IMAGE: https://d33wubrfki0l68.cloudfront.net/7b60237df4c51f8c2a3a46a8c53115081e6c5047/03e92/blog/python-wget/cover.png AUTHOR IMAGE:https://d33wubrfki0l68.cloudfront.net/8d26b33f8cc54b03f7829648f19496c053fc9ca0/8a664/images/authors/roel.jpg TITLE: Using the Cheerio NPM Package for Web Scraping LINK: /blog/cheerio-npm/ HEADER IMAGE: https://d33wubrfki0l68.cloudfront.net/212de31150b614ca36cfcdeada7c346cacb27551/9bfd1/blog/cheerio-npm/cover.png AUTHOR IMAGE:https://d33wubrfki0l68.cloudfront.net/bd86ac02a54c3b0cdc2082fec9a8cafc124d01d7/9d072/images/authors/ben.jpg