En primer lugar quiero agradecer la gran difusión y acogida que ha tenido la iniciativa de poner a disposición de la comunidad científica, la base de datos de Tesis Doctorales Teseo. Mi intención en este trabajo es proporcionar los datos disponibles desde el sitio web de Teseo [https://www.educacion.gob.es/teseo]. Dado lo impactante de los resultados obtenidos, quiero explicar con detalle algunos aspectos a considerar y el método de obtención de los datos, con el objetivo de certificar la metodología.
Aspectos a considerar sobre Teseo
Para obtener los registros de la base de datos Teseo, sin tener acceso al servidor de la fuente de los datos, puede emplearse el método de crawling basado en permalinks con número correlativo de registro. Todas las tesis disponibles desde el buscador de Teseo, disponen de un marcador o permalink con un número de referencia que hace mención a su ficha. Por ejemplo, mi tesis doctoral tiene el marcador «https://www.educacion.gob.es/teseo/mostrarRef.do?ref=933534«. Por tanto es posible desarrollar un programa que analice todos los enlaces correlativos desde un determinado número, por ejemplo la referencia 1 hasta la referencia 5 millones. De esta forma se puede asegurar que todos los registros de la base de datos, si están publicados y disponibles desde la web de Teseo podrán ser descargados.
Por otra parte hay que tener en consideración que existen muchas entradas duplicadas. De hecho la mayor parte de los registros disponen de una o más duplicaciones. Sin ir más lejos, la ficha correspondiente a mi Tesis Doctoral, titulada «Aplicaciones de la Sindicación para la Gestión de Catálogos Bibliográficos» puede encontrarse duplicada en las referencias «933534» y «933535«. Si observamos el primer registro disponible en Teseo, las referencias «3«, «4» y «5» se observa el mismo efecto por triplicado, y como este caso, pueden citarse centenares. La duplicación observada en muchos registros y entradas puede deberse a múltiples factores de los que sólo podemos añadir algunas hipótesis: a) Que cada modificación de los datos en un registro de Teseo produce una duplicación como si fuera un control de versiones; b) Existe algún problema de duplicación indefinido al actualizar los registros de la base de datos Teseo; c) Podría ejecutarse algún tipo de sistema de redundancia de los datos, para asegurar su presencia desde múltiples entradas. En todos los escenarios posibles, las duplicaciones invalidarían el objetivo de los marcadores o permalinks de Teseo, como método de identificación unívoca.
Aunque el método que se ha aplicado para la obtención de los registros de la base de datos de Teseo tiene en cuenta todos estos factores, tal como se explicará a continuación, también tienen que tenerse en consideración algunos aspectos que han podido influir en los resultados obtenidos. Con esto quiero incidir en que los medios que se han dispuesto para esta importante operación de recopilación han sido muy limitados. En concreto un equipo portátil de reducida capacidad y una conexión a Internet que ha podido tener algún tipo de interrupción durante el proceso. Estos factores sí han podido influir en la obtención de los resultados y puedo admitir que los datos pudieran ser distintos a los presentados. Por consiguiente, estoy desarrollando un segundo análisis para confirmar y en caso necesario, corregir, los resultados obtenidos hasta el momento. Mi intención en todo caso es proporcionar información real y estoy abierto a sugerencias y aportaciones constructivas para lograr una base de datos lo más idéntica posible a Teseo.
Sistema de recopilación automática de Teseo
Dicho todo esto, quiero compartir el método automatizado de recopilación de la base de datos Teseo. El código en cuestión es el que se muestra a continuación.
<meta http-equiv='content-type' content='text/html; charset=UTF-8' /> <?php $namedb = "teseo"; $con = mysqli_connect ( 'localhost', 'root', 'root', 'teseo' ); if (mysqli_connect_errno ()) { $con = new mysqli ( "localhost", "root", "root" ); } mysqli_select_db ( $con, "$namedb" ); $cf_agent = "MBOT webcrawler by Prof. Dr. Manuel Blázquez Ochando"; $cf_header = "Content-Type: text/plain, text/xml, text/html, text/htm, text/jsp, text/json, text/x-json, application/xml, application/xhtml+xml, text/plain, text/php, text/asp, application/jsp, application/json, application/x-httpd-php, application/php, application/asp, text/vcard, text/xvcard"; $cf_buffer = "6291456"; $cf_timecache = "1"; $cf_timeconnect = "1000"; $cf_timeout = "1000"; for($i = 0; $i <= 5000000; $i ++) { $url1 = "https://www.educacion.gob.es/teseo/mostrarRef.do?ref=$i"; $thread1 = curl_init (); curl_setopt ( $thread1, CURLOPT_URL, $url1 ); curl_setopt ( $thread1, CURLOPT_USERAGENT, $cf_agent ); curl_setopt ( $thread1, CURLOPT_HTTPHEADER, array ( "'$cf_header'" ) ); curl_setopt ( $thread1, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt ( $thread1, CURLOPT_FAILONERROR, true ); curl_setopt ( $thread1, CURLOPT_FOLLOWLOCATION, true ); curl_setopt ( $thread1, CURLOPT_LOW_SPEED_TIME, 3 ); curl_setopt ( $thread1, CURLOPT_LOW_SPEED_LIMIT, 1048576 ); curl_setopt ( $thread1, CURLOPT_AUTOREFERER, true ); curl_setopt ( $thread1, CURLOPT_RETURNTRANSFER, true ); curl_setopt ( $thread1, CURLOPT_FORBID_REUSE, true ); curl_setopt ( $thread1, CURLOPT_FRESH_CONNECT, true ); curl_setopt ( $thread1, CURLOPT_BUFFERSIZE, $cf_buffer ); curl_setopt ( $thread1, CURLOPT_DNS_CACHE_TIMEOUT, $cf_timecache ); curl_setopt ( $thread1, CURLOPT_CONNECTTIMEOUT_MS, $cf_timeconnect ); curl_setopt ( $thread1, CURLOPT_TIMEOUT_MS, $cf_timeout ); $html1 = curl_exec ( $thread1 ); curl_close ( $thread1 ); $dom1 = new DOMDocument (); @$dom1->loadHTML ( $html1 ); $xpath1 = new DOMXPath ( $dom1 ); // Título @$data00 = $xpath1->query ( "//div[@id='contenido']/div/ul/li" )->item ( 0 )->nodeValue; $data00 = utf8_decode ( $data00 ); $data00 = preg_replace ( "/(Título:)/", "", $data00 ); $data00 = trim ( $data00, chr ( 0xC2 ) . chr ( 0xA0 ) ); $data00 = mb_strtolower ( $data00, 'UTF-8' ); $data00 = ucfirst ( trim ( $data00 ) ); // Autor @$data01 = $xpath1->query ( "//div[@id='contenido']/div/ul/li" )->item ( 1 )->nodeValue; $data01 = utf8_decode ( $data01 ); $data01 = preg_replace ( "/(Autor:)/", "", $data01 ); $data01 = trim ( $data01, chr ( 0xC2 ) . chr ( 0xA0 ) ); $data01 = mb_strtolower ( $data01, 'UTF-8' ); $data01 = ucwords ( trim ( $data01 ) ); // Universidad @$data02 = $xpath1->query ( "//div[@id='contenido']/div/ul/li" )->item ( 2 )->nodeValue; $data02 = utf8_decode ( $data02 ); $data02 = preg_replace ( "/(Universidad:)/", "", $data02 ); $data02 = trim ( $data02, chr ( 0xC2 ) . chr ( 0xA0 ) ); $data02 = mb_strtolower ( $data02, 'UTF-8' ); $data02 = ucfirst ( trim ( $data02 ) ); // Fecha de lectura @$data03 = $xpath1->query ( "//div[@id='contenido']/div/ul/li" )->item ( 3 )->nodeValue; $data03 = utf8_decode ( $data03 ); $data03 = preg_replace ( "/(Fecha de Lectura:)/", "", $data03 ); $data03 = trim ( $data03, chr ( 0xC2 ) . chr ( 0xA0 ) ); $array_data03 = explode ( "/", $data03 ); $data03 = $array_data03 [2] . "-" . $array_data03 [1] . "-" . $array_data03 [0]; if (preg_match ( "/departamento/i", $data03 )) { @$data03 = $xpath1->query ( "//div[@id='contenido']/div/ul/li" )->item ( 4 )->nodeValue; $data03 = utf8_decode ( $data03 ); $data03 = preg_replace ( "/(Fecha de Lectura:)/", "", $data03 ); $data03 = trim ( $data03, chr ( 0xC2 ) . chr ( 0xA0 ) ); $array_data03 = explode ( "/", $data03 ); $data03 = $array_data03 [2] . "-" . $array_data03 [1] . "-" . $array_data03 [0]; } // Dirección (1*) for($a0 = 0; $a0 <= 10; $a0 ++) { @$st04 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[5]/ul/li" )->item ( $a0 )->nodeValue; $st04 = utf8_decode ( $st04 ); $st04 = preg_replace ( "/\x{00a0}/", "", $st04 ); $st04 = mb_strtolower ( $st04, 'UTF-8' ); $st04 = ucwords ( trim ( $st04 ) ); if (preg_match ( "/director/i", $st04 )) { if (strlen ( $st04 ) <= 2) { } else { $array4 [] = "$st04"; } } elseif (preg_match ( "/(presidente|vocal|secretario)/i", $st04 )) { if (strlen ( $st04 ) <= 2) { } else { $array5 [] = "$st04"; } } else { if (strlen ( $st04 ) <= 2) { } else { $array6 [] = "$st04"; } } unset ( $st04 ); } // Tribunal (1*) for($a0 = 0; $a0 <= 10; $a0 ++) { @$st05 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[6]/ul/li" )->item ( $a0 )->nodeValue; $st05 = utf8_decode ( $st05 ); $st05 = preg_replace ( "/\x{00a0}/", "", $st05 ); $st05 = mb_strtolower ( $st05, 'UTF-8' ); $st05 = ucwords ( trim ( $st05 ) ); if (preg_match ( "/(director)/i", $st05 )) { if (strlen ( $st05 ) <= 2) { } else { $array4 [] = "$st05"; } } elseif (preg_match ( "/(presidente|vocal|secretario)/i", $st05 )) { if (strlen ( $st05 ) <= 2) { } else { $array5 [] = "$st05"; } } else { if (strlen ( $st05 ) <= 2) { } else { $array6 [] = "$st05"; } } unset ( $st05 ); } // Descriptores (1*) for($a0 = 0; $a0 <= 10; $a0 ++) { @$st06 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[7]/ul/li" )->item ( $a0 )->nodeValue; $st06 = utf8_decode ( $st06 ); $st06 = preg_replace ( "/\x{00a0}/", "", $st06 ); $st06 = mb_strtolower ( $st06, 'UTF-8' ); $st06 = ucfirst ( trim ( $st06 ) ); if (preg_match ( "/(director)/i", $st06 )) { if (strlen ( $st06 ) <= 2) { } else { $array4 [] = "$st06"; } } elseif (preg_match ( "/(presidente|vocal|secretario)/i", $st06 )) { if (strlen ( $st06 ) <= 2) { } else { $array5 [] = "$st06"; } } else { if (strlen ( $st06 ) <= 2) { } else { $array6 [] = "$st06"; } } unset ( $st06 ); } // En caso de que no existan descriptores se prueba la recuperación con otra llave $test6 = count ( $array6 ); if ($test6 == "0") { for($a0 = 0; $a0 <= 10; $a0 ++) { @$st06 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[8]/ul/li" )->item ( $a0 )->nodeValue; $st06 = utf8_decode ( $st06 ); $st06 = preg_replace ( "/\x{00a0}/", "", $st06 ); $st06 = mb_strtolower ( $st06, 'UTF-8' ); $st06 = ucfirst ( trim ( $st06 ) ); if (! preg_match ( "/(director|presidente|vocal|secretario|marcador)/i", $st06 )) { if (strlen ( $st06 ) <= 2) { } else { $array6 [] = "$st06"; } } } } // Resumen @$data07 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[10]" )->item ( 0 )->nodeValue; $data07 = utf8_decode ( $data07 ); $data07 = preg_replace ( "/(Resumen:)/", "", $data07 ); $data07 = trim ( $data07, chr ( 0xC2 ) . chr ( 0xA0 ) ); $data07 = mb_strtolower ( $data07, 'UTF-8' ); $data07 = ucfirst ( trim ( $data07 ) ); if (preg_match ( "/(marcador)/i", $data07 )) { @$data07 = $xpath1->query ( "//div[@id='contenido']/div/ul/li[11]" )->item ( 0 )->nodeValue; $data07 = utf8_decode ( $data07 ); $data07 = preg_replace ( "/(Resumen:)/", "", $data07 ); $data07 = trim ( $data07, chr ( 0xC2 ) . chr ( 0xA0 ) ); $data07 = mb_strtolower ( $data07, 'UTF-8' ); $data07 = ucfirst ( trim ( $data07 ) ); $data07 = preg_replace_callback ( '/[.!?].*?\w/', create_function ( '$matches', 'return strtoupper($matches[0]);' ), $data07 ); } else { $data07 = preg_replace_callback ( '/[.!?].*?\w/', create_function ( '$matches', 'return strtoupper($matches[0]);' ), $data07 ); } foreach ( $array4 as $item4 ) { $data04 .= "$item4|"; } $data04 = substr ( "$data04", 0, - 1 ); foreach ( $array5 as $item5 ) { $data05 .= "$item5|"; } $data05 = substr ( "$data05", 0, - 1 ); foreach ( $array6 as $item6 ) { $data06 .= "$item6|"; } $data06 = substr ( "$data06", 0, - 1 ); // Comprobar si existe duplicación $results = mysqli_query ( $con, "SELECT COUNT(*) AS nrows FROM catalogoteseo WHERE titulo LIKE '%$data00%';" ); $row = mysqli_fetch_array ( $results ); if ($row [nrows] >= "1") { } else { $datetime = date ( c ); // Insertar registro mysqli_query ( $con, "INSERT INTO catalogoteseo SET core='7', regdate='$datetime', ref='$url1', titulo='$data00', autor='$data01', universidad='$data02', fecha='$data03', director='$data04', tribunal='$data05', materia='$data06', resumen='$data07';" ); } unset ( $html1 ); unset ( $data00 ); unset ( $data01 ); unset ( $data02 ); unset ( $data03 ); unset ( $data04 ); unset ( $data05 ); unset ( $data06 ); unset ( $data07 ); unset ( $array4 ); unset ( $array5 ); unset ( $array6 ); unset ( $item4 ); unset ( $item5 ); unset ( $item6 ); } echo "FIN";
A pesar de resultar un programa sencillo, puede resultar bastante complejo si no se está familiarizado con el lenguaje de programación PHP. Por ello, procedo a realizar una descripción detallada del funcionamiento del crawler.
El programa está diseñado para recuperar todas las páginas web de Teseo que tengan el patrón de enlace «https://www.educacion.gob.es/teseo/mostrarRef.do?ref=NNNN» donde «NNNN» es un número que varía correlativamente entre «1» y «5.000.000». El valor numérico de la referencia del enlace, permite recuperar todo el espectro de registros posible en la base de datos Teseo. Ello puede comprobarse en el bucle «for($i=0; $i<=5000000; $i++)» en la línea 23. Por tanto, el programa rastrea 5 millones de marcadores de la base de datos Teseo y obtiene sus códigos fuente en formato HTML, mediante las funciones cURL que se muestran entre las líneas 27 y 47.
$url1 = "https://www.educacion.gob.es/teseo/mostrarRef.do?ref=$i"; $thread1 = curl_init(); curl_setopt($thread1, CURLOPT_URL, $url1); curl_setopt($thread1, CURLOPT_USERAGENT, $cf_agent); curl_setopt($thread1, CURLOPT_HTTPHEADER, array("'$cf_header'")); curl_setopt($thread1, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($thread1, CURLOPT_FAILONERROR, true); curl_setopt($thread1, CURLOPT_FOLLOWLOCATION, true); curl_setopt($thread1, CURLOPT_LOW_SPEED_TIME, 3); curl_setopt($thread1, CURLOPT_LOW_SPEED_LIMIT, 1048576); curl_setopt($thread1, CURLOPT_AUTOREFERER, true); curl_setopt($thread1, CURLOPT_RETURNTRANSFER, true); curl_setopt($thread1, CURLOPT_FORBID_REUSE, true); curl_setopt($thread1, CURLOPT_FRESH_CONNECT, true); curl_setopt($thread1, CURLOPT_BUFFERSIZE, $cf_buffer); curl_setopt($thread1, CURLOPT_DNS_CACHE_TIMEOUT, $cf_timecache); curl_setopt($thread1, CURLOPT_CONNECTTIMEOUT_MS, $cf_timeconnect); curl_setopt($thread1, CURLOPT_TIMEOUT_MS, $cf_timeout); $html1 = curl_exec($thread1); curl_close($thread1);
Para facilitar el proceso de filtrado de la información disponible en las fichas que se han descargado en formato HTML, se crea un objeto DOM (Document Object Model) que sirve para realizar un mapeado de todos los nodos de la estructura de las fichas en formato HTML, véase líneas 49 a 51.
$dom1 = new DOMDocument(); @$dom1->loadHTML($html1); $xpath1 = new DOMXPath($dom1);
El siguiente paso es la extracción de los elementos que contienen la información de la ficha, tales como el título, autor, universidad, fecha de lectura, director/es, miembros del tribunal, descriptores y resumen. La extracción de los datos de la ficha puede comprobarse desde la línea 53 hasta la 229, en las que se observa el empleo de consultas de tipo XPath para la selección de los contenidos. También se pueden apreciar mecanismos de decodificación de set de caracteres (función utf8_decode), preparación de texto (función preg_replace), eliminación de espacios y tabulaciones extras que dificultan el procesamiento de los datos (función trim), logrando una normalización del texto (funciones mb_strtolower, ucfirst y ucwords). En adición a los mecanismos de preparación del texto, el programa es capaz de detectar algunos errores frecuentes de posicionamiento de los contenidos y reubicar la información en las variables correctas, utilizando para ello funciones basadas en expresiones regulares como [preg_match(«/(presidente|vocal|secretario)/»,…)].
En las líneas 231 a 235 se comprueba si existe algún tipo de duplicación de los datos recopilados en referencia a la base de datos de volcado, destinataria del almacenamiento de la información. De esta forma se evitan los problemas de duplicación de Teseo, permitiendo asegurar que todos los registros insertados sean únicos, puesto que sus títulos siempre serán diferentes.
// Comprobar si existe duplicación a partir del campo título $results = mysqli_query($con, "SELECT COUNT(*) AS nrows FROM catalogoteseo WHERE titulo LIKE '%$data00%';"); $row = mysqli_fetch_array($results); if($row[nrows] >= "1"){ // Si existe 1 registro en la tabla de volcado, se detecta duplicación } else { // En caso contrario, entonces inserta el registro en la tabla }
Si el título de la Tesis Doctoral no está presente, o no figura en la tabla destinataria del volcado, denominada tabla «catalogoteseo», entonces se ejecutan las instrucciones que figuran en las líneas 236 a 240, que corresponden a la inserción de los datos obtenidos de la ficha de Tesis Doctoral.
// Insertar registro mysqli_query($con, "INSERT INTO catalogoteseo SET core='7', regdate='$datetime', ref='$url1', titulo='$data00', autor='$data01', universidad='$data02', fecha='$data03', director='$data04', tribunal='$data05', materia='$data06', resumen='$data07';");
El último paso del programa es eliminar todas las variables y arrays de datos que se han empleado, para evitar residuos en el próximo ciclo del bucle. Todo el proceso descrito se repite varios millones de veces hasta alcanzar el marcador «https://www.educacion.gob.es/teseo/mostrarRef.do?ref=5000000» a partir del cual el programa finaliza y escribe en pantalla la palabra «FIN».
Consideraciones finales
Teseo es una base de datos viva que puede variar y de hecho varía a diario.
Es posible que Teseo no muestre todas las Tesis Doctorales de que dispone en realidad y sólo se publiquen las fichas de aquellas, de las que sí se posean unos datos mínimos básicos. Ello explicaría las diferencias entre los datos obtenidos y los oficiales, disponibles en [http://www.mecd.gob.es/educacion-mecd/areas-educacion/universidades/estadisticas-informes/estadisticas/tesis-doctorales.html]
También cabe la posibilidad de que existan lagunas en algunos rangos cronológicos de Teseo y que éstos no queden patentes en las fichas públicas, aunque sí existan en terceras bases de datos o registros, en cuyo caso no se dispondría del acceso necesario y sería imposible su volcado o descarga.
Por otra parte, si Teseo dispone de más de 5 millones de referencias entre sus marcadores, también cabría la posibilidad de que existan registros fuera del rango analizado por el crawler. En ese caso, sólo se requeriría especificar un rango mayor que abarcara los registros restantes de Teseo. Ésta posibilidad, aunque improbable, se está estudiando para ser confirmada o descartada.
En caso de obtener nuevos datos sobre los datasets de Teseo, puedo asegurar que informaré puntualmente y se actualizarán tanto en las referencias de las entradas del portal mblazquez [ref1] y [ref2] como del proyecto publicado en sourceforge [https://sourceforge.net/projects/teseo-database/] para que la información siempre sea lo más precisa posible.
Agradecimientos
Finalmente quiero agradecer nuevamente todo el apoyo recibido y la confianza depositada en el trabajo que vengo desarrollando. He intentado ser lo más transparente y fiel a la realidad con las herramientas y medios disponibles. Aprovecho para transmitir un afectuoso saludo a todos los lectores y seguidores de mblazquez.es
Relación de artículos de Teseo
Hola Manuel. Mi más sincera enhorabuena por este trabajo, y mi agradecimiento por hacer accesible de esta manera tanto la base de datos, como el código para recopilar la información. Seguro que ambos serán útiles a mucha gente en el futuro y harán posibles estudios muy interesantes.
Sobre el tema de los duplicados, yo no los habría considerado como tales. Según he visto (sin dedicarle mucho tiempo) es que por alguna razón, en Teseo han decidido asignar los IDs saltándose dos números entre un ID y otro: 3, 6, 9… 933534, 933537, 933540… y además, por defecto, si intentas acceder a un registro que no existe (4, 5, 7, 8, 933535, 933536, 933538, 933539…) te dirige automáticamente al registro existente con un ID inferior más cercano. De esta manera, aunque te metas en el registro que correspondería a 933535 (algo que por otra parte en condiciones normales no debería ocurrir), el código que aparece en el campo marcador sigue siendo 933534, es decir, el del registro de tu tesis, y todos los demás campos son exactamente idénticos (no son, por ejemplo, el registro original, y una versión modificada). Sería diferente si cuando te intentaras meter en 933535, el campo marcador también reflejara el código 933535, y más todavía si hubiera diferencias en algunos de los campos. En ese caso, la identificación unívoca del documento se perdería, pero tal y como están las cosas, parece que eso no es un problema. Está claro que para descargar la base de datos era necesario comprobar ese rango de números al completo (pues no se sabe de antemano lo que te vas a encontrar), pero gracias a ese campo marcador, no hay ningún problema para quedarnos con lo que nos interesa.
De nuevo te transmito mi enhorabuena. Un saludo!
Muchas gracias Alberto, por tus comentarios y por seguir esta investigación. Es un placer contar con tus aportaciones.
Sobre lo que indicas, la verdad es que resulta un tanto extraño el baile de identificadores. Desde luego parece claro que una ficha puede tener 1 o más identificadores próximos, aunque unos sean válidos y otros no. Ignoro si la base de datos en origen dispone de dichas entradas. Lo que está claro es que se hace necesario un control de datos duplicados para procesar todos los permalinks o marcadores y creo que el método aplicado responde a esa situación. De todos modos seguiré informando de avances y actualizaciones que se produzcan. Gracias nuevamente, Saludos!
[…] Cómo se obtuvieron los datos de TESEO, aspectos a considerar y nuevas acciones […]
[…] Cómo se obtuvieron los datos de TESEO, aspectos a considerar y nuevas acciones […]