返回博客
指南
Mihai MaximLast updated on Mar 31, 20262 min read

JSoup:Java 中的 HTML 解析

JSoup:Java 中的 HTML 解析

JSoup 简介

网络爬虫可以被视为一种数字寻宝活动。你浏览网站,挖掘出所需的所有信息。这种技术被广泛应用于各种场景,例如查找最低价格、分析客户情绪,或是收集研究数据。

Java 被视为非常适合网页抓取的编程语言,因为它拥有种类繁多的库和框架,能够辅助这一过程。JSoup 是 Java 中最著名的网页抓取库之一。JSoup 允许你浏览和搜索网站的 HTML 内容,并提取所需的所有数据。

通过将 Java 与 JSoup 结合使用,您可以创建出色的网络爬虫应用程序,从而快速、轻松地从网站中提取数据。在本文中,我将带您了解使用 JSoup 进行网络爬虫的基础知识。

创建 JSoup 项目

在本节中,我们将使用 Maven 创建一个新的 Java 项目,并配置它以便通过 exec-maven-plugin 从命令行运行。这将使您能够轻松地将项目打包并在服务器上运行,从而实现数据提取过程的自动化和可扩展性。之后,我们将安装 JSoup 库。

创建 Maven 项目

Maven 是一款用于 Java 项目的构建自动化工具。它负责管理依赖项、构建和文档,从而简化了复杂 Java 项目的管理。借助 Maven,您可以轻松管理和组织项目的构建流程、依赖项及文档。它还支持与其他工具和框架的便捷集成。

安装 Maven 是一个简单的过程,只需几个步骤即可完成。

首先,从官方网站(https://maven.apache.org/download.cgi)下载 Maven 的最新版本。

下载完成后,将压缩包内容解压到您指定的目录中。

接下来,您需要设置环境变量。

在 Windows 系统中,将 JAVA_HOME 变量设置为 JDK 的安装路径,并将 Maven 安装目录下的 bin 文件夹添加到 PATH 变量中。

在 Linux/macOS 系统上,您需要在 ~/.bashrc 或 ~/.bash_profile 文件中添加以下几行:

export JAVA_HOME=path/to/the/jdk

export PATH=$PATH:path/to/maven/bin

在终端中运行 mvn --version 以确认 Maven 安装成功。

安装好 Maven 后,现在可以创建一个新的 Java Maven 项目:

mvn archetype:generate -DgroupId=com.project.scraper

-DartifactId=jsoup-scraper-project

-DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

这将创建一个名为“jsoup-scraper-project”的新文件夹,其中包含项目内容。


应用程序的入口点(主类)将位于“com.project.scraper”包中。

从命令行运行项目

为了从命令行运行 Maven Java 项目,我们将使用 exec-maven-plugin。

要安装该插件,您需要将其添加到项目的 pom.xml 文件中。具体操作是将以下代码片段添加到 pom.xml 文件的 <build><plugins> 部分:

<build>

 <plugins>

   <plugin>

     <groupId>org.codehaus.mojo</groupId>

     <artifactId>exec-maven-plugin</artifactId>

     <version>3.1.0</version>

     <executions>

       <execution>

         <goals>

           <goal>java</goal>

         </goals>

       </execution>

     </executions>

     <configuration>

       <mainClass>com.project.scraper.App</mainClass>

     </configuration>

   </plugin>

 </plugins>

</build>

请确保为项目的主类选择了正确的路径。

在终端(位于项目目录下)输入 mvn package exec:java 即可运行该项目。

安装 JSoup 库

要安装 JSoup 库,请将以下依赖项添加到项目的 pom.xml 文件中:

<dependency>

 <groupId>org.jsoup</groupId>

 <artifactId>jsoup</artifactId>

 <version>1.14.3</version>

</dependency>

请访问 https://mvnrepository.com/artifact/org.jsoup/jsoup 查看最新版本。

使用 JSoup 在 Java 中解析 HTML

在本节中,我们将探索 https://www.scrapethissite.com/pages/forms/ 网站,并了解如何提取有关冰球队的信息。通过分析一个真实的网站,您将理解使用 JSoup 进行网页抓取所涉及的概念和技术,以及如何将它们应用到您自己的项目中。

获取 HTML

要获取网站上的 HTML,您需要向其发送一个 HTTP 请求。在 JSoup 中,connect() 方法用于建立与指定 URL 的连接。它返回一个 Connection 对象,可用于配置请求并从服务器获取响应。

让我们看看如何使用 connect() 方法从指定 URL 获取 HTML,并将其写入本地 HTML 文件(hockey.html):

package com.project.scraper;

import org.jsoup.Jsoup;

import org.jsoup.nodes.Document;

import java.io.*;

import java.io.IOException;

public class App

{

   public static void main( String[] args )

   {

       String RAW_HTML;

       try {

           Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/")

                   .get();

           RAW_HTML = document.html();

           FileWriter writer = new FileWriter("hockey.html");

           writer.write(RAW_HTML);

           writer.close();

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

现在我们可以打开该文件,并使用开发者工具查看 HTML 的结构:

我们需要的数据位于页面上的一个 HTML 表格中。既然已经访问了该页面,我们就可以使用选择器从表格中提取内容了。

编写选择器

JSoup 中的选择器与 JavaScript 中的选择器有相似之处。两者语法相似,都允许您根据标签名、类名、ID 以及 CSS 属性从 HTML 文档中选择元素。

以下是 JSoup 中常用的主要选择器:

  • getElementsByTag(): 根据标签名选择元素。
  • getElementsByClass(): 根据类名选择元素。
  • getElementById():根据 ID 选择元素。
  • select():根据 CSS 选择器选择元素(类似于 querySelectorAll)

现在让我们使用其中一些方法来提取所有球队名称:

try {

   Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/")

           .get();

   Elements rows = document.getElementsByTag("tr");

   for(Element row : rows) {

      

       Elements teamName = row.getElementsByClass("name");

      

       if(teamName.text().compareTo("") != 0)

           System.out.println(teamName.text());

      

   }

} catch (IOException e) {

   e.printStackTrace();

}

// Prints the team names:

Boston Bruins

Buffalo Sabres

Calgary Flames

Chicago Blackhawks

Detroit Red Wings

Edmonton Oilers

Hartford Whalers

...

我们遍历了每一行,并对每一行使用类选择器 'name' 输出球队名称。

最后一个示例突出了选择器方法的灵活性,以及对已提取的元素多次应用这些方法的能力。这在处理复杂且庞大的 HTML 文档时尤为有用。

以下是另一个版本,它使用 Java 流和 select() 方法来打印所有球队名称:

try {

   Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/")

           .get();

   Elements teamNamesElements = document.select("table .team .name");

   String[] teamNames = teamNamesElements.stream()

                                         .map(element -> element.text())

                                         .toArray(String[]::new);

   for (String teamName : teamNames) {

       System.out.println(teamName);

   }

} catch (IOException e) {

   e.printStackTrace();

}

// Also prints the team names:

Boston Bruins

Buffalo Sabres

Calgary Flames

...

现在让我们打印所有表格标题和行:

try {

   Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/")

           .get();

   Elements tableHeadersElements = document.select("table th");

   Elements tableRowsElements = document.select("table .team");

   String[] tableHeaders =

   tableHeadersElements.stream()

                       .map(element -> element.text())

                       .toArray(String[]::new);

   String[][] tableRows =

   tableRowsElements.stream()

            .map(

                table_row -> table_row

                .select("td")

                .stream()

                .map(row_element -> row_element.text())

                .toArray(String[]::new)

               )

            .toArray(String[][]::new);

   for (int i = 0; i < tableHeaders.length; i++) {

       System.out.print(tableHeaders[i] + " ");

   }

   for (int i = 0; i < tableRows.length; i++) {

       for (int j = 0; j < tableRows[i].length; j++) {

           System.out.print(tableRows[i][j] + " ");

       }

       System.out.println();

   }

} catch (IOException e) {

   e.printStackTrace();

}

// Prints

Team Name Year Wins Losses OT Losses Win ...

Boston Bruins 1990 44 24  0.55 299 264 35 

Buffalo Sabres 1990 31 30  0.388 292 278 14 

Calgary Flames 1990 46 26  0.575 344 263 81 

Chicago Blackhawks 1990 49 23  0.613 284 211 73 

Detroit Red Wings 1990 34 38  0.425 273 298 -25

...

请注意,我们使用流来存储行数据。以下是使用 for 循环实现的更简单的方法:

String[][] tableRows = new String[tableRowsElements.size()][];

for (int i = 0; i < tableRowsElements.size(); i++) {

   Element table_row = tableRowsElements.get(i);

   Elements tableDataElements = table_row.select("td");

   String[] rowData = new String[tableDataElements.size()];

   for (int j = 0; j < tableDataElements.size(); j++) {

       Element row_element = tableDataElements.get(j);

       String text = row_element.text();

       rowData[j] = text;

   }

   tableRows[i] = rowData;

}

分页处理

从网站提取数据时,信息通常分散在多个页面上。为了抓取所有相关数据,必须向网站的每个页面发送请求并提取其中的信息。我们可以轻松地将此功能集成到项目中。

我们只需修改 URL 中的 page_num 查询参数,并使用 connect() 方法发起另一个 HTTP 请求即可。

int pageLimit = 25;

String [] tableHeaders = new String[0];

Vector<String[][]> rowsGroups = new Vector<String [][]>();

for (int currentPage=1; currentPage<pageLimit; currentPage++) {

   try {

       Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/?page_num=" + currentPage)

               .get();

       if(currentPage == 1) {

           Elements tableHeadersElements = document.select("table th");

           tableHeaders = tableHeadersElements.stream()

                   .map(element -> element.text())

                   .toArray(String[]::new);

       }

       Elements tableRowsElements = document.select("table .team");

       String[][] tableRows = new String[tableRowsElements.size()][];

       for (int i = 0; i < tableRowsElements.size(); i++) {

           Element table_row = tableRowsElements.get(i);

           Elements tableDataElements = table_row.select("td");

           String[] rowData = new String[tableDataElements.size()];

           for (int j = 0; j < tableDataElements.size(); j++) {

               Element row_element = tableDataElements.get(j);

               String text = row_element.text();

               rowData[j] = text;

           }

           tableRows[i] = rowData;

       }

       rowsGroups.add(tableRows);

   } catch (IOException e) {

       e.printStackTrace();

   }

   // do something with the headers and the the table rows groups

}

由于每页的表格拥有相同的表头,请务必确保不要重复抓取这些表头。

完整代码

以下是从 https://www.scrapethissite.com/pages/forms/ 网站提取所有表格的完整代码。我还添加了一个将数据保存为 .CSV 格式的函数:

package com.project.scraper;

import org.jsoup.Jsoup;

import org.jsoup.nodes.Document;

import org.jsoup.nodes.Element;

import org.jsoup.select.Elements;

import java.io.*;

import java.io.IOException;

import java.util.Vector;

public class App

{

   public static void main( String[] args )

   {

       int pageLimit = 25;

       String [] tableHeaders = new String[0];

       Vector<String[][]> rowsGroups = new Vector<String [][]>();

       for (int currentPage=1; currentPage<pageLimit; currentPage++) {

           try {

               Document document = Jsoup.connect("https://www.scrapethissite.com/pages/forms/?page_num=" + currentPage)

                       .get();

               if(currentPage == 1) {

                   Elements tableHeadersElements = document.select("table th");

                   tableHeaders = tableHeadersElements.stream()

                           .map(element -> element.text())

                           .toArray(String[]::new);

               }

               Elements tableRowsElements = document.select("table .team");

               String[][] tableRows = new String[tableRowsElements.size()][];

               for (int i = 0; i < tableRowsElements.size(); i++) {

                   Element table_row = tableRowsElements.get(i);

                   Elements tableDataElements = table_row.select("td");

                   String[] rowData = new String[tableDataElements.size()];

                   for (int j = 0; j < tableDataElements.size(); j++) {

                       Element row_element = tableDataElements.get(j);

                       String text = row_element.text();

                       rowData[j] = text;

                   }

                   tableRows[i] = rowData;

               }

               rowsGroups.add(tableRows);

           } catch (IOException e) {

               e.printStackTrace();

           }

       }

       writeFullTableToCSV(rowsGroups, tableHeaders, "full_table.csv");

   }

   public static void writeFullTableToCSV(Vector<String[][]> rowsGroups, String[] headers, String fileName) {

       File file = new File(fileName);

       try {

           FileWriter writer = new FileWriter(file);

           // write the headers first

           for (int i = 0; i < headers.length; i++) {

               writer.append(headers[i]);

               if (i != headers.length - 1) {

                   writer.append(",");

               }

           }

           writer.append("\n");

           // write all the rows groups

           for (String [][] rowsGroup : rowsGroups) {

               for (String[] row : rowsGroup) {

                   for (int i = 0; i < row.length; i++) {

                       writer.append(row[i]);

                       if (i != row.length - 1) {

                           writer.append(",");

                       }

                   }

                   writer.append("\n");

               }

           }

           writer.flush();

           writer.close();

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

}

总结

在本文中,我们介绍了如何安装 Maven 并创建新的 Java Maven 项目,以及如何通过命令行运行该项目。我们还讨论了如何通过在项目的 pom.xml 文件中添加依赖项来安装 JSoup 库。 最后,我们通过一个示例演示了如何使用 JSoup 解析 HTML 并从网站中提取数据。按照本文所述的步骤操作,您应该已经为搭建 JSoup 项目并开始从网站中提取数据打下了坚实的基础。JSoup 为网络爬虫提供了丰富的选项和可能性,我鼓励您去探索并将其应用到自己的项目中。

如您所见,数据通常分布在多个网页中。向同一域名频繁发送请求可能会导致您的 IP 被封禁。使用我们的产品 WebScrapingAPI,您将完全不必担心此类问题。我们的 API 确保您可以根据需要发送任意数量的请求。最棒的是,您可以免费试用。

关于作者
Mihai Maxim, 全栈开发工程师 @ WebScrapingAPI
Mihai Maxim全栈开发工程师

米海·马克西姆(Mihai Maxim)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。