Cómo limpiar tablas HTML con PHP (guía paso a paso)
En muchos proyectos web nos encontramos con tablas HTML llenas de atributos innecesarios como class, id o style. Además, a veces incluyen columnas que no queremos mostrar, como la clásica columna “Acciones” en sistemas de inventario o administración.
Con este tutorial se aprendera a limpiar tablas HTML con PHP de una manera sencilla.
Cómo limpiar tablas HTML con PHP (guía paso a paso)
En este tutorial aprenderás a:
- Eliminar clases, estilos e identificadores de tablas HTML.
- Quitar columnas específicas (como “Acciones”).
- Generar un archivo limpio en HTML o TXT desde el navegador.

¿Por qué limpiar una tabla HTML?
Una tabla HTML puede venir de un exportador automático (ej. sistemas de ventas, inventarios, CMS) y contener:
- Estilos en línea
(style="...")
que rompen el diseño responsive. - Clases e identificadores heredados de frameworks como Bootstrap o DataTables.
- Columnas innecesarias (acciones, botones, iconos).
Limpiarla ayuda a:
- Mejorar la accesibilidad.
- Optimizar para SEO (tablas más ligeras, sin basura de código).
- Preparar datos para exportación a Excel, CSV o TXT.
Script PHP para limpiar tablas HTML
Aquí un ejemplo funcional en PHP que procesa un archivo con una tabla y genera una versión limpia:
<?php
// limpiar_tabla_web.php
// Versión robusta: sube archivo, limpia tablas, guarda HTML y TXT en ./downloads/ y muestra enlaces.
// Mostrar errores para depuración (quítalo en producción)
ini_set('display_errors', 1);
error_reporting(E_ALL);
/**
* Normaliza texto: decodifica entidades, colapsa espacios y trim.
*/
function normalize_text(string $s): string {
$s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$s = preg_replace('/[\r\n\t]+/', ' ', $s);
$s = preg_replace('/\s+/', ' ', $s);
return trim($s);
}
/**
* Construye URL absoluta al recurso en downloads/
*/
function build_download_url($filename) {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
// dirname puede devolver '\' en Windows; normalizamos
$dir = rtrim(str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])), '/');
return "{$scheme}://{$host}{$dir}/downloads/{$filename}";
}
$messages = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['archivo'])) {
// Validar subida
$fileErr = $_FILES['archivo']['error'] ?? UPLOAD_ERR_NO_FILE;
if ($fileErr !== UPLOAD_ERR_OK) {
$messages[] = "Error en la subida: código $fileErr. Verifica upload_max_filesize y post_max_size en php.ini.";
} else {
$tmpName = $_FILES['archivo']['tmp_name'];
if (!is_uploaded_file($tmpName)) {
$messages[] = "Archivo no válido (is_uploaded_file falló).";
} else {
$removeActions = isset($_POST['remove_actions']);
$raw = file_get_contents($tmpName);
if ($raw === false) {
$messages[] = "No se pudo leer el archivo temporal.";
} else {
// Preparar DOM
libxml_use_internal_errors(true);
$dom = new DOMDocument('1.0', 'UTF-8');
$raw_html = mb_convert_encoding($raw, 'HTML-ENTITIES', 'UTF-8');
// Flags opcionales (si no existen en la versión PHP, loadHTML igualmente funcionará)
$flags = 0;
if (defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD')) {
$flags = LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD;
}
$loaded = @$dom->loadHTML('<?xml encoding="utf-8" ?>' . $raw_html, $flags);
$libErrors = libxml_get_errors();
libxml_clear_errors();
if (!$loaded) {
$messages[] = "DOMDocument::loadHTML falló. Errores: " . print_r($libErrors, true);
} else {
$xpath = new DOMXPath($dom);
$tables = $xpath->query('//table');
if ($tables->length === 0) {
$messages[] = "No se encontró ninguna <table> en el archivo.";
} else {
// Preparar carpeta downloads/
$downloadsDir = __DIR__ . '/downloads';
if (!is_dir($downloadsDir)) {
if (!mkdir($downloadsDir, 0755, true)) {
$messages[] = "No se pudo crear la carpeta downloads/. Revisa permisos.";
}
}
if (!is_writable($downloadsDir)) {
$messages[] = "La carpeta downloads/ no es escribible. Ajusta permisos (chmod).";
}
$htmlOutputs = [];
$tsvLines = [];
// Procesar cada tabla
foreach ($tables as $tableIndex => $table) {
// Recolectar TRs que pertenezcan directamente a esta tabla (evita tablas anidadas)
$trs = [];
foreach ($table->getElementsByTagName('tr') as $tr) {
$cur = $tr;
$inSameTable = false;
while ($cur = $cur->parentNode) {
if ($cur === $table) { $inSameTable = true; break; }
if (strtolower($cur->nodeName) === 'table') break;
}
if (!$inSameTable) continue;
// Obtener celdas directas (td/th)
$cells = [];
foreach ($tr->childNodes as $child) {
if ($child->nodeType === XML_ELEMENT_NODE) {
$nm = strtolower($child->nodeName);
if ($nm === 'td' || $nm === 'th') $cells[] = $child;
}
}
// Fallback si no hay celdas directas
if (count($cells) === 0) {
foreach ($tr->getElementsByTagName('th') as $c) $cells[] = $c;
foreach ($tr->getElementsByTagName('td') as $c) $cells[] = $c;
}
if (count($cells) > 0) $trs[] = $cells;
}
if (count($trs) === 0) continue; // tabla sin filas útiles
// Detectar fila cabecera (thead o primer tr con <th>)
$headerRow = null;
// preferir THEAD
$thead = $table->getElementsByTagName('thead');
if ($thead->length > 0) {
foreach ($thead->item(0)->getElementsByTagName('tr') as $trCandidate) {
$headerRow = $trCandidate;
break;
}
}
if (!$headerRow) {
// buscar primer TR que contenga <th>
foreach ($table->getElementsByTagName('tr') as $trCandidate) {
foreach ($trCandidate->childNodes as $c) {
if ($c->nodeType === XML_ELEMENT_NODE && strtolower($c->nodeName) === 'th') {
$headerRow = $trCandidate;
break 2;
}
}
}
}
// Si no hay headerRow, no hay eliminación por nombre de columna
$removeIndexes = [];
if ($removeActions && $headerRow) {
// identificar índices de "Acciones" (case-insensitive)
$ci = 0;
foreach ($headerRow->childNodes as $c) {
if ($c->nodeType !== XML_ELEMENT_NODE) continue;
$nm = strtolower($c->nodeName);
if ($nm !== 'th' && $nm !== 'td') continue;
$txt = normalize_text($c->textContent);
if (stripos($txt, 'acciones') !== false) {
$removeIndexes[] = $ci;
}
$ci++;
}
}
// Reconstruir tabla limpia
$out = new DOMDocument('1.0', 'UTF-8');
$out->formatOutput = false;
$newTable = $out->createElement('table');
$out->appendChild($newTable);
foreach ($trs as $rowCells) {
$newTr = $out->createElement('tr');
$rowTextParts = [];
$colIndex = 0;
foreach ($rowCells as $cell) {
if (in_array($colIndex, $removeIndexes, true)) { $colIndex++; continue; }
$tag = (strtolower($cell->nodeName) === 'th') ? 'th' : 'td';
$newCell = $out->createElement($tag);
$text = normalize_text($cell->textContent);
$newCell->appendChild($out->createTextNode($text));
$newTr->appendChild($newCell);
$rowTextParts[] = $text;
$colIndex++;
}
if ($newTr->hasChildNodes()) {
$newTable->appendChild($newTr);
if (count($rowTextParts) > 0) $tsvLines[] = implode("\t", $rowTextParts);
}
}
$htmlOutputs[] = $out->saveHTML($newTable);
} // end foreach tables
if (count($htmlOutputs) === 0) {
$messages[] = "Se encontraron tablas pero ninguna produjo salida limpia.";
} else {
// Guardar archivos en downloads/
$baseHtmlName = 'tabla_limpia_' . time() . '_' . bin2hex(random_bytes(4)) . '.html';
$baseTxtName = 'tabla_limpia_' . time() . '_' . bin2hex(random_bytes(4)) . '.txt';
$htmlPath = $downloadsDir . '/' . $baseHtmlName;
$txtPath = $downloadsDir . '/' . $baseTxtName;
$fullHtmlContent = implode("\n\n", $htmlOutputs);
$okHtml = @file_put_contents($htmlPath, $fullHtmlContent);
$okTxt = @file_put_contents($txtPath, implode(PHP_EOL, $tsvLines));
if ($okHtml === false || $okTxt === false) {
$messages[] = "Error al guardar archivos en downloads/. Revisa permisos. file_put_contents devolvió false.";
} else {
$downloadHtmlUrl = build_download_url($baseHtmlName);
$downloadTxtUrl = build_download_url($baseTxtName);
$messages[] = "Ficheros generados correctamente:";
$messages[] = "<a href=\"{$downloadHtmlUrl}\" target=\"_blank\">Descargar HTML limpio</a>";
$messages[] = "<a href=\"{$downloadTxtUrl}\" target=\"_blank\">Descargar TXT (TSV)</a>";
// También mostrar vista previa limpia
$preview = htmlspecialchars($fullHtmlContent, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}
}
}
}
}
}
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Limpiar tabla - Web</title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Arial;max-width:900px;margin:20px auto;padding:10px;}
.msg{background:#f7f7f7;border-left:4px solid #888;padding:8px;margin:8px 0;}
.err{border-left-color:#c33;background:#fff0f0;}
.ok{border-left-color:#2a8;background:#f0fff6;}
pre.preview{background:#111;color:#dcdcdc;padding:10px;overflow:auto;white-space:pre-wrap;}
</style>
</head>
<body>
<h2>Limpiar tabla (web)</h2>
<form method="post" enctype="multipart/form-data">
<label>Archivo (HTML / TXT que contenga la tabla):</label><br>
<input type="file" name="archivo" accept=".html,.htm,.txt" required><br><br>
<label><input type="checkbox" name="remove_actions"> Eliminar columna "Acciones"</label><br><br>
<button type="submit">Procesar y generar ficheros</button>
</form>
<hr>
<?php
if (!empty($messages)) {
foreach ($messages as $m) {
$isLink = (strpos($m, '<a ') !== false);
$cls = $isLink ? 'ok' : 'msg';
echo "<div class=\"{$cls}\">";
echo $isLink ? $m : htmlspecialchars($m, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo "</div>";
}
}
if (!empty($libErrors)) {
echo "<div class=\"msg err\"><strong>Warnings/parsing:</strong><pre>" . htmlspecialchars(print_r($libErrors, true)) . "</pre></div>";
}
if (!empty($preview)) {
echo "<h3>Vista previa (HTML limpio):</h3>";
echo "<div style='border:1px solid #ddd;padding:8px;margin-bottom:12px;'>";
// mostramos el HTML limpio interpretado
echo $fullHtmlContent;
echo "</div>";
echo "<h4>HTML como texto (para copiar/pegar):</h4>";
echo "<pre class='preview'>" . $preview . "</pre>";
}
?>
<hr>
<small>Si no se crean los ficheros:
<ul>
<li>Revisa permisos: la carpeta <code>downloads/</code> debe ser escribible por el proceso web (chmod 755 o 775; si nada funciona prueba 0777 temporalmente).</li>
<li>Revisa <code>upload_max_filesize</code> y <code>post_max_size</code> en <code>php.ini</code> si la subida falla.</li>
<li>Revisa logs de PHP/Apache para errores (error_log).</li>
</ul>
</small>
</body>
</html>
Este script:
- Carga la tabla original.
- Elimina atributos (class, style, id).
- Convierte elementos no permitidos (botones, imágenes) en texto.
- Devuelve un archivo descargable limpio.
Cómo usarlo en el navegador
- Guarda el código como limpiar_tabla.php.
- Súbelo a tu servidor local (ej. XAMPP, Laragon) o hosting.
- Crea un formulario HTML para subir el archivo con la tabla:
<form method="post" enctype="multipart/form-data" action="limpiar_tabla.php"> <input type="file" name="archivo" accept=".html,.txt" required> <label><input type="checkbox" name="remove_actions"> Eliminar columna "Acciones"</label> <button type="submit">Procesar</button> </form>
- Sube tu archivo con la tabla y descarga la versión limpia.
Ejemplo práctico: quitar atributos class id style en tabla HTML
Puedes usar un fichero con extension html o txt y dentro el contenido de una tabla con clases, ids, styles en linea, etc.
Esto vendria a ser la entrada:
<table id="example" class="table" style="width:100%">
<tr><th>Código</th><th>Producto</th><th>Acciones</th></tr>
<tr><td>001</td><td>Tela Azul</td><td><button>Ver</button></td></tr>
</table>
La salida despues de procesar la información
<table>
<tr><th>Código</th><th>Producto</th></tr>
<tr><td>001</td><td>Tela Azul</td></tr>
</table>
En resumen, este script nos da la opcion de cargar un fichero html o txt con una tabla que contiene cientos o miles de registros y el cual podra limpiar y dejarlo listo.