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