Herramienta para construir índices para un buscador interno

Un marco de herramientas para mantener el sitio web

En el tema de cómo se hace un buscador interno, en el capítulo que habla sobre construir índices para el buscador expuse una herramienta para hacer un índice de palabras basado en los documentos a indexar. En ese enlace se explica cómo se usa pero ahora voy a dar un visión general de cómo está hecho. La estructura esta ideada para su reutilización con el objeto de presentar otras herramientas. La idea general es hacer aplicaciones para realizar tareas de mantenimiento del sitio, mediante PHP ejecutándose en nuestro servidor localhost y usando formularios que se presentan en el navegador como interfaz para manejar la herramienta. De hecho he construido un marco de herramientas con algunas utilidades:

menú de herramientas

Hay cosas como un analizador XML hecho a medida que me sirve de base para un verificador de etiquetas XML/HTML o para otra herramienta que me revisa si hay vínculos rotos. Algunas como Telnet y Explorador PHP son las mismas que ya expuse aunque adaptadas a este nuevo marco. El marco de trabajo funciona usando una serie de módulos enlazados para aprovechar integralmente los recursos. Por ejemplo, el visor de ficheros en una simple aplicación que permite abrir un documento de texto y editarlo. Aunque lo uso sobre todo para visualizar los archivos de texto, se basa en un módulo llamado ficheros.php. Este módulo contiene funciones generales para manejo de ficheros: extraer el contenido, guardarlo, codificarlo, etc. Así estas funciones se aprovechan en otros módulos. Esta es una lista de algunas tareas que puedo realizar en localhost con un conjunto de herramientas como éste:

  • analizador-xml: Realmente es un parseador XML propio que me sirve de base para otras acciones como verificaciones, validaciones y modificaciones de partes de un documento. Tiene también algunas utilidades que simulan el manejo del DOM con funciones propias, pudiendo obtener elementos, atributos, editarlos, insertar nuevos como hermanos, hijos, etc. Estas utilidades sirven en otras herramientas para obtener y/o editar elementos de un HTML.
  • verificador-xhtml: Analizar la buena formación de los XML, verificando que los elementos estén bien anidados, correctamente cerrados, atributos entrecomillados, etc., según las reglas XML. Bajo estas reglas puede analizar documentos HTML o PHP siempre que estén escritos con sintaxis XHTML (tal como los de este sitio).
  • vinculos-rotos: Revisar los vínculos en toda clase de elementos HTML que lleven atributos como href, src, data, action... y también los url() de CSS. Incluso con posibilidad de realizar modificaciones in-situ para ciertos errores o formatos predefinidos.
  • buscador: Búsqueda extensiva de cualquier texto en todo el sitio, en cualquier tipo de documento de texto (html, js, css,...), pudiendo usar expresiones regulares y filtros avanzados como eliminar partículas del lenguaje que no aportan concepto, buscar alguna palabra, todas, etc. En parte hace algo parecido al buscador de este sitio, pero además puede buscar texto en otros tipos de archivos como JS o CSS.
  • copias-seguridad: Hacer y restaurar copias de seguridad con repetición y anotación de versiones de copias, con lo que permitirá restaurar copias entre fechas. Realmente no uso esta herramienta para hacer copias de seguridad de los archivos de este sitio web en localhost (aunque serviría). La utilidad principal para crearla se basa en que cualquier otra de las herramientas de este marco que modifique los archivos tendrá la posibilidad de que se haga una copia de seguridad previa usando esta herramienta. En caso de error con alguna modificación de archivos siempre podemos recuperar de estas copias.
  • modificar-contenidos: Modificar contenidos específicos en documentos HTML una vez parseados con el analizador XML, pudiendo agregar literales de texto HTML a nodos existentes que pueden ser selecionados. Por ejemplo, la barra del pie de este sitio está realmente casi vacía, pero inicialmente no contenía ningún botón. El enlace "contacto" es un elemento <a> que fue agregado automáticamente con esta herramienta a los por entonces casi 80 documentos existentes.
  • tabla-contenidos: Crear tablas de contenidos extrayendo directamente de los elementos HTML. Un ejemplo de uso es para hacer las entradas de lista de los menús de índices como el de este documento. La herramienta usa el analizador XML buscando todos los encabezados que estén identificados y construyendo una lista <ol> con vínculos a esos encabezados. El literal HTML obtenido puede ser insertado de forma automática en un elemento de un documento o bien se muestra para copiarlo y pegarlo manualmente.
  • listador-rutas: Construir listas de rutas como por ejemplo, para actualizar el sitemap. Esta herramienta es una de uso común para el resto pues la mayor parte de las acciones se realizan iterando por la lista de documentos del sitio.
  • robots: Actualizar el archivo robots.txt definiendo que agentes de usuario, carpetas o archivos están permitidas o no (allow/disallow).
  • plantillas: Crear y usar plantillas de documentos HTML, CSS, PHP, etc. pudiendo incorporar campos particularizados para los elementos de metainformación del documento que se actualizan automáticamente (fecha), se predefinen (autor) o se modifican antes de crear la plantilla (título).
  • indices-web: Es la herramienta que se expone en este tema para construir índices de encabezados o bien índices de palabras para hacer un buscador interno.
  • reemplazar-texto: Es una herramienta de uso general para reemplazar texto usando expresiones regulares en todos los documentos de texto plano (html, php, js, etc.) del sitio.

El propósito inicial de este marco de herramientas era aprender PHP con una aplicación práctica. Pero finalmente se ha convertido en un apoyo importante para actualizar y gestionar los documentos en localhost, páginas que luego subo a este sitio. La ventaja de este conjunto integrado es que en cualquier momento puedo incorporar otra herramienta beneficiándose de los módulos generales que comparte. Mencionar esta aplicación aquí es para hacer entender que no es buena idea construir herramientas aisladas, sin conexión entre ellas. El beneficio de reutilizar código es uno de los conceptos principales a tener en cuenta a la hora de hacer aplicaciones informáticas.

No puedo exponer públicamente todo ese marco pues muchas partes aún están en evolución, dado que voy mejorándolo a medida que aprendo cosas nuevas. Pero sí puedo exponer por el momento algunas herramientas aisladas. Esto de todas formas me lleva un doble trabajo, pues primero las codifico para el marco y luego he de aislarlas del mismo para publicarlas aquí (tras lo cual se convierten en herramientas no integradas). Una de ellas es la que ya expuse para construir índices web y que explicaré un poco más a continuación.

Interfaz de la herramienta para crear índices

Esta herramienta usa una interfaz que es un documento index.php para gestionar realmente el proceso con un módulo separado. Podemos denominarlo módulo de acciones que para esta herramienta se llama indexa.php. El módulo lo veremos después. La interfaz tiene esta apariencia visual:

lista de rutas

Se trata de una página PHP con pestañas, tal como las explico en contenedores de pestañas en cuerpos de tabla. En este caso nos sirve para separar el proceso de creación en pasos consecutivos. Dado que usaré esta estructura para otras herramientas, explicaré brevemente el código de esta interfaz index.php. He omitido algunas partes señaladas con puntos suspensivos para no alargarme en exceso, pues con el resto es suficiente para entenderlo. El documento PHP se inicia con el script para declarar variables, recoger controles de formulario y ejecutar las acciones de la herramienta:

<?php 
include_once("indexa.php");
//variables generales
$pestanya = "p-1";
$mensaje = "";
$accion = "";
//variables particulares de esta herramienta
$lista_rutas = array();
$desde = "/";
$extensiones = array("html","php");
$carpetas = array("no-publicable","res","ejemplos");
...
if (isset($_POST)){
    foreach($_POST as $campo => $valor){
        switch ($campo) {
            case "lista-rutas": 
                $lista_rutas = explode(SALTO, $valor); break;
            case "desde": $desde = $valor; break;
            case "extensiones": 
                $extensiones = explode(",", $valor); break;
            case "carpetas": 
                $carpetas = explode(",", $valor); break;
            ...
            case "accion": 
                $accion = $valor; break;
            case "pestanya": 
                $pestanya = $valor; break;
        }
    }
}
switch ($accion){
    case "listar-rutas":
        $met = ini_get("max_execution_time");
        set_time_limit(0);
        $arr_lista = listar_rutas($desde, $extensiones, $carpetas);
        $mensaje = $arr_lista["mensaje"];
        if (!$arr_lista["error"]) $lista_rutas = $arr_lista["lista"];   
        set_time_limit($met);
        break;
    case "indexar":
        ...
    case "arrays":
        ...
        break;
}
?>

Iniciamos el script incluyendo el módulo indexa.php que explicaremos más abajo. Luego declaramos las variables. Hay unas generales que son para el control de la interfaz: mensajes, pestaña activada y acción. Luego se declaran las variables particulares para esta herramienta. Por ejemplo, en $lista_rutas pondremos la lista de rutas construidas, con $desde recogemos el valor del campo de la carpeta desde donde iniciamos la lista de rutas, etcétera.

A continuación pasamos a revisar el POST recogiendo los campos recibidos que se asignan a las variables declaradas previamente. Veáse que no aplico ningún tipo de seguridad, como filtrar con htmlspecialchars(), pues esta aplicación sólo la voy a usar en localhost. Debe tener esto en cuenta por si le da otra utilidad. Los campos que son listas de valores se recogen como una lista de cadenas separadas por comas. Así luego aplicamos un explode() para meterlas en un array. El campo $accion nos traerá la correspondiente accion solicitada. El campo $pestanya nos servirá para devolver la interfaz con la pestaña que esté activada.

Por último si hay alguna acción solicitada se ejecutará en este switch final. Para esta herramientas hay tres acciones posibles que son las mismas que aparecen en las pestañas: listar rutas, indexarlas y construir los arrays de índices. Sólo he puesto la de listar rutas como ejemplo. PHP tiene una limitación en tiempo en la ejecución de un script para evitar colapsar el servidor. Por defecto suele venir con 30 segundos. Pero para la ejecución de estas herramientas en localhost hemos de saltarnos esta limitación. Por eso guardo la opción de configuración que exista con $met = ini_get("max_execution_time"). Luego pongo ese límite a cero con set_time_limit(0) que significará que no habrá límite de tiempo. Ejecutamos la acción, en este caso listar_rutas(), una función que se encuentra en el módulo indexa.php. Al finalizar volvemos a poner el límite de tiempo como estaba.

Uno de los puntos críticos al usar un servidor Apache montado como localhost con PHP es el relacionado con los tiempos de ejecución. Aparte del tiempo máximo de PHP también Apache server tiene el suyo (por defecto viene con 300 segundos). Estos tiempos se pueden configurar para que no limiten la repuesta de nuestras herramientas si fuera el caso de que la ejecución se demorase en exceso. Hay que tener en cuenta que no podemos permitir de ninguna forma que se corte un script sobre todo cuando está realizando modificaciones en los documentos.

Yo lo he resuelto con el marco de herramientas que expuse al principio. Se trata de un módulo llamado paginador.php que me permite controlar la ejecución en cortos lapsos de tiempo. Por ejemplo, si un módulo tiene que iterar por todos los documentos del sitio haciendo algo, configuro el módulo paginador para que haga un corte cada cierto número de segundos (15 por ejemplo). E incluso puedo forzar la detención en cualquier momento antes de ese límite. Obtendré entonces unos resultados parciales permitiéndome continuar la ejecución del resto de documentos a partir de ese corte. De esa forma puedo evitar las limitaciones de PHP y de Apache.

Sin embargo esta facilidad no está disponible en estas herramientas no integradas que estoy presentando. Como vemos, desactivamos la limitación de PHP totalmente, con lo que el script sigue ejecutándose hasta el final. Si el número de documentos es muy alto, el tiempo puede superar al que tenga Apache, con lo que se producirá un corte no deseado.

Tras esto pasamos a enviar el HTML. Su estructura se encuentra explicada en el vínculo que dije antes sobre contenedores de pestañas en cuerpos de tabla. Se observa en marrón el PHP que permite mostrar mensajes de error si los hubiera así como rellenar los valores de los campos a partir de las variables:

<!DOCTYPE html ...
<head>
    ...
    <script>
        window.onload = function(){
            var idth = "<?php echo $pestanya;?>";
            var th = document.getElementById("th-" + idth);
            if (th){
                activaP(th, idth);
            }
        }
        function activaP(este, id){
            ...
        }
        function nodoPadre(nodo) {
            ...
        }
        function remiteForm(accion, pestanya){
            document.getElementById("accion").value = accion;
            document.getElementById("pestanya").value = pestanya;
            document.forms[0].submit();            
        }
    </script>
    <style type="text/css">
        ...
    </style>
</head>
<body>
    ...
    <?php echo '<p 
        style="color: red">'.$mensaje.'</p>'; ?>       
    <form action="./" method="post">
    <input type="hidden" name="pestanya" id="pestanya" value="p-1" />
    <input type="hidden" name="accion" id="accion" value="" />
    <table border="0"  class="tabla-herramienta">
    <thead>
        <tr>
            <th style="color: blue; font-weight: bold;
                background-color: rgb(235, 235, 225)"
                id="th-p-1"
                onclick="activaP(this, 'p-1')">(Paso 1) 
                    Construir lista rutas</th>
            <th id="th-p-2"
                onclick="activaP(this, 'p-2')">(Paso 2) 
                    Construir índice</th>
            <th id="th-p-3"
                onclick="activaP(this, 'p-3')">(Paso 3) 
                    Construir arrays</th>
        </tr>
    </thead>        
    <tbody id="p-1" style="display: table-row-group">        
        <tr><td colspan="3">    
            Iniciar lista de rutas en la carpeta:
            <input type="text" name="desde" size="50"
            value="<?php echo $desde; ?>" />
            ...
        </td></tr>
        <tr><td colspan="3">             
            <textarea rows="20" name="lista-rutas" ...
            ><?php  echo implode(SALTO, 
                $lista_rutas);?></textarea>
        </td></tr>
    </tbody>
    <tbody id="p-2" style="display: none">        
        <tr><td colspan="3">         
        ...      
        </td></tr>         
    </tbody>
    <tbody id="p-3" style="display: none">
        <tr><td colspan="3">
        ...     
        </td></tr>               
    </tbody>
    </table> 
    </form>
</body>
</html>

Hay dos elementos <input type="hidden"> que junto a la función de Javascript remiteForm(accion, pestanya) nos servirán para enviar al servidor la acción solicitada y la pestaña que está activa.

La lista de rutas

Muchas de las herramientas que estoy haciendo se basan en iterar por los documentos para hacer algo con ellos: buscar texto, modificar partes del mismo, analizarlos, fabricar listas de contenidos, etc. Por lo tanto lo primero es construir la lista de rutas, tal como aparece en la imagen del apartado anterior.

La cuestión más importante a tener en cuenta cuando hacemos nuestras propias herramientas es que hemos de tener bien estructurado nuestro sitio. Trabajo con un servidor Apache en localhost y mantengo la misma estructura de carpetas que luego veré en mi sitio definitivo. A excepción de una carpeta denominada como no-publicable donde incluyo cualquier cosa que finalmente no subiré. Ahí también están ubicadas las herramientas. Pero el resto mantiene exactamente la misma estructura. Además mi sitio (por el momento) se puede clasificar como de contenidos estáticos, o en todo caso semiestáticos si consideramos el contenido generado en los documentos .php.

Así este constructor de lista de rutas lo podemos iniciar desde cualquier carpeta, usualmente desde la carpeta raíz poniendo /. Luego especificamos las extensiones html,php para que incluya todos los documentos de ese tipo. Esto es una lista de cadenas separadas por coma y sin espacio entre ellas. Finalmente podemos omitir las carpetas donde deseamos que no busque documentos. Es el caso de la denominada como no publicable y otras que albergan recursos de scripts, estilos, imágenes, etc (son las que se ven en la imagen no-publicable,res,ejemplos). Con un botón para construir la lista de rutas obtenemos ese listado.

Como dije, nuestro sitio debe estar bien estructurado. Así yo guardo los recursos señalados JS, CSS, imágenes, ejemplos, etc en carpetas que denomino res y la propia ejemplos. Estas denominaciones de carpetas pueden encontrarse en cualquier nivel de profundidad del árbol de carpetas. Esto me sirve para simplificar el uso de las herramientas. Pero también es una comodidad cuando tengamos que excluirlas en un robots.txt por ejemplo. Lo que hay en ellas digamos que no lo necesita un buscador y que en el resto están exactamente los documentos con el verdadero contenido del sitio. Es más, el listado obtenido con esta herramienta es realmente una lista de documentos del sitio, un sitemap.txt, que de hecho es el que por ahora estoy enviando a los buscadores como Google o Yahoo.

Todas las acciones de la interfaz que vimos antes se encomiendan al módulo indexa.php que incorporamos allí con un include_once(). El módulo se inicia definiendo unas constantes y luego están todas las funciones necesarias para ejecutar las acciones. A continuación se expone sólo la de listar rutas:

//Constantes    
define("RAIZ", $_SERVER["DOCUMENT_ROOT"]);
define("SALTO", "\r\n");
//Construir lista de rutas 
function listar_rutas($desde, $extensiones, $carpetas){
    $lista = array();
    if ($desde == "/") $desde = "";
    if (!file_exists(RAIZ.$desde)) {
        $lista[] = "ERROR: No existe '".$desde."'.";    
    } else {
        foreach(@scandir(RAIZ.$desde) as $ruta){
            if (($ruta != ".")&&($ruta != "..")){
                if (@is_dir(RAIZ.$desde."/".$ruta)){
                    $nombre_carpeta = basename($ruta);
                    if (!in_array($nombre_carpeta, $carpetas)){
                        $lista = array_merge($lista, 
                        listar_rutas($desde."/".$ruta, 
                            $extensiones, $carpetas));
                    }              
                } else {
                    $nombre_doc = basename($ruta);
                    $extension = 
                        strtolower(substr(strrchr($nombre_doc, 
                            "."), 1));  
                    if (in_array($extension, $extensiones)){                 
                        $lista[] = $desde."/".$ruta;
                    }
                }
                $arrerror = error_get_last();
                if (!is_null($arrerror)){
                    $lista[] = $arrerror["message"];
                    break;
                }            
            }
        }
    }
    $arrerror = error_get_last();
    if (!is_null($arrerror)){
        $lista[] = $arrerror["message"];
        break;
    }    
    return $lista;
}

Todas las rutas se referencian desde la carpeta raíz del sitio que obtenemos con $_SERVER["DOCUMENT_ROOT"]. La constante define("SALTO", "\r\n") tiene una utilidad en ciertos casos. Por un lado en Windows el salto de línea es "\r\n". En otros sistemas operativos es sólo "\n". Por lo tanto siempre conviene tener esto en cuenta y si fuera el caso modificarlo aquí.

La función listar_rutas($desde, $extensiones, $carpetas) es un algoritmo recursivo que usa la función de PHP scandir(). Es similar a lo que expuse en el explorador PHP para recuperar carpetas y archivos, pero con una mejora al usar esa función de PHP y sólo para recuperar archivos. Si partimos de la carpeta raíz "/" obtenemos todos los archivos y carpetas de esa raíz. Si es una carpeta (un directorio) volvemos a llamar a la propia función de forma recursiva para que siga ahondando en esa rama del árbol. Si por contra es un archivo lo ponemos en la lista con $lista[] = $desde."/".$ruta;. Al final el recursivo devolverá toda la lista con return $lista.

Puede ver algo más sobre la recursividad en este mismo sitio en:

Estructura de las funciones del módulo de acciones de una herramienta

El apartado anterior nos sirve para conocer la relación entre la interfaz y el módulo de acciones. El módulo indexa.php tiene las otras funciones de esta herramienta para indexar la lista de rutas y construir los arrays. No voy a explicar el resto de funciones pues sería muy largo. Estas son las declaraciones de las funciones que contiene ese módulo:

function listar_rutas($desde, $extensiones, $carpetas) 
   
function indexar($lista_rutas,
                $tipo_indice="encabezados",
                $solo_texto=false,
                $con_id_vacio=false,
                $lista_index=array(),
                $archivo_indice="",                
                $tages=array("h1","h2","h3","h4","h5")
                )
                
function construir_indice_arrays($script_buscador, $archivo_indice)

function guardar_indice($archivo, $resultado)

function extraer_contenido($archivo)

Las dos últimas funciones son accesorias para leer el contenido de un archivo y para guardar un resultado. Estas son funciones que podríamos considerar genéricas y que podríamos tener en un módulo común o general. De hecho el marco de herramientas del que hablé al inicio trabaja de esa forma, obteniéndose una mayor eficiencia al poder reutilizar código. Sin embargo obliga a instalar todo el marco de trabajo completo y es algo que por ahora prefiero no compartir por las razones que expuse más arriba. La herramienta que expongo ahora esta totalmente aislada y no necesita de ningún módulo adicional, pero como desventaja tendremos que repetir códigos de uso común como las funciones para gestión de archivos.

Aunque no voy a exponer más código para no aburrir, si que conviene hablar de la estructura general de estas funciones. La que vimos antes listar_rutas() sólo devuelve un array. Aunque puse un control de errores, una vez iniciado el recursivo no debería producirse ninguno, pero en todo caso el mensaje de error se devuelve como una entrada del array. Pero para las funciones normalmente se hace un control de errores con devolución de mensajes. Por ejemplo, para la función sencilla para leer el contenido de un archivo:

function extraer_contenido($archivo){
    $mensaje = "";
    $error = false;
    $contenido = "";
    if (file_exists(RAIZ.$archivo)){
        if (($contenido = file_get_contents(RAIZ.$archivo)) 
            === false){
            $error = true;
            $mensaje = "Error: No pudo leerse el archivo '".
                $archivo."'."; 
        }
    } else {
        $error = true;
        $mensaje = "No existe el archivo '".$archivo."'.";
    }
    return array("error"=>$error, "mensaje"=>$mensaje, 
    "contenido"=>$contenido);
}

Devolvemos un array con el error, un mensaje y, en su caso, un valor de devolución que en este ejemplo es el contenido de texto del archivo. Por ejemplo, esta utilidad para leer un archivo se usa dentro de la función construir_indice_arrays() para obtener el contenido de texto del archivo de índices:

...
$arr_cont = extraer_contenido(dirname($_SERVER['PHP_SELF']).
    "/".$archivo_indice);
$mensaje .= $arr_cont["mensaje"];
$error = $arr_cont["error"];
if (!$error){
    $contenido = $arr_cont["contenido"]; 
    ...

Este control de errores nos permite manejar el flujo entre la ejecución de las distintas funciones y presentar el mensaje en la interfaz. Este es un ejemplo con una ruta "/def.html" que no existe, deteniéndose la ejecución y presentándose el mensaje:

control de errores

Existirán entornos de programación o aplicaciones ya hechas que lo harán mejor que todas las herramientas que podamos fabricarnos. Quizás las únicas ventajas de hacerlas uno mismo sean el aprendizaje de los lenguajes de programación y, por que no decirlo, la satisfacción de una aplicación que hará exactamente lo que nosotros deseamos.