DEV Community

Cover image for Hoe Ik Rowspans en Colspans Verwerk bij Tabelextractie
circobit
circobit

Posted on

Hoe Ik Rowspans en Colspans Verwerk bij Tabelextractie

Tabellen met samengevoegde cellen zijn overal. Wikipedia gebruikt ze. Sportsitesstatistieken gebruiken ze. Financiële rapporten gebruiken ze.

En ze breken vrijwel elke tabelscraper die ik heb gezien.

Het probleem: wanneer je door <tr>- en <td>-elementen itereert, krijg je wat er in de HTML staat—niet wat er visueel getoond wordt. Een cel met rowspan="3" verschijnt één keer in de HTML maar beslaat drie rijen in de output.

De naïeve aanpak faalt stilletjes. Je krijgt verkeerd uitgelijnde kolommen, ontbrekende data en exports die er totaal niet uitzien als de originele tabel.

Zo los je het op.

Het Probleem: HTML Komt Niet Overeen met het Raster

Bekijk deze eenvoudige tabel met een samengevoegde cel:

<table>
  <tr>
    <td rowspan="2">A</td>
    <td>B</td>
  </tr>
  <tr>
    <td>C</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Visuele weergave:

| A | B |
|   | C |
Enter fullscreen mode Exit fullscreen mode

Maar als je naïef cellen extraheert door rijen te doorlopen:

// Verkeerde aanpak
table.querySelectorAll('tr').forEach(row => {
  const cells = row.querySelectorAll('td');
  console.log(cells.map(c => c.textContent));
});
// Output: ["A", "B"], ["C"]
// Verwacht: ["A", "B"], ["A", "C"]
Enter fullscreen mode Exit fullscreen mode

Rij 2 mist de "A" die er visueel overheen spant.

De Oplossing: Bouw een Matrix

De juiste aanpak is om een 2D-raster vooraf te vullen volgens de span-attributen.

function extractTableMatrix(table) {
  const rows = Array.from(table.rows);
  const grid = [];

  rows.forEach((rowEl, rowIndex) => {
    if (!grid[rowIndex]) grid[rowIndex] = [];
    const gridRow = grid[rowIndex];

    let colIndex = 0;

    // Sla kolommen over die al bezet zijn door rowspans van eerdere rijen
    while (gridRow[colIndex] !== undefined) {
      colIndex++;
    }

    const cells = Array.from(rowEl.cells);

    cells.forEach((cell) => {
      // Ga voorbij bezette cellen
      while (gridRow[colIndex] !== undefined) {
        colIndex++;
      }

      const text = cell.textContent.trim();
      const rowSpan = parseInt(cell.rowSpan, 10) || 1;
      const colSpan = parseInt(cell.colSpan, 10) || 1;

      // Vul het blok dat deze cel beslaat
      for (let r = 0; r < rowSpan; r++) {
        const targetRowIndex = rowIndex + r;
        if (!grid[targetRowIndex]) grid[targetRowIndex] = [];

        for (let c = 0; c < colSpan; c++) {
          const targetColIndex = colIndex + c;
          if (grid[targetRowIndex][targetColIndex] === undefined) {
            grid[targetRowIndex][targetColIndex] = text;
          }
        }
      }

      colIndex += colSpan;
    });
  });

  return grid;
}
Enter fullscreen mode Exit fullscreen mode

Nu komt de output overeen met de visuele tabel:

const matrix = extractTableMatrix(table);
// [["A", "B"], ["A", "C"]]
Enter fullscreen mode Exit fullscreen mode

Waarom Dit Ertoe Doet: Praktijkvoorbeelden

Wikipedia-landentabellen

Wikipedia maakt veelvuldig gebruik van samengevoegde cellen. Een tabel die landen per regio opsomt kan er zo uitzien:

<tr>
  <td rowspan="5">Europa</td>
  <td>Frankrijk</td>
</tr>
<tr>
  <td>Duitsland</td>
</tr>
<!-- ... -->
Enter fullscreen mode Exit fullscreen mode

Zonder juiste verwerking verlies je de "Europa"-koppeling voor rij 2-5.

Sportstatistiekensites

Sites zoals FBREF gebruiken zowel rowspan ALS colspan. Spelersstatistiekentabellen bevatten:

  • Gegroepeerde headers met colspan (Speeltijd → WS | Basis | Min)
  • Samenvattingsrijen met rowspan die over spelersdetails heen spannen

Een naïeve scraper exporteert onzin. Een matrixgebaseerde aanpak behoudt de structuur.

Financiële Rapporten

Kwartaalrapporten bevatten vaak:

<td colspan="4">2024</td>
<!-- Dan: K1, K2, K3, K4 -->
Enter fullscreen mode Exit fullscreen mode

De jaarkop moet doorwerken naar alle vier kwartaalkolommen.

Randgevallen om te Verwerken

Lege Cellen in het Midden

Soms zijn cellen echt leeg. Verwar dit niet met "nog niet ingevuld door een span."

// Normaliseer naar hetzelfde aantal kolommen
const maxCols = grid.reduce((max, row) => 
  Math.max(max, row ? row.length : 0), 0
);

const normalized = grid.map(row => {
  const r = row || [];
  const out = new Array(maxCols);
  for (let i = 0; i < maxCols; i++) {
    out[i] = r[i] != null ? r[i] : "";
  }
  return out;
});
Enter fullscreen mode Exit fullscreen mode

Geneste Tabellen

Tabellen binnen cellen veroorzaken ruis. Meestal wil je de tekstinhoud van de geneste tabel, niet een recursieve extractie.

function extractCellText(cell) {
  const clone = cell.cloneNode(true);

  // Verwijder geneste tabellen, scripts, stijlen
  clone.querySelectorAll("table, script, style, noscript")
    .forEach(el => el.remove());

  return clone.textContent.replace(/\s+/g, " ").trim();
}
Enter fullscreen mode Exit fullscreen mode

Colspan + Rowspan Gecombineerd

Het algoritme verwerkt dit automatisch—je vult gewoon een grotere rechthoek in het raster.

<td rowspan="2" colspan="3">Grote Cel</td>
Enter fullscreen mode Exit fullscreen mode

Vult een 2×3 blok met dezelfde waarde.

Wanneer een Tool Gebruiken

Dit helemaal zelf schrijven is leerzaam maar tijdrovend. Voor productiegebruik, overweeg:

  • HTML Table Exporter — Browserextensie die deze randgevallen automatisch afhandelt
  • Pandas read_html() — Werkt maar heeft moeite met complexe geneste tabellen
  • Beautiful Soup — Vereist handmatige span-verwerking

Het Volledige Algoritme

Hier is de productieversie met alle randgevallen afgehandeld:

function extractCellText(cell) {
  if (!cell) return "";
  const clone = cell.cloneNode(true);

  // Verwijder elementen die niet-zichtbare content bevatten
  const invisibleSelectors = "style, script, noscript, template, link";
  clone.querySelectorAll(invisibleSelectors).forEach(el => el.remove());

  const text = clone.textContent || "";
  return text.replace(/\s+/g, " ").trim();
}

function extractTableMatrix(table) {
  const rows = Array.from(table.rows);
  const grid = [];

  rows.forEach((rowEl, rowIndex) => {
    if (!grid[rowIndex]) grid[rowIndex] = [];
    const gridRow = grid[rowIndex];

    let colIndex = 0;

    while (gridRow[colIndex] !== undefined) {
      colIndex++;
    }

    const cells = Array.from(rowEl.cells);

    cells.forEach((cell) => {
      while (gridRow[colIndex] !== undefined) {
        colIndex++;
      }

      const text = extractCellText(cell);
      const rowSpan = parseInt(cell.rowSpan, 10) || 1;
      const colSpan = parseInt(cell.colSpan, 10) || 1;

      for (let r = 0; r < rowSpan; r++) {
        const targetRowIndex = rowIndex + r;
        if (!grid[targetRowIndex]) grid[targetRowIndex] = [];
        const targetRow = grid[targetRowIndex];

        for (let c = 0; c < colSpan; c++) {
          const targetColIndex = colIndex + c;
          if (targetRow[targetColIndex] === undefined) {
            targetRow[targetColIndex] = text;
          }
        }
      }

      colIndex += colSpan;
    });
  });

  // Normaliseer alle rijen naar dezelfde lengte
  const maxCols = grid.reduce((max, row) => 
    Math.max(max, row ? row.length : 0), 0
  );

  return grid.map(row => {
    const r = row || [];
    const out = new Array(maxCols);
    for (let i = 0; i < maxCols; i++) {
      out[i] = r[i] != null ? r[i] : "";
    }
    return out;
  });
}
Enter fullscreen mode Exit fullscreen mode

Samenvatting

Aanpak Verwerkt Spans Productierijp
Naïeve iteratie
Matrix met spans
Pandas read_html Gedeeltelijk Hangt af
Browserextensie

Het kernidee: denk aan tabellen als rasters, niet als geneste HTML-elementen. Bouw eerst het raster, exporteer daarna.

Lees voor meer over het exporteren van Wikipedia's bijzonder lastige tabellen onze gids over de beste Chrome-extensies voor het exporteren van tabellen.


Wil je tabellen extraheren zonder code te schrijven? Meer informatie op gauchogrid.com/nl/html-table-exporter of probeer het gratis in de Chrome Web Store.

Top comments (0)