Java中使用Jsoup解析HTML表格教程

Jsoup是一个用于抓取 HTML 页面的开源库。它提供了一个使用 DOM API 方法进行数据解析、提取和操作的 API。

在本文中,我们将了解如何使用 Jsoup 解析 HTML 表。我们将使用 Jsoup 从 HTML 表中检索和更新数据,并添加和删除表中的行。

要使用 Jsoup 库,请将以下依赖项添加到项目中:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.17.2</version>
</dependency>

我们可以在 Maven 中央存储库中找到最新版本的Jsoup库。

表结构
为了说明如何通过 jsoup 解析 HTML 表,我们将使用一个示例 HTML 结构。完整的 HTML 结构可在本文末尾提到的 GitHub 存储库中提供的代码库中找到。在这里,我们显示一个仅包含两行数据的表格,用于代表性目的:

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Maths</th>
            <th>English</th>
            <th>Science</th>
         </tr>
    </thead>
    <tbody>
        <tr>
            <td>Student 1</td>
            <td>90</td>
            <td>85</td>
            <td>92</td>
        </tr>
     </tbody>
</table>

正如我们所看到的,我们正在解析带有带有thead标记的标题行,后跟tbody标记中的数据行的表。我们假设 HTML 文档中的表格采用上述格式。

解析表
首先,要从解析的文档中选择 HTML 表,我们可以使用下面的代码片段:

Element table = doc.select("table");
Elements rows = table.select("tr"); 
Elements first = rows.get(0).select("th,td");
正如我们所看到的,从文档中选择表元素,然后,为了获取行元素,从表元素中选择tr 。由于表中有多行,因此我们选择了第一行中的th或td元素。通过使用这些函数,我们可以编写以下函数来解析表数据。

在这里,我们假设表中没有使用colspan或rowspan元素,并且第一行带有 header标签。

下面是解析表的代码:

public List<Map<String, String>> parseTable(Document doc, int tableOrder) {
    Element table = doc.select("table").get(tableOrder);
    Element tbody = table.select(
"tbody").get(0);
    Elements dataRows = tbody.select(
"tr");
    Elements headerRow = table.select(
"tr")
      .get(0)
      .select(
"th,td");
    List<String> headers = new ArrayList<String>();
    for (Element header : headerRow) {
        headers.add(header.text());
    }
    List<Map<String, String>> parsedDataRows = new ArrayList<Map<String, String>>();
    for (int row = 0; row < dataRows.size(); row++) {
        Elements colVals = dataRows.get(row).select(
"th,td");
        int colCount = 0;
        Map<String, String> dataRow = new HashMap<String, String>();
        for (Element colVal : colVals) {
            dataRow.put(headers.get(colCount++), colVal.text());
        }
        parsedDataRows.add(dataRow);
    }
    return parsedDataRows;
}

在此函数中,参数doc是从文件加载的 HTML 文档,tableOrder是文档中的第 n 个表格元素。我们使用List<Map<String, String>>在tbody元素下的表中存储dataRows 列表。列表的每个元素都是一个代表dataRow的Map。该映射将列名存储为键,并将该列的行值存储为映射值。使用地图列表可以轻松访问检索到的数据。

列表索引代表行号,我们可以通过其映射键获取特定的单元格数据。

我们可以使用下面的测试用例验证表数据是否正确检索:

@Test
public void whenDocumentTableParsed_thenTableDataReturned() {
    JsoupTableParser jsoParser = new JsoupTableParser();
    Document doc = jsoParser.loadFromFile("Students.html");
    List<Map<String, String>> tableData = jsoParser.parseTable(doc, 0);
    assertEquals(
"90", tableData.get(0).get("Maths")); 
}

从 JUnit 测试用例中,我们可以确认,由于我们已经解析了所有表格单元格的文本并将其存储在HashMap对象的ArrayList中,因此列表中的每个元素代表表中的一个数据行。行由 HashMap 表示,其中键作为列标题,单元格文本作为值。使用这个结构,我们可以轻松访问表数据。

更新解析表的元素
要在解析时插入或更新元素,我们可以在从行检索的td元素上使用以下代码:

colVals.get(colCount++).text(updateValue);
或者

colVals.get(colCount++).html(updateValue);
更新解析表中的值的函数如下所示:

public void updateTableData(Document doc, int tableOrder, String updateValue) {
    Element table = doc.select("table").get(tableOrder);
    Element tbody = table.select(
"tbody").get(0);
    Elements dataRows = tbody.select(
"tr");
    for (int row = 0; row < dataRows.size(); row++) {
        Elements colVals = dataRows.get(row).select(
"th,td");
        for (int colCount = 0; colCount < colVals.size(); colCount++) {
            colVals.get(colCount).text(updateValue);
        }
    }
}

在上面的函数中,我们从表的tbody元素获取数据行。该函数遍历表的每个单元格并将其值设置为参数值UpdatedValue。它将所有单元格更新为相同的值,以演示可以使用 Jsoup 更新单元格值。我们可以通过指定数据行的行索引和列索引来更新各个单元格的值。

下面的测试验证了更新功能:

@Test
public void whenTableUpdated_thenUpdatedDataReturned() {
    JsoupTableParser jsoParser = new JsoupTableParser();
    Document doc = jsoParser.loadFromFile("Students.html");
    jsoParser.updateTableData(doc, 0,
"50");
    List<Map<String, String>> tableData = jsoParser.parseTable(doc, 0);
    assertEquals(
"50", tableData.get(2).get("Maths"));
}

JUnit 测试用例确认更新操作将所有表格单元格值更新为 50。这里我们验证 Maths 列的第三个数据行的数据。

同样,我们可以为表格的特定单元格设置所需的值。

向表中添加行
我们可以使用以下函数向表中添加一行:

public void addRowToTable(Document doc, int tableOrder) {
    Element table = doc.select("table").get(tableOrder);
    Element tbody = table.select(
"tbody").get(0);
    Elements rows = table.select(
"tr");
    Elements headerCols = rows.get(0).select(
"th,td");
    int numCols = headerCols.size();
    Elements colVals = new Elements(numCols);
    for (int colCount = 0; colCount < numCols; colCount++) {
        Element colVal = new Element(
"td");
        colVal.text(
"11");
        colVals.add(colVal);
    }
    Elements dataRows = tbody.select(
"tr");
    Element newDataRow = new Element(
"tr");
    newDataRow.appendChildren(colVals);
    dataRows.add(newDataRow);
    tbody.html(dataRows.toString());
}

在上面的函数中,我们从表的标题行获取列数,从表的tbody元素获取数据行数。将新行添加到dataRows列表后,我们使用dataRows更新tbody HTML 内容。

我们可以使用以下测试用例验证行添加:

@Test
public void whenTableRowAdded_thenRowCountIncreased() {
    JsoupTableParser jsoParser = new JsoupTableParser();
    Document doc = jsoParser.loadFromFile("Students.html");
    List<Map<String, String>> tableData = jsoParser.parseTable(doc, 0);
    int countBeforeAdd = tableData.size();
    jsoParser.addRowToTable(doc, 0);
    tableData = jsoParser.parseTable(doc, 0);
    assertEquals(countBeforeAdd + 1, tableData.size());
}

从JUnit测试用例中我们可以确认,对表进行addRowToTable操作将表中的行数增加了1。该操作在列表末尾添加了一个新行。

类似地,我们可以在将行添加到行元素集合时通过指定索引来在任意位置添加行。

从表中删除行
我们可以使用以下函数从表中删除一行:

public void deleteRowFromTable(Document doc, int tableOrder, int rowNumber) {
    Element table = doc.select("table").get(tableOrder);
    Element tbody = table.select(
"tbody").get(0);
    Elements dataRows = tbody.select(
"tr");
    if (rowNumber < dataRows.size()) {
        dataRows.remove(rowNumber);
    }
}

在上面的函数中,我们获取表的tbody元素。从tbody中,我们获得了dataRows列表。从 dataRows 列表中,我们删除表中 rowNumber 位置的行。我们可以使用以下测试用例来验证行删除:

@Test
public void whenTableRowDeleted_thenRowCountDecreased() {
    JsoupTableParser jsoParser = new JsoupTableParser();
    Document doc = jsoParser.loadFromFile("Students.html");
    List<Map<String, String>> tableData = jsoParser.parseTable(doc, 0);
    int countBeforeDel = tableData.size();
    jsoParser.deleteRowFromTable(doc, 0, 2);
    tableData = jsoParser.parseTable(doc, 0);
    assertEquals(countBeforeDel - 1, tableData.size());
}

JUnit 测试用例确认对表执行deleteRowFromTable操作将表中的行数减少了1。

同样,我们可以通过指定索引来删除任意位置的行,同时将其从行 元素集合中删除。