java - 将数据从复杂的 HTML 表中提取到 Java 中的二维数组

标签 java html algorithm parsing jsoup

如何在 Java 中将 具有 colspan 和 rowspan 的 HTML 表转换为二维数组(矩阵)?

我在 Python 和 jQuery 中找到了很好的解决方案,但在 Java 中却找不到(只有非常简单的表格通过 jsoup)。 XSLT 有一个很好的解决方案,但由于格式错误的输入 HTML 文件,它对我来说不合适。

输入表示例:

  <body>
    <table border="1">
        <tr><td>H1</td><td colspan="2">H2</td><tr>
        <tr><td></td><td>SubH2_1</td><td>SubH2_2</td><tr>
       <tr><td rowspan="3">A1</td><td>B1</td><td rowspan="2">C1</td></tr>
       <tr><td rowspan="2">B2</td></tr>
       <tr><td>C3</td></tr>
       <tr><td>C4</td><td>C5</td><td>C6</td></tr>
        <tr><td>D7</td><td colspan="2">D9</td></tr>
        <tr><td  colspan="3">Notes</td></tr>
   </table>
</body>

enter image description here

期望的输出:

    [['H1', 'H2', 'H2'],
     ['', 'SubH2_1', 'SubH2_2'],
     ['A1', 'B1', 'C1'],
     ['A1', 'B2', 'C3'],
     ['C4', 'C5', 'C6'],
     ['D7', 'D9', 'D9'],
     ['Notes', 'Notes', 'Notes']]

最佳答案

我找到了一种使用 Jsoup 的方法和 Java 8 Stream API:

//given:
final InputStream html = getClass().getClassLoader().getResourceAsStream("table.html");

//when:
final Document document = Jsoup.parse(html, "UTF-8", "/");

final List<List<String>> result = document.select("table tr")
    .stream()
    // Select all <td> tags in single row
    .map(tr -> tr.select("td"))
    // Repeat n-times those <td> that have `colspan="n"` attribute
    .map(rows -> rows.stream()
        .map(td -> Collections.nCopies(td.hasAttr("colspan") ? Integer.valueOf(td.attr("colspan")) : 1, td))
        .flatMap(Collection::stream)
        .collect(Collectors.toList())
    )
    // Fold final structure to 2D List<List<Element>>
    .reduce(new ArrayList<List<Element>>(), (acc, row) -> {
        // First iteration - just add current row to a final structure
        if (acc.isEmpty()) {
            acc.add(row);
            return acc;
        }

        // If last array in 2D array does not contain element with `rowspan` - append current
        // row and skip to next iteration step
        final List<Element> last = acc.get(acc.size() - 1);
        if (last.stream().noneMatch(td -> td.hasAttr("rowspan"))) {
            acc.add(row);
            return acc;
        }

        // In this case last array in 2D array contains an element with `rowspan` - we are going to
        // add this element n-times to current rows where n == rowspan - 1
        final AtomicInteger index = new AtomicInteger(0);
        last.stream()
            // Map to a helper list of (index in array, rowspan value or 0 if not present, Jsoup element)
            .map(td -> Arrays.asList(index.getAndIncrement(), Integer.valueOf(td.hasAttr("rowspan") ? td.attr("rowspan") : "0"), td))
            // Filter out all elements without rowspan
            .filter(it -> ((int) it.get(1)) > 1)
            // Add all elements with rowspan to current row at the index they are present 
            // (add them with `rowspan="n-1"`)
            .forEach(it -> {
                final int idx = (int) it.get(0);
                final int rowspan = (int) it.get(1);
                final Element td = (Element) it.get(2);

                row.add(idx, rowspan - 1 == 0 ? (Element) td.removeAttr("rowspan") : td.attr("rowspan", String.valueOf(rowspan - 1)));
            });

        acc.add(row);
        return acc;
    }, (a, b) -> a)
    .stream()
    // Extract inner HTML text from Jsoup elements in 2D array
    .map(tr -> tr.stream()
        .map(Element::text)
        .collect(Collectors.toList())
    )
    .collect(Collectors.toList());

我添加了很多注释来解释特定算法步骤中发生的情况。

在此示例中,我使用了以下 html 文件:

<body>
<table border="1">
    <tr><td>H1</td><td colspan="2">H2</td></tr>
    <tr><td></td><td>SubH2_1</td><td>SubH2_2</td></tr>
    <tr><td rowspan="2">A1</td><td>B1</td><td>C1</td></tr>
    <tr><td>B2</td><td>C3</td></tr>
    <tr><td>C4</td><td>C5</td><td>C6</td></tr>
    <tr><td>D7</td><td colspan="2">D9</td></tr>
    <tr><td  colspan="3">Notes</td></tr>
</table>
</body>

和你的一样,唯一不同的是它有rowspan使用固定 - 在您的示例中 A1重复三次而不是两次。还有两个<tr>在这个例子中被正确关闭,否则两个额外的空数组出现在最终结构中。

这是控制台输出:

[H1, H2, H2]
[, SubH2_1, SubH2_2]
[A1, B1, C1]
[A1, B2, C3]
[C4, C5, C6]
[D7, D9, D9]
[Notes, Notes, Notes]

您可以使用粘贴在问题中的精确 HTML 来运行此示例,它会产生一些不同的输出:

[H1, H2, H2]
[]
[, SubH2_1, SubH2_2]
[]
[A1, B1, C1]
[A1, B2, C1]
[A1, B2, C3]
[C4, C5, C6]
[D7, D9, D9]
[Notes, Notes, Notes]

那些空数组出现是因为有两个未闭合的<tr> HTML 中的元素。

<tr><td>H1</td><td colspan="2">H2</td><tr>
<tr><td></td><td>SubH2_1</td><td>SubH2_2</td><tr>

关闭它们并再次运行算法将产生以下输出:

[H1, H2, H2]
[, SubH2_1, SubH2_2]
[A1, B1, C1]
[A1, B2, C1]
[A1, B2, C3]
[C4, C5, C6]
[D7, D9, D9]
[Notes, Notes, Notes]

如你所见A1存在 3 次,因为它有一个属性 rowspan="3"B2rowspan="2"C1rowspan="2"以及。它生成的 HTML 看起来“几乎”与我的第一个示例中的相同,但是当您仔细查看这 3 行时,您会发现它们不在同一像素级别。根据您预期的响应,我已经修复了输入 HTML 的外观和行为符合您的预期。

如果我无法修改输入的 HTML 怎么办?

好吧,如果您不能修改输入的 HTML,那么您将不得不:

  • 过滤掉所有由于未关闭而创建的空数组<tr>标签
  • 检查您对 A1 的输出预期, B2C3 - HTML View 不显示用 HTML 编写的此表的确切结构。

示例项目源代码

在这里你可以找到full source code的 JUnit 测试,我曾经找到你问题的答案。欢迎下载this sample Maven project托管在 GitHub 上以尝试算法的实现。

希望对你有帮助。

关于java - 将数据从复杂的 HTML 表中提取到 Java 中的二维数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45233038/

相关文章:

php在同一页面上打开列出的文件/目录而不重定向

java - 益智游戏安卓DFS算法

c - Quicksort 取决于选择 Pivot

java - 为什么允许从另一个构造函数调用一个构造函数?

java - 使用客户端登录授权 Google 云端硬盘服务 java?

java - 在Oracle SQL中编译Java源代码时Base64出现错误

html - 移动设备上的 Bootstrap 多级菜单问题

java - Mybatis 可以支持将动态列映射到 bean 的映射字段吗?

css - 我怎样才能停止自动下载 html5 音频

algorithm - 我的 padovan 系列代码有什么问题?