Cómo analizar la tabla con rowpan y colspan

Primero, he leído Analizar una tabla con rowpan y colspan . Incluso respondí la pregunta. Por favor, lea antes de marcar esto como duplicado.

A B
C D
E F
G H

Se rendirá como

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

Sin embargo, esto se renderizará así.

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

Mi código de la respuesta anterior solo puede analizar la tabla que tiene todas las columnas definidas en la primera fila.

 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(''' 
125
34
67
''', 'html.parser') print(table_to_2d(soup.table))

Mi pregunta es cómo analizar una tabla en una matriz 2D que representa exactamente cómo se procesa en el navegador. O alguien puede explicar cómo el navegador hace que la tabla también esté bien.

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

En un escenario diferente, analizando una tabla con filas de filas, realicé un seguimiento de los conteos de filas por número de columna para garantizar que los datos de diferentes celdas terminaran en la columna correcta. Una técnica similar puede ser utilizada aquí.

Primera cuenta de columnas; mantener sólo el número más alto. Mantenga una lista de números de filas de 2 o más y reste 1 de cada fila de columnas que procese. De esa manera usted sabe cuántas columnas ‘extra’ hay en cada fila. Tome la cuenta más alta de columnas para construir su matriz de salida.

A continuación, recorra las filas y las celdas de nuevo, y esta vez haga un seguimiento de los espacios de las filas en una asignación de diccionario desde el número de columna hasta el recuento activo. Nuevamente, cuídese sobre cualquier cosa con un valor de 2 o superior a la siguiente fila. A continuación, cambie los números de columna para tener en cuenta cualquier fila de filas que esté activa; la primera td en una fila sería realmente la segunda si hubiera un intervalo de filas activo en la columna 0, etc.

Su código copia el valor de las columnas y filas divididas en la salida repetidamente; Logré lo mismo creando un bucle sobre los números de colspan y rowspan de una celda determinada (cada valor predeterminado en 1) para copiar el valor varias veces. Estoy ignorando las células superpuestas; Las especificaciones de la tabla HTML indican que las celdas superpuestas son un error y que el agente de usuario es el responsable de 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 tus otros ejemplos; primera mesa

 >>> table1 = BeautifulSoup(''' ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ... 
AB
CD
EF
GH
''', '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(''' ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ... 
AB
CD
EF
GH
... ''', '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 tramos que se extienden más allá de la tabla real y los tramos "0" (que se extienden hasta los extremos), como en el siguiente ejemplo:

 
A B C D
E

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

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

Este exceso de scope 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(''' ...  ...  ...  ...  ...  ...  ...  ...  ...  ...  ... 
ABCD
E
''', '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 atributos rowpan y colspan simultáneamente. P.ej

 
A B C D
E
E C C
E C C C C C

Esta tabla rinde 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 de borde, pero extender la contabilidad de filas a las celdas en el product de las filas 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 de arriba se mostrará

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

Utilice el método habitual de desplazamiento, simplemente cambie el tipo de analizador a lxml.

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

Ahora ve la forma habitual de analizarlo.