如何用rowspan和colspan解析表

Posted

技术标签:

【中文标题】如何用rowspan和colspan解析表【英文标题】:How to parse table with rowspan and colspan 【发布时间】:2018-07-01 18:33:18 【问题描述】:

首先,我阅读了Parsing a table with rowspan and colspan。我什至回答了这个问题。请在将其标记为重复之前阅读。

<table border="1">
  <tr>
    <th>A</th>
    <th>B</th>
  </tr>
  <tr>
    <td rowspan="2">C</td>
    <td rowspan="1">D</td>
  </tr>
  <tr>
    <td>E</td>
    <td>F</td>
  </tr>
  <tr>
    <td>G</td>
    <td>H</td>
  </tr>
</table>

它会像这样渲染

+---+---+---+
| A | B |   |
+---+---+   |
|   | D |   |
+ C +---+---+
|   | E | F |
+---+---+---+
| G | H |   |
+---+---+---+
<table border="1">
  <tr>
    <th>A</th>
    <th>B</th>
  </tr>
  <tr>
    <td rowspan="2">C</td>
    <td rowspan="2">D</td>
  </tr>
  <tr>
    <td>E</td>
    <td>F</td>
  </tr>
  <tr>
    <td>G</td>
    <td>H</td>
  </tr>
</table>

但是,这将呈现为这样。

+---+---+-------+
| A | B |       |
+---+---+-------+
|   |   |       |
| C | D +---+---+
|   |   | E | F |
+---+---+---+---+
| G | H |       |
+---+---+---+---+

我上一个答案的代码只能解析第一行中定义了所有列的表。

def table_to_2d(table_tag):
    rows = table_tag("tr")
    cols = rows[0](["td", "th"])
    table = [[None] * len(cols) for _ in range(len(rows))]
    for row_i, row in enumerate(rows):
        for col_i, col in enumerate(row(["td", "th"])):
            insert(table, row_i, col_i, col)
    return table


def insert(table, row, col, element):
    if row >= len(table) or col >= len(table[row]):
        return
    if table[row][col] is None:
        value = element.get_text()
        table[row][col] = value
        if element.has_attr("colspan"):
            span = int(element["colspan"])
            for i in range(1, span):
                table[row][col+i] = value
        if element.has_attr("rowspan"):
            span = int(element["rowspan"])
            for i in range(1, span):
                table[row+i][col] = value
    else:
        insert(table, row, col + 1, element)

soup = BeautifulSoup('''
    <table>
        <tr><th>1</th><th>2</th><th>5</th></tr>
        <tr><td rowspan="2">3</td><td colspan="2">4</td></tr>
        <tr><td>6</td><td>7</td></tr>
    </table>''', 'html.parser')
print(table_to_2d(soup.table))

我的问题是如何将表格解析为二维数组,该数组准确地表示它在浏览器中的呈现方式。或者有人可以解释一下浏览器如何呈现表格也可以。

【问题讨论】:

您可能需要对表格进行两次遍历:一次确定其真实尺寸,一旦知道,再进行一次遍历以提取数据。 @JohnGordon 跑两遍不是问题。但我需要了解渲染表格的规则是什么。没有它,我会认为第二个代码有 4 行而不是 3 行。 可能与此处相关:How to parse an HTML table with rowspans in Python? 只是好奇,这样的解析器有什么实际需要吗?在我看来,使用自定义逻辑处理跨度通常比以后从列表中获取任何信息要好。 【参考方案1】:

您不能只计算tdth 单元格,不。您必须对表进行扫描以获取每行的列数,并将前一行的任何活动行跨度添加到该计数中。

在different scenario parsing a table with rowspans 中,我跟踪了每个列号的行跨度计数,以确保来自不同单元格的数据最终位于正确的列中。这里可以使用类似的技术。

第一个计数列;只保留最高的数字。保留 2 或更大的行跨数列表,并为您处理的每一行列减去 1。这样你就知道每行有多少“额外”列。取最高的列数来构建您的输出矩阵。

接下来,再次循环遍历行和单元格,这一次在字典中跟踪从列号到活动计数的行跨度。再一次,将任何值为 2 或超过下一行的东西转移到下一行。然后移动列号以考虑任何活动的行跨度;如果在第 0 列上有活动的行跨度,则连续的第一个 td 实际上是第二个,等等。

您的代码将跨越的列和行的值重复复制到输出中;我通过在给定单元格的 colspanrowspan 数字(每个默认为 1)上创建循环以多次复制该值来实现相同的目的。我忽略了重叠的单元格; HTML table specifications 表示重叠单元格是错误的,由用户代理来解决冲突。在下面的代码中 colspan 胜过 rowspan 单元格。

from itertools import product

def table_to_2d(table_tag):
    rowspans = []  # track pending rowspans
    rows = table_tag.find_all('tr')

    # first scan, see how many columns we need
    colcount = 0
    for r, row in enumerate(rows):
        cells = row.find_all(['td', 'th'], recursive=False)
        # count columns (including spanned).
        # add active rowspans from preceding rows
        # we *ignore* the colspan value on the last cell, to prevent
        # creating 'phantom' columns with no actual cells, only extended
        # colspans. This is achieved by hardcoding the last cell width as 1. 
        # a colspan of 0 means “fill until the end” but can really only apply
        # to the last cell; ignore it elsewhere. 
        colcount = max(
            colcount,
            sum(int(c.get('colspan', 1)) or 1 for c in cells[:-1]) + len(cells[-1:]) + len(rowspans))
        # update rowspan bookkeeping; 0 is a span to the bottom. 
        rowspans += [int(c.get('rowspan', 1)) or len(rows) - r for c in cells]
        rowspans = [s - 1 for s in rowspans if s > 1]

    # it doesn't matter if there are still rowspan numbers 'active'; no extra
    # rows to show in the table means the larger than 1 rowspan numbers in the
    # last table row are ignored.

    # build an empty matrix for all possible cells
    table = [[None] * colcount for row in rows]

    # fill matrix from row data
    rowspans =   # track pending rowspans, column number mapping to count
    for row, row_elem in enumerate(rows):
        span_offset = 0  # how many columns are skipped due to row and colspans 
        for col, cell in enumerate(row_elem.find_all(['td', 'th'], recursive=False)):
            # adjust for preceding row and colspans
            col += span_offset
            while rowspans.get(col, 0):
                span_offset += 1
                col += 1

            # fill table data
            rowspan = rowspans[col] = int(cell.get('rowspan', 1)) or len(rows) - row
            colspan = int(cell.get('colspan', 1)) or colcount - col
            # next column is offset by the colspan
            span_offset += colspan - 1
            value = cell.get_text()
            for drow, dcol in product(range(rowspan), range(colspan)):
                try:
                    table[row + drow][col + dcol] = value
                    rowspans[col + dcol] = rowspan
                except IndexError:
                    # rowspan or colspan outside the confines of the table
                    pass

        # update rowspan bookkeeping
        rowspans = c: s - 1 for c, s in rowspans.items() if s > 1

    return table

这会正确解析您的示例表:

>>> from pprint import pprint
>>> pprint(table_to_2d(soup.table), width=30)
[['1', '2', '5'],
 ['3', '4', '4'],
 ['3', '6', '7']]

并处理您的其他示例;第一张表:

>>> table1 = BeautifulSoup('''
... <table border="1">
...   <tr>
...     <th>A</th>
...     <th>B</th>
...   </tr>
...   <tr>
...     <td rowspan="2">C</td>
...     <td rowspan="1">D</td>
...   </tr>
...   <tr>
...     <td>E</td>
...     <td>F</td>
...   </tr>
...   <tr>
...     <td>G</td>
...     <td>H</td>
...   </tr>
... </table>''', 'html.parser')
>>> pprint(table_to_2d(table1.table), width=30)
[['A', 'B', None],
 ['C', 'D', None],
 ['C', 'E', 'F'],
 ['G', 'H', None]]

第二个:

>>> table2 = BeautifulSoup('''
... <table border="1">
...   <tr>
...     <th>A</th>
...     <th>B</th>
...   </tr>
...   <tr>
...     <td rowspan="2">C</td>
...     <td rowspan="2">D</td>
...   </tr>
...   <tr>
...     <td>E</td>
...     <td>F</td>
...   </tr>
...   <tr>
...     <td>G</td>
...     <td>H</td>
...   </tr>
... </table>
... ''', 'html.parser')
>>> pprint(table_to_2d(table2.table), width=30)
[['A', 'B', None, None],
 ['C', 'D', None, None],
 ['C', 'D', 'E', 'F'],
 ['G', 'H', None, None]]

最后但同样重要的是,代码正确处理超出实际表的跨度和"0" 跨度(延伸到末端),如下例所示:

<table border="1">
  <tr>
    <td rowspan="3">A</td>
    <td rowspan="0">B</td>
    <td>C</td>
    <td colspan="2">D</td>
  </tr>
  <tr>
    <td colspan="0">E</td>
  </tr>
</table>

有两行每行 4 个单元格,即使 rowspan 和 colspan 值会让您相信可能有 3 和 5:

+---+---+---+---+
|   |   | C | D |
| A | B +---+---+
|   |   |   E   |
+---+---+-------+

像浏览器一样处理这种跨越;它们被忽略,并且 0 跨度扩展到剩余的行或列:

>>> span_demo = BeautifulSoup('''
... <table border="1">
...   <tr>
...     <td rowspan="3">A</td>
...     <td rowspan="0">B</td>
...     <td>C</td>
...     <td colspan="2">D</td>
...   </tr>
...   <tr>
...     <td colspan="0">E</td>
...   </tr>
... </table>''', 'html.parser')
>>> pprint(table_to_2d(span_demo.table), width=30)
[['A', 'B', 'C', 'D'],
 ['A', 'B', 'E', 'E']]

【讨论】:

我发现这有一个错误。 row_elem.select('&gt; td,&gt; th') 不遵循列的顺序。相反,它先选择 td,然后再选择 th。如果一行有 td 和 th,而 th 在 td 前面,则顺序错误。 确实如此;该 CSS 规则需要更新。我会尽快找到一些时间来解决这个问题。 @Joshua:我用 .find_all() 调用替换了那些 CSS 选择器。 仅供参考 &gt;td,&gt; th 不是有效的 CSS 选择器。如果您习惯了旧的 BeautifulSoup select(4.7 之前),那么您过去可能已经摆脱了这样做。您可以使用 :scope &gt; :is(td, th) 之类的东西,这将使您获得相同的行为。新的实现旨在遵循 CSS 规范。 @facelessuser:是的,新的 CSS 引擎是一个巨大的改进。我将保留代码原样,因为它没有损坏,并且可以在 BeautifulSoup 版本 【参考方案2】:

需要注意的是,Martijn Pieters 解决方案不考虑同时具有 rowspan 和 colspan 属性的单元格的情况。 例如

<table border="1">
    <tr>
        <td rowspan="3" colspan="3">A</td>
        <td>B</td>
        <td>C</td>
        <td>D</td>
    </tr>
    <tr>
        <td colspan="3">E</td>
    </tr>
    <tr>
        <td colspan="1">E</td>
        <td>C</td>
        <td>C</td>
    </tr>
    <tr>
        <td colspan="1">E</td>
        <td>C</td>
        <td>C</td>
        <td>C</td>
        <td>C</td>
        <td>C</td>
    </tr>
</table>

此表呈现给

+-----------+---+---+---+
| A         | B | C | D |
|           +---+---+---+
|           | E         |
|           +---+---+---+
|           | E | C | C |
+---+---+---+---+---+---+
| E | C | C | C | C | C |
+---+---+---+---+---+---+

但是如果我们应用我们得到的函数

[['A', 'A', 'A', 'B', 'C', 'D'],
 ['A', 'E', 'E', 'E', None, None],
 ['A', 'E', 'C', 'C', None, None],
 ['E', 'C', 'C', 'C', 'C', 'C']]

可能存在一些边缘情况,但将行跨度簿记扩展到行跨度和列跨度的product 中的单元格,即

   for drow, dcol in product(range(rowspan), range(colspan)):
            try:
                table[row + drow][col + dcol] = value
                rowspans[col + dcol] = rowspan
            except IndexError:
                # rowspan or colspan outside the confines of the table
                pass

似乎在这个线程中的例子上工作,对于上面的表格它会输出

[['A', 'A', 'A', 'B', 'C', 'D'],
 ['A', 'A', 'A', 'E', 'E', 'E'],
 ['A', 'A', 'A', 'E', 'C', 'C'],
 ['E', 'C', 'C', 'C', 'C', 'C']]

【讨论】:

不,我关注的是 OP 必须处理的特定配置。一个完全通用的解决方案需要更多的工作,因为 HTML 表格是复杂的野兽。例如,我也不处理 theadtfootcolgroup 元素。【参考方案3】:

使用通常的遍历方法,只需将解析器类型更改为lxml即可。

soup = BeautifulSoup(resp.text, "lxml")

现在用通常的方式解析它。

【讨论】:

以上是关于如何用rowspan和colspan解析表的主要内容,如果未能解决你的问题,请参考以下文章

html表单元素的colspan和rowspan

colSpan 和 row Span 如何添加到材料表 Header Angular 7?

材料表标题React中的Col span和Row span

怎么动态的rowspan合并行

table合并单元格colspan和rowspan .

减少引导表宽度,colspan 不起作用