in

如何使用JSoup在Java中解析HTML

如何使用JSoup在Java中解析HTML

如果你突然想到了下一个大项目的想法:”我利用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作为依赖关系:

安装jsoup

本文使用的是 1.14.3 版本,这是在写作时的最新版本。当你阅读本文时,请到 jsoup下载页面 查看哪个版本是最新的。

plugins 部分,添加 Exec Maven Plugin

安装jsoup1


使用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 方法,可以接受不同的输入类型。

一些值得注意的有以下几点。

  1. parse(File file, @Nullable String charsetName) : 解析一个HTML文件(也支持gzipped文件)。
  2. parse(InputStream in, @Nullable String charsetName, String baseUri) : 读取一个 InputStream 并解析它。
  3. parse(String html ): 解析一个HTML字符串。

所有这些方法都返回解析后的 Document 对象。

让我们看看最后一个的操作。首先,导入所需的类:

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

main 方法中,写下以下代码:

用JSoup在Java中解析HTML1

正如你所看到的,一个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 确实有两个孩子。

用JSoup在Java中解析HTML2

在这个特定的例子中, 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