Saltar al contenido

Cómo analizar una tabla con rowspan y colspan

Solución:

No puedes simplemente contar td o th células, no. Tendrá que hacer un escaneo a través de la tabla para obtener el número de columnas en cada fila, agregando a ese recuento los espacios de filas activos de una fila anterior.

En un escenario diferente, analizando una tabla con intervalos de filas, realicé un seguimiento de los recuentos de intervalos por número de columna para asegurarme de que los datos de diferentes celdas terminaran en la columna correcta. Aquí se puede utilizar una técnica similar.

Columnas de primer recuento; conserve solo el número más alto. Mantenga una lista de números de intervalo de filas de 2 o más y reste 1 de cada uno por cada fila de columnas que procese. De esa manera, sabrá cuántas columnas “adicionales” hay en cada fila. Tome el recuento de columnas más alto para construir su matriz de salida.

A continuación, vuelva a recorrer las filas y celdas y, esta vez, realice un seguimiento de los intervalos de filas en un mapeo de diccionario desde el número de columna hasta el recuento activo. Nuevamente, pase cualquier cosa con un valor de 2 o más a la siguiente fila. Luego, cambie los números de columna para tener en cuenta los espacios de filas que estén activos; el primero td en una fila en realidad sería el segundo si hubiera un espacio de filas activo en la columna 0, etc.

Su código copia el valor de las columnas y filas distribuidas en la salida repetidamente; Logré lo mismo creando un bucle sobre el colspan y rowspan números de una celda determinada (cada uno por defecto es 1) para copiar el valor varias veces. Estoy ignorando las celdas superpuestas; las especificaciones de la tabla HTML establecen que las celdas superpuestas son un error y que el agente de usuario debe resolver los conflictos. En el siguiente código, colspan triunfa sobre las celdas de rowpan.

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

Esto analiza su tabla de muestra correctamente:

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

y maneja sus otros ejemplos; primera mesa:

>>> 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]]

Y el segundo:

>>> 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]]

Por último, pero no menos importante, el código maneja correctamente los intervalos que se extienden más allá de la tabla real, y "0" tramos (extendiéndose hasta los extremos), como en el siguiente ejemplo:

<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>

Hay dos filas de 4 celdas, aunque los valores de rowspan y colspan le harían creer que podría haber 3 y 5:

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

Tal sobreexpansión se maneja como lo haría el navegador; se ignoran y los intervalos de 0 se extienden a las filas o columnas restantes:

>>> 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']]

Es importante tener en cuenta que la solución de Martijn Pieters no tiene en cuenta el caso de las celdas que tienen el atributo rowpan y colspan simultáneamente. P.ej

<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>

Esta tabla se renderiza a

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

pero si aplicamos la función obtenemos

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

Puede haber algunos casos extremos, pero la extensión de la contabilidad del intervalo de filas a las celdas del product de rowpan y colspan, es decir

   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

parece funcionar en ejemplos en este hilo, y para la tabla anterior se generará

[['A', 'A', 'A', 'B', 'C', 'D'],
 ['A', 'A', 'A', 'E', 'E', 'E'],
 ['A', 'A', 'A', 'E', 'C', 'C'],
 ['E', 'C', 'C', 'C', 'C', 'C']]
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *