# ¿Cómo puedo preparar mi texto digital para su estudio?
## Extracción (web scraping), limpieza y marcado automático de corpus

Pablo Ruiz

► To cite this version:

Pablo Ruiz. ¿Cómo puedo preparar mi texto digital para su estudio? Extracción (web scraping), limpieza y marcado automático de corpus. Clara Martínez; Rocío Ortuño; Antonio Huertas. Las humanidades digitales en la enseñanza de las literaturas hispánicas. Aplicaciones prácticas, Peter Lang, pp.85–111, 2023. [https://hal.science/hal-04747964v1](https://hal.science/hal-04747964v1)

HAL Id: hal-04747964

https://hal.science/hal-04747964v1

Submitted on 22 Oct 2024

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_1.png" /></figure>

Distributed under a Creative Commons Attribution 4.0 International License
# ¿Cómo puedo preparar mi texto digital para su estudio? Extracción (*web scraping*), limpieza y marcado automático de corpus

Pablo RUIZ FABO

Université de Strasbourg
ruizfabo@unistra.fr

https://orcid.org/0000-0002-4349-4835

**Resumen:** El capítulo describe la extracción de contenido (*scraping*) a partir de fuentes web (expresadas sobre todo en HTML). Es necesario hacer scraping cuando los textos electrónicos que queremos analizar no están disponibles en un formato apropiado para nuestras herramientas de análisis informático. Se describen brevemente el marcado HTML, XML y JSON, y el lenguaje conocido como expresiones regulares para manipular cadenas de texto. Se presenta un tutorial de la herramienta OpenRefine para scraping. Un repositorio acompaña al capítulo, dando más detalles y presentando el scraping con el lenguaje Python. Se proponen dos actividades para el aula.

**Palabras clave:** Scraping. OpenRefine. HTML y XML. JSON. Python

## 1. INTRODUCCIÓN

En ocasiones, los textos que nos interesan están disponibles en línea pero su formato no permite un análisis directo ya que el texto y metadatos deben ser extraídos a partir del documento. También puede ocurrir que los documentos estén dispersos a través de varias direcciones web, y hay que obtener primero las direcciones para acceder al contenido. El capítulo describe esta tarea de extracción, a veces llamada scraping. Describimos la extracción a partir de fuentes en el lenguaje de marcado HTML, así como los lenguajes de estructuración de datos XML y JSON. Primero se presentan las bases tecnológicas que subyacen a esta tarea y después una propuesta práctica con una herramienta concreta para llevarla a cabo. La estructura del capítulo es la siguiente: la sección 2 presenta bases tecnológicas que permiten comprender la realización de esta tarea y los lenguajes de marcado en los que la información que extraer está representada. La sección 3 presenta brevemente opciones tecnológicas para la extracción de contenidos. La sección 4 es de tipo práctico; describe cómo llevar a cabo la
extracción de información a partir de HTML con la herramienta OpenRefine. El repositorio del capítulo da detalles omitidos aquí por razones de espacio y se recomienda su consulta<fnref n="1" />; presenta además la extracción a partir de fuentes en XML y JSON (lenguajes descritos en 2.3), así como usando el lenguaje Python. La sección 5 propone una aplicación didáctica.

Cabe señalar que, antes de extraer contenidos web, debemos conocer si las condiciones de uso definidas para esos contenidos (por la legislación, o en la licencia elegida por las personas que los han creado) lo permiten. En el capítulo usamos textos bajo licencias que permiten su almacenamiento y reutilización.

## 2. BASES TECNOLÓGICAS

Para extraer la información requerida, los métodos de *scraping* se basan en regularidades en la estructura de un sitio web, en las direcciones (URL) que identifican sus contenidos y en la estructura de las páginas del sitio, generalmente expresadas en el lenguaje HTML. Además, es necesario poder efectuar de forma más o menos automatizada manipulaciones de datos clásicas, como generar URLs, acceder a estos, almacenar el contenido identificado por ellos (de forma temporal o en un soporte permanente), aplicar operaciones de extracción de datos y almacenar el resultado final de la extracción de forma permanente (p. ej. en archivos o en una base de datos). Una comprensión básica de las características de un sitio web y de las operaciones de manipulación de datos en las que se basa el *scraping* es conveniente para utilizar las herramientas de *scraping* eficazmente. Esta sección presenta estos aspectos brevemente.

### 2.1. Estructura de un URL

Es interesante conocer la estructura de una dirección web o URL ya que da información sobre cómo extraer contenidos del sitio correspondiente. Los URL constan de diferentes partes, que presentamos de forma simplificada según lo que es pertinente para una tarea de *scraping* de dificultad media.

<footnote n="1">1 https://github.com/HD-aula-Literatura/II-2-scraping.</footnote>
### 2.1.1. Componentes obligatorios del URL

Para ilustrar los componentes obligatorios, tomamos los ejemplos siguientes:

(a) https://digiphilit.uantwerpen.be/home/proyecto

(b) https://digiphilit.uantwerpen.be/2021/02/26/digiphilit-en-el-manila-times/

**Esquema:** especificación del estándar que rige la comunicación a establecer (https en el ejemplo).

**Nombre de anfitrión o host o servidor:** se encuentra después del separador entre el esquema y el resto del URL. En el ejemplo, *digiphilit.uantwerpen.be*.

**Ruta:** en el ejemplo (a), se trata de `/proyecto`. En el ejemplo (b), se puede considerar que la ruta es `/2021/02/26/digiphilit-en-el-manila-times`. Una ruta no tiene por qué corresponder físicamente a un directorio. Las páginas de una gran parte de sitios web se generan automáticamente a partir de contenido almacenado en una base de datos (de forma que el contenido y el diseño se pueden cambiar por separado), con lo cual los URL no corresponden a directorios o ficheros. En todo caso, estos URL nos pueden dar información útil para el *scraping*; en el ejemplo (2) vemos que el URL indica la fecha de publicación de la página. Podríamos usar esto para seleccionar URLs de un año determinado. Cuando las rutas corresponden a una estructura de directorios, la barra `/` separa los directorios y podríamos encontrar la sintaxis `..` (una secuencia de dos puntos, que representa el directorio superior) o un solo punto `.` (que representa el directorio actual).

### 2.1.2. Componentes opcionales

Tomamos los URL siguientes para ilustrarlos:

(c) https://digiphilit.uantwerpen.be/proyecto#humanidades_digitales

(d) https://poemas.uned.es/canciones/?fwp_buscar_en_la_letra=luna

(e) https://prf1.org/disco/showpoem.php?id=2450

**Identificador de fragmento:** sigue al separador `#`. Indica un fragmento dentro de una página. En el ejemplo (c), se refiere a la sección `#humanidades_digitales` de la página que precede al identificador.

**Cadena de búsqueda:** aparece después del separador? Consiste en una serie de pares `clave = valor`, unidos por un signo igual (`=`). Si hay varios pares, el separador es `&`. A pesar de su denominación, su función no siempre está relacionada
con hacer búsquedas. En el ejemplo (d) corresponde a la búsqueda de la palabra "luna" en una base de datos de musicalizaciones de poemas, pero en el ejemplo (e) no hay una búsqueda en ese sentido; el URL se refiere al poema cuyo atributo *id* es 2450. Como se ve en este ejemplo, la cadena de búsqueda también expresa información pertinente para el scraping: A veces generaremos URLs con pares de clave-valor adecuados para acceder a un documento o recurso concreto, o extraeremos informaciones de este tipo de URL.

## 2.2. El marcado HTML

El acrónimo quiere decir Hypertext Markup Language (lenguaje de marcado para hipertexto). Es uno de los lenguajes utilizados para crear sitios web. Los elementos HTML definen la estructura, hipervínculos (links) y metadatos del sitio.

Un documento HTML representa una estructura de datos jerárquica (arbórea). Esta estructura se puede expresar como una secuencia de caracteres que respeta unas reglas de sintaxis (ver abajo). Al hacer scraping, a veces analizamos HTML con funciones que usan la estructura arbórea, y a veces manipulamos cadenas de texto que no representan HTML; veremos ejemplos de cada caso en la sección 4. Exponemos ahora propiedades de la sintaxis HTML pertinentes para el scraping<fnref n="2" />.

El árbol del documento se expresa con una jerarquía de pares de etiquetas anidables. P. ej. la etiqueta `<div>` que indica el inicio de un bloque de contenido, y la etiqueta `<p>` que inicia cada párrafo dentro del bloque; las etiquetas de fin correspondientes son `</div>` y `</p>` (ver ejemplo similar en figura 1). En ocasiones puede haber no un par de etiquetas de abertura y cierre, sino una sola etiqueta vacía de autocierre, como `<br/>` (figura 1).

Las etiquetas de abertura de los elementos pueden llevar atributos, que se expresan con el nombre del atributo y el signo = para unirlo a su valor, que va entre comillas dobles o simples. Algunos atributos se pueden usar con todos los elementos, como class o id, que identifican instrucciones de formato de las hojas de estilos CSS (otro componente de una página web, responsable de la presentación visual del contenido). Estos atributos son muy usados en scraping, ya que los tipos de contenido que nos interesan pueden ir identificados con un valor de class o id, que podemos aprovechar en nuestra extracción (ver figura 1). También hay atributos específicos a ciertos elementos. Entre estos, un atributo

<footnote n="2">2 Para una descripción completa, ver el tutorial en español https://lenguajehtml.com/html/ o https://www.w3schools.com/html/default.asp (con ejercicios interactivos, en inglés).</footnote>
importante para el scraping es href, que indica la dirección a la que lleva un link (elemento <a>).

Otra característica pertinente son las entidades HTML<fnref n="3" />, usadas en algunas páginas, que empiezan por & y acaban por ;, como &eacute;ute; (para é con tilde), o &#160; (Tabla 1), que representa un espacio de no separación. Las herramientas de scraping tienen métodos para sustituir una entidad por el carácter correspondiente.

```html
<div class="poem">
<p>&#160;Tarde sucia de invierno. El caserío, <br /> &amp;#160;como si fuera un croquis al creyón, <br /> &amp;#160;se hunde como en la noche. El humo de un bohío, <br /> &amp;#160;que sube en forma de tirabuzón; <br /> &lt;br /&gt; <br /> &amp;#160;mancha el paisaje que produce frío, <br /> &amp;#160;y debajo de la genuflexión <br /> &lt;br /&gt; <br /> &amp;#160;de la arboleda, somormuja el río <br /> &lt;br /&gt; <br /> &amp;#160;su canción, su somnífera canción. <br /> &lt;br /&gt; <br /> &lt;br /&gt; <br /> &lt;/p&gt; <br /> &lt;/div&gt;
```

Tarde sucia de invierno. El caserío,
como si fuera un croquis al creyón,
se hunde en la noche. El humo de un bohío,
que sube en forma de tirabuzón;

mancha el paisaje que produce frío,
y debajo de la genuflexión
de la arboleda, somormuja el río
su canción, su somnífera canción.

**Figura 1.** Izquierda: marcado HTML del fragmento de una página web con un poema de Delmira Agustini. Derecha. Presentación del fragmento en el navegador. El marcado divide el texto en versos y estrofas. El poema se delimitó con un elemento `<div>`, que lleva un atributo class cuyo valor es poem. Dentro del `<div>` un elemento `<p>` contiene el poema. Se usaron elementos `<br/>` para separar los versos. Fuente: adaptado de https://es.wikisource.org/wiki/Una_vi%C3%B1eta (consultado el 05/08/2022)

<footnote n="3">3 Ver: https://es.wikipedia.org/wiki/Anexo:Referencias_a_entidades_de_characteres_XML_y_HTML (consultado el 05/08/2022).</footnote>
## 2.3. Los lenguajes XML y JSON

Se trata de dos lenguajes bastante diferentes entre sí que abordamos en la misma sección dado el breve tratamiento que ofrecemos de ambos.

XML (*eXtensible Markup Language*) es un lenguaje utilizado para representar información de forma estructurada; hablamos de él aquí ya que algunos documentos obtenidos por scraping pueden estar en XML. El capítulo III.1 del volumen presenta el XML con más detalle. Como el HTML, se basa en una estructura arbórea, expresable mediante una cadena de texto, en la que el contenido está incluido dentro de pares de etiquetas anidadas, y las etiquetas de abertura pueden tener atributos. Mientras que en HTML el conjunto de etiquetas posible está predefinido, y sus funciones se refieren a la estructuración y presentación de documentos web, en XML cada persona puede definir sus propias etiquetas (es extensible) y crear su propia especificación de marcado. Una especificación muy utilizada en humanidades es el XML-TEI (*Text Encoding Initiative*, TEI Consortium, 2022). Hay tecnologías específicas para manipular documentos XML, como XPath, XSLT o XQuery. La herramienta OpenRefine tiene algunas funciones para manipular XML, presentadas en el repositorio del capítulo<fnref n="4" />.

JSON (se puede pronunciar *jotasón*) es otro lenguaje para representar contenido de forma estructurada. El formalismo para representar JSON mediante una cadena de texto identifica el contenido con pares anidables de clave-valor en vez de con pares de etiquetas anidables (figura 2). OpenRefine permite manipular JSON, como se describe en el repositorio del capítulo<fnref n="5" />.

<footnote n="4">4 https://github.com/HD-aula-Literatura/II-2-scraping/blob/main/01-extraccion-con-openrefine/02-extraccion-desde-xml.md.</footnote>

<footnote n="5">5 https://github.com/HD-aula-Literatura/II-2-scraping/blob/main/01-extraccion-con-openrefine/03-extraccion-desde-json.md.</footnote>
<figure><img src="image_2.png" /><figcaption>**Figura 2.** Estrofa expresada en XML (izquierda) y JSON equivalente (derecha). En JSON una clave es verso y su valor es una lista con tres cadenas de texto, una por verso, mientras que en XML el texto de cada verso aparece dentro de una etiqueta <verso>. Elaboración propia</figcaption></figure>

## 2.4. Las expresiones regulares

Las expresiones regulares o regex son un lenguaje de manipulación de cadenas de texto. Permiten encontrar secuencias de caracteres que siguen un patrón determinado. Son útiles para scraping ya que nos permiten encontrar la información que extraer basándonos en patrones tipográficos o (con fuentes web) en regularidades de los URLs que identifican la información (ejemplo en tabla 1). El lenguaje también permite sustituir partes de una cadena de texto por otra, con lo cual puede ser útil no sólo para extraer información, sino para generar marcado que la estructure. Usamos regex básicas en la sección 4. No podemos describir el lenguaje aquí por falta de espacio, pero ofrecemos un tutorial de elaboración propia en Zenodo (Ruiz Fabo, 2022)<fnref n="6" />.

<footnote n="6">6 Tutorial de expresiones regulares: https://doi.org/10.5281/zenodo.6981766.</footnote>
<table><caption>**Tabla 1.** Ejemplo de expresión regular para identificar URLs de fechas concretas, asumiendo un formato https://test.com/aaaa/mm/dd. Ver nota 6 para un tutorial completo de expresiones regulares</caption><thead><tr><td>Regex</td><td>Fechas aceptadas (aaaa/dd/mm)</td><td>Razón</td></tr></thead><tbody><tr><td rowspan="3">https://test.com/201[0-9]/1[1-2]/[0-9]{2}</td><td>Año entre 2010 y 2019</td><td>201 [0-9] exige que las tres primeras cifras sean 201</td></tr><tr><td>Mes 11 o 12</td><td>1 [1-2] exige que la primera cifra sea 1</td></tr><tr><td>Cualquier día del mes</td><td>[0-9] {2} acepta dos cifras cualesquiera</td></tr></tbody></table>

# 3. HERRAMIENTAS PARA SCRAPING

## 3.1. Generalidades

Existen diversos repositorios públicos con textos literarios y metadatos pertinentes para su análisis (ver capítulo 1), donde los textos ya se proveen en formatos explotables por herramientas de análisis textual automático (p. ej. TEI, texto delimitado o texto plano). Sin embargo, extraer contenido a partir de fuentes web (*scraping*) cuyo formato no es adecuado para los análisis es una necesidad recurrente en proyectos de humanidades y otros campos (p. ej. periodismo de datos). Por eso, existen herramientas que facilitan esta tarea. El medio más flexible para hacer *scraping* es utilizar un lenguaje de programación apropiado para recorrer contenido web, analizar su marcado y extraer el contenido, como Python o R. Estos lenguajes son útiles para la enseñanza y aprendizaje en campos relacionados con el análisis de textos, pero aprenderlos puede suponer una inversión de tiempo no asequible cuando el objetivo es solo hacer *scraping*. En el otro extremo de facilidad de uso, existen plataformas web que ofrecen funciones de *scraping* a través de una interfaz de uso simple<fnref n="7" />. Sin embargo, estas tienen a veces funciones no gratuitas al tratarse de iniciativas comerciales. Además, al estar alojadas en infraestructuras gestionadas por sus proveedores, no está garantizado un acceso a largo plazo a ellas. Como término medio

<footnote n="7">7 Como ejemplo, en el portal SSHOC Marketplace, que recopila recursos digitales para la investigación en humanidades, promovido por la infraestructura de la Comisión Europea para la ciencia abierta (EOSC), la búsqueda del término *scraping* lista hoy 6 plataformas de este tipo: https://marketplace.sshopencloud.eu/sea rch?order=scre&q=scraping&categories=tool-or-service, consultado el 02/08/2022.</footnote>
entre flexibilidad y facilidad de uso, en este capítulo abordamos la herramienta OpenRefine, una solución de uso gratuito instalable en nuestros propios ordenadores y con interfaz gráfica.

## 3.2. La herramienta OpenRefine

OpenRefine permite realizar scraping a partir de fuentes HTML, extraer datos de los formatos XML y JSON y otras operaciones de manipulación y depuración de datos<fnref n="8" />. Tiene funciones básicas de uso simple y en un uso avanzado permite automatizar tareas con funciones programables. La herramienta cuenta con una comunidad que se ocupa de su desarrollo informático, y se usa ampliamente, como lo sugiere la variedad de materiales de formación disponibles, sobre todo en inglés (Williamson, 2017; Van Hooland et al., 2013/2017). Teniendo en cuenta estas características y sus perspectivas de mantenimiento futuro, en tanto que herramienta de código abierto ampliamente utilizada, la hemos elegido para este capítulo, y la sección 4 muestra su uso para scraping.

# 4. PRÁCTICA: OPENREFINE Y PYTHON

Abordamos primero un ejemplo de scraping de fuentes web en HTML, con OpenRefine y después con Python. Por razones de espacio, el scraping con Python se presenta en el repositorio de GitHub que acompaña al capítulo<fnref n="9" />. El repositorio presenta también la extracción de texto a partir de contenidos expresados en XML y en JSON (lenguajes vistos en 2.3).

OpenRefine es multiplataforma (Windows, Linux, Mac). Requiere el entorno Java, pero si no está instalado y no se quiere instalar aparte, la página de descargas proporciona un instalador que incluye el entorno.

* Página de descarga: https://openrefine.org/download.html

* Instrucciones de instalación: https://docs.openrefine.org/manual/installing

Proponemos tareas de scraping de dificultad media, para mostrar diversas posibilidades de la herramienta. Se extraerán poemas del portal Wikisource. Comenzamos por el scraping de un poema individual. Veremos después cómo extraer los URL de cada poema disponible para una autora. Con la lista de los URLs, se podrá aplicar automáticamente a cada uno el procesamiento definido anteriormente para un poema individual.

<footnote n="8">8 https://openrefine.org/, consultado el 02/08/2022.</footnote>

<footnote n="9">9 https://github.com/HD-aula-Literatura/II-2-scraping/tree/main/02-extraccion-con-python.</footnote>
## 4.1. Extraer un poema de Wikisource

Para el poema individual, tomamos "Una viñeta" de Delmira Agustini: https://es.wikisource.org/wiki/Una_vi%C3%B1eta.

Una vez arrancada, la herramienta se muestra en el navegador en http://127.0.0.1:3333/ (figura 3, arriba). Usaremos la función de pegar el URL a analizar desde el portapapeles (Clipboard). Tras confirmar (Next), en la siguiente ventana creamos el proyecto (Create project, figura 3 abajo). Las opciones adecuadas se ven en la figura 3 abajo; la opción *Line-based text files* interpreta cada línea como un URL. El proyecto se llama *AgustiniPoemaIndiv* en el ejemplo.

<figure><img src="image_3.png" /><figcaption>Figura 3. Inicio del proyecto al pegar el URL desde el portapapeles</figcaption></figure>
Para continuar (figura 4), con el menú desplegable de la columna 1, elegimos *Edit column / Add column by fetching URLs*. En la ventana siguiente (figura 5), hemos elegido *htmlOriginal* como nombre de la nueva columna. En el diálogo de la figura 5 cabe destacar la opción *Throttle delay*, que se refiere a los milise-gundos que OpenRefine espera después de descargar una página, antes de descargar la siguiente; esta espera se efectúa como precaución para no hacer peticiones excesivas de modo automático a servidores web. Por defecto la espera son 5 s, pero si tenemos muchos URL lo podemos acortar, p. ej., a 1200 ms.

<figure><img src="image_4.png" /><figcaption>**Figura 4.** Recuperar HTML a partir de los URL de la columna 1 (primera ventana)</figcaption></figure>

Después de lanzar la extracción, la columna *htmlOriginal* (figura 6) conteni-drá el HTML de la página cuyo URL está en *Column 1*. A partir de este HTML extraeremos el poema y metadatos. Mirando el marcado, o buscando (`Ctrl+F`) palabras clave como *autor*, *author*, *título*, *title*, vemos que, dentro del HTML, nos interesan las partes que se ven en las figuras 7 y 8.

Como vemos en la figura 7, el nombre de la autora está en un elemento `<span>` con un atributo class cuyo valor es *ws-author*. El título del poema está en un elemento `<title>`, si bien está acompañado de la expresión `" - Wiki-source"`, que eliminaremos. Para extraer la información, crearemos nuevas columnas a partir de la columna *htmlOriginal* (figura 9).
### Add column by fetching URLs based on column Column 1

New column name

htmlOriginal

Throttle delay

5000

milliseconds

On error

set to blank

store error

Cache responses

HTTP headers to be used when fetching URLs:

Show

<figure><img src="image_5.png" /><figcaption>Figura 6. Columna htmlOriginal creada bajando el HTML a partir de su URL</figcaption></figure>

```html
<title>Una viñeta - Wikisource</title>
<span class="ws-author">Delmira Agustini</span>
```

<caption>Figura 7. HTML para los metadatos</caption>
<figure><img src="image_6.png" /><figcaption>Figura 8. HTML del poema</figcaption></figure>
<figure><img src="image_7.png" /><figcaption>Figura 9. Diálogo para añadir nueva columna a partir de *htmlOriginal*</figcaption></figure>
### Add column based on column htmlOriginal

New column name [autor] (a)

On error ○ set to blank ⚙️ store error ○ copy value from original column

Expression

Language [General Refine Expression Language (GREL)]

value.parseHtml().select("span(class=ws-author)") (b)

No syntax error.

Preview

(c)

History

Starred

Help

<table><caption>**Figura 10.** Etapa intermedia en la extracción del nombre de la autora a partir de la columna *htmlOriginal* guardando el resultado en una nueva columna *autor*</caption><thead><tr><th>row</th><th>value</th><th>value.parseHtml().select("span ... (d)")</th></tr></thead><tbody><tr><td>1.</td><td>&lt;DOCTYPE html&gt;<br/>&lt;html class="client-nojs" lang="es" dir="ltr"&gt;<br/>&lt;head&gt;</td><td>[ &lt;span class="ws-author">&gt;Delmira Agustini&lt;/span&gt; ]</td></tr></tbody></table>

La figura 10 muestra el diálogo principal para extraer información a partir del HTML. En (a) indicaremos el nombre para la nueva columna. En (b) escribimos una expresión que analiza la columna de origen. El lenguaje es *General Refine Expression Language (GREL)*.<fnref n="10" /> En el panel de previsualización, la columna izquierda (c) reproduce el contenido de la columna de origen, y el resultado de la expresión se ve en la parte derecha (d). Explicamos la expresión de extracción de contenido de (b), reproducida aquí como (1):

(1) `value.parseHtml().select("span(class=ws-author")`

* **value**: variable de OpenRefine; se refiere al contenido de la columna de entrada

* **parseHtml()**: como mencionado en 2.2, un documento HTML es una estructura arbórea, representable con una cadena de texto que sigue la sintaxis adecuada. La función parseHtml() crea el árbol del documento a partir de la cadena de texto.

* **select()**: esta instrucción recorre el árbol del documento para identificar los elementos que corresponden a la expresión entre los paréntesis, en este caso **span(class=ws-author)**. La expresión se refiere a los elementos `<span>` que tienen un atributo class cuyo valor es *ws-author* y va entre comillas; no se usan comillas alrededor del valor del atributo, contrariamente al marcado HTML en sí.<fnref n="11" />

<footnote n="10">10 https://docs.openrefine.org/manual/grel visitado el 04/08/2022.</footnote>

<footnote n="11">11 Ver https://jsoup.org/apidocs/org/jsoup/select/Selector.html para las expresiones de selección posibles, que se basan a su vez en el lenguaje CSS: https://languajecss.com/css/.</footnote>
El resultado de la expresión (figura 10 (d)) aparece entre corchetes. Esto quiere decir que se trata de una serie de valores (un *array*). Para acceder al primer elemento, usaremos el índice [0]. Pero no queremos marcado HTML como resultado, sino la cadena de texto contenida en el elemento. La instrucción `innerHtml()` se puede usar para extraer el texto. La expresión final se muestra en (2) y en la figura 11 (a). El resultado es la nueva columna *autora*, con su valor, como se ve en la figura 11 (b).

(2) `value.parseHtml().select("span(class=ws-author)") [0].innerHtml()`

<figure><img src="image_8.png" /><figcaption>Figura 11. Expresión final para extraer el nombre de la autora en la columna *autora*</figcaption></figure>

El título del poema se puede extraer creando una nueva columna *título*, a partir de la columna *htmlOriginal*, cuyo valor se define con la expresión en (3):

(3) `value.parseHtml().select("title").[0].innerHtml().replace(" - Wikisource", "")`

La expresión (3) usa **replace()** para eliminar la cadena superflua " - Wiki-source". La instrucción toma como primer argumento (los argumentos son las secuencias entrecomilladas en los paréntesis) la cadena a reemplazar, y como segundo, la cadena que la reemplaza; aquí, reemplazamos por una cadena vacía, lo que eliminará la primera cadena.

Vamos ahora a extraer el texto del poema, cuyo marcado se ve en la figura 8. Se puede hacer la extracción en una sola etapa, pero para mostrar mejor las funciones de la herramienta, presentamos un estado intermedio de la transformación de datos a efectuar. La expresión en (4) y en la figura 12 extrae un *array* que contiene todos los versos del poema<fnref n="12" />:

<footnote n="12">12 Una expresión más compacta para el <p> del poema es `"div [class = poem] > p"`, ver nota 11.</footnote>
(4) `value.parseHtml().select("div(class=poem)") [0].select("p") [0].innerHtml().split("<br>")`

La instrucción `split()`, en (4), toma `<br>` como argumento, y divide la cadena en partes (en este caso en versos), según las ocurrencias del delimitador elegido (`<br>`)$^{13}$.

<figure><img src="image_9.png" /><figcaption>**Figura 12.** Estado intermedio en extracción del texto del poema. Se muestra para destacar que `split()` devuelve un *array* como resultado</figcaption></figure>

El *array* de resultado en la figura 12 está compuesto de cadenas de texto, una por verso, que se muestran entre comillas y separadas por comas. Hay también cadenas que solo contienen un espacio entre las comillas; corresponden a la separación entre estrofas. A partir de este *array*, vamos a extraer el poema como una única cadena de texto, manteniendo la separación de estrofas, con la expresión (5), usada también en la figura 13. Explicamos después las novedades de la expresión, destacadas en negrita.

<footnote n="13">13 ¿Por qué usamos `<br>` en (4), si la fuente HTML expresaba los elementos como `<br/>`? Es porque manipulamos el árbol HTML, no cadenas de texto literales, y el analizador HTML de OpenRefine, activado con `parseHtml()`, representa estos elementos como `<br>`, como se comprueba con el resultado de `value.parseHtml().select("div(class = poem") [0].select("p") [0].innerHtml()` en el panel de extracción. Si en algún caso manipulamos cadenas (con el valor de value, sin aplicar `parseHtml()`), la expresión usada como argumento de `split()` deberá ser exacta.</footnote>
(5) `forEach(value.parseHtml().select("div[WCLASS=POEM]") [0].select("p") [0].innerHTML().unescape("html").split("<br>"), verso, verso.trim()).join("\n")`

* `unescape("html")`: sustituye las entidades HTML (ver 2.2) por el carácter que representan, en este caso sustituye `&nsbp;` por un espacio de no separación.

* `forEach(SECUENCIA, NOMBRE Variable, EXPRESIÓN)`: Existe un constructo *for each* en muchos lenguajes informáticos, para iterar sobre una secuencia y manipular el contenido de sus miembros. Este tipo de estructura se conoce como bucle (loop). En el lenguaje utilizado en OpenRefine (GREL),¹⁰ la instrucción `forEach` toma tres argumentos: una secuencia (*array*), el nombre de la variable que servirá para recorrer los miembros de la secuencia (es decir, un iterador) y una expresión que típicamente mencionará a este iterador, para efectuar una operación sobre cada miembro de la secuencia. En el ejemplo, la secuencia es el *array* que contiene las cadenas que corresponden a los versos, ya visto en la figura 9, con la diferencia de que ahora las entidades HTML están reemplazadas por los caracteres que representan. El iterador, la variable para recorrer la secuencia, es verso (si bien se puede usar cualquier otro nombre). El bucle `forEach` asigna iterativamente a la variable verso cada miembro del *array* (el contenido de cada verso en este caso), desde el primer hasta el último elemento del *array*.

* `trim()`: Con cada verso, dentro del bucle se efectúa esta operación, que retira espacios en blanco, si los hay, del inicio y fin de una cadena de texto. En este caso sí que los hay, como se ve en la figura 9, y `trim()` los eliminará.

* `join("\n")`: Esta función toma un *array* de cadenas de texto como entrada, y las une usando la secuencia de caracteres (delimitador) que se le da como argumento (entrecromillada dentro del paréntesis). La secuencia elegida aquí es `\n`, que representa un salto de línea. Por eso, el resultado que será almacenado en la columna *textPoema* es el texto del poema. Las separaciones de estrofas se deben a las cadenas dentro del *array* que sólo contienen espacios. Al aplicar `trim()`, estos espacios desaparecen, pero queda en el *array* una cadena vacía o de longitud 0 caracteres. Al aplicar `join("\n")`, un salto de línea adicional se inserta, que crea la división de estrofas. Cabe mencionar también que las celdas de OpenRefine no pueden almacenar *arrays*, con lo que para almacenar el resultado de una operación hay que usar, bien `toString()` como instrucción final, bien `join()` con un delimitador adecuado. El delimitador puede ser una cadena vacía, como en `.join("")`; esto une las cadenas inmediatamente una tras la otra. Si intentamos guardar un *array* (sin `join`) y hemos activado la opción *store error* (como en las figuras de este capítulo), OpenRefine mostrará en la columna un mensaje de error que nos advierte del problema. Para guardar cada miembro del *array* en líneas separadas (siempre dentro de la misma celda OpenRefine), el delimitador es `\n`.

La tabla 2 lista las etapas seguidas para extraer la autora, título y texto del poema.
<table><caption>Tabla 2. Expresiones de extracción aplicadas al HTML obtenido para el URL del poema</caption><thead><tr><td>Columna de entrada</td><td>Columna creada</td><td>Expresión de OpenRefine</td></tr></thead><tbody><tr><td>htmlOriginal</td><td>autora</td><td>value.parseHtml(). select("span(class = ws-author) ") [0]. innerHtml()</td></tr><tr><td>htmlOriginal</td><td> titulo</td><td>value.parseHtml().select("title") [0]. innerHtml().replace(" - Wikisource", "")</td></tr><tr><td>htmlOriginal</td><td>textoPoema</td><td>forEach(value.parseHtml(). select("div(class = poem") [0]. select("p") [0]. innerHtml(). unescape("html").split("<br>") , verso, verso.trim()).join("\n")</td></tr></tbody></table>

Add column based on column htmlOriginal

New column name textoPoema

On error

<figure><img src="image_10.png" /></figure>

set to blank

<figure><img src="image_11.png" /></figure>

store error

<figure><img src="image_12.png" /></figure>

copy value from original column

Expression

Language General Refine Expression Language (GREL)

forEach(value.parseHtml().select("div(class=poem)") [0].select("p") [0].innerHtml().unescape("html").split("<br>") , verso, verso.trim()).join("\n")

No syntax error.

<figure><img src="image_13.png" /><figcaption>Figura 13. Expresión para extraer el texto limpio del poema, con sus estrofas, a partir de la columna htmlOriginal y guardarlo en una nueva columna textoPoema</figcaption></figure>

## 4.2. Extracción de los URL para todos los poemas de una autora

Vemos ahora cómo extraer los URL para todos los poemas de una autora, en este caso Alfonsina Storni: https://es.wikisource.org/wiki/Autor:Alfonsina_Storni. Nos fijaremos en la estructura de la página, visualmente y en la fuente HTML. Con el menú contextual del navegador (botón derecho del ratón) hay opciones para ver el código fuente y para inspeccionar el HTML para las partes de la página que nos interesan. Si colocamos el ratón encima de uno de los enlaces que nos interesa extraer (Alma desnuda), con la opción inspeccionar
obtenemos la información siguiente (figura 14): Los enlaces (elementos `<a>`) están bajo elementos `<ul>` (listas no ordenadas). Seleccionaremos estos elementos con OpenRefine.

<figure><img src="image_14.png" /><figcaption>**Figura 14.** Uso de la función de inspección de la fuente HTML del navegador (Firefox en este caso). Vemos el elemento HTML `<ul>` para la lista de enlaces resaltada en la página</figcaption></figure>

Después de copiar el URL inicial y crear un proyecto (ver figura 3), creamos la columna del HTML original, a partir de la cual extraeremos los enlaces. Tendremos en cuenta que el primer elemento `<ul>` (*Obras*) no contiene enlaces a poemas individuales, y lo ignoraremos. Además, los enlaces no muestran el esquema (*https*) ni el servidor; comienzan con la ruta `/wiki/`, y habrá que añadir la parte que falta.

Aprovechando las funciones del lenguaje GREL de OpenRefine y el panel de previsualización de resultados, podemos probar expresiones hasta extraer exclusivamente los enlaces deseados. Empezamos con la expresión en (6), y guardaremos su resultado en una columna *enlacesTemp* (figura 15).

(6) `forEach(value.parseHtml().select("ul").slice(1,100).join("").parseHtml().select("a mw-redirect\]"), enlace, enlace.htmlAttr("href")).join("\n")`

Como vimos en (5), el primer argumento del bucle `forEach` tiene que ser una secuencia. La secuencia creada aquí es una lista de enlaces (elementos `<a>` dentro de listas `<ul>`). Estos se han obtenido como sigue: Primero se seleccionan
todas las listas <ul>. Después con slice (1,99), se ignora la primera (índice 0) y se seleccionan las listas desde la segunda hasta la 100. Hay menos de 100 listas, pero cuando el número solicitado es mayor a las disponibles, GREL selecciona todas las posibles sin dar error. El resultado de slice() es un array. Para poder extraer los elementos <a> dependientes de los <ul> seleccionados, la opción elegida es crear una cadena a partir del array con join(""), después crear un árbol HTML a partir de la cadena, con parseHtml(), y seleccionar los elementos <a>. Si miramos los elementos <a> en la fuente HTML, veremos que algunos llevan el atributo class con valor mw-redirect y que no corresponden a poemas de Storni (son redirecciones a otros autores o autoras). Esto es la razón para usar la expresión a(class!= mw-redirect], que acepta solo aquellos <a> cuyo class no sea igual (operador !=) a mw-redirect, o que no lleven class. El segundo argumento del bucleforEach es una variable, aquí llamada enlace, para recorrer la secuencia. El tercer argumento aplica, gracias a la variable enlace, la instrucción htmlAttr("href") a cada elemento de la secuencia. Esta instrucción extrae el valor del atributo href (URL del enlace) de los elementos <a>. Para guardar el resultado, se debe crear una cadena a partir del resultado del bucle. Por eso se usa .join("\n"), que une los enlaces extraídos con un salto de línea, tras el paréntesis que cierra el bucle.

<figure><img src="image_15.png" /><figcaption>**Figura 15.** (a) Selección de los enlaces de los poemas individuales. (b) Inicio de la lista de resultados (enlaces correctos). (c) Final de la lista que muestra enlaces erróneos, que filtraremos después con otra expresión</figcaption></figure>
Inspeccionando el resultado, el principio de la lista (figura 15 b) contiene rutas (partes de URL) correctas, pero hacia el final (figura 15 c) vemos que hay enlaces que no corresponden a poemas, que contienen expresiones como *Acerca_de*, *wikimedia* o *mediawiki*. Filtraremos los enlaces de la columna *enlacesTemp* para seleccionar exclusivamente los que se refieren a poemas, con la expresión (7). Bajo la expresión se explica su funcionamiento.

(7) `forEach(filter(value.split("\n"), enlace, and(enlace contains("/wiki/"), not (enlace.contains (/:/|Portada/))), enlaceLimpio, "https://es.wikisource.org" + enlaceLimpio).join ("\n")`

* `value.split("\n")` crea un array con el contenido de cada celda de la columna. Cabe señalar que ahora trabajamos con cadenas de texto, no con un árbol HTML (no se ha aplicado `parseHtml()`), ya que el contenido de *enlacesTemp* no representaba elementos HTML sino simples cadenas de texto.

* `filter(secuencia, iterador, test)` hace lo siguiente: Filtra el contenido de la secuencia de su primer argumento, según el test que se le da como tercer argumento, usando para aplicar el test a cada elemento la variable (iterador) que se le da como segundo argumento. Devuelve como resultado un array, cuyo contenido son los elementos de la secuencia original que pasan el test. En nuestro ejemplo, la secuencia original es el resultado de `value.split("\n")` y el iterador es la variable `enlace`. El test tiene dos condiciones, siguiendo la sintaxis **and**(**condición 1**, **condición 2**). En la primera condición, `and(enlace.contains("/wiki/"))`, se verifica si el enlace contiene la cadena `/wiki/`, que como cadena de texto debe darse entrecomillada. La segunda condición, `not(enlace.contains(/:/|Portada/))`, exige que el enlace no contenga cadenas que correspondan a la expresión regular `:/Portada`, especificada como argumento.⁶ Como expresión regular debe escribirse entre barras oblicuas. Esta expresión busca cadenas que contienen el signo dos puntos o la cadena *Portada* (el operador `|` separa las cadenas que se buscan).

Una vez que `filter()` ha seleccionado los enlaces que nos interesan, debemos prefijar la parte del URL que no estaba en los enlaces. La salida de `filter()` es un array. Como tal, lo recorremos con un bucle `forEach`, y a cada elemento le prefijamos la cadena `https://es.wikisource.org` con el operador de concatenación de cadenas + (*signo más*). La variable `enlaceLimpio` hace de iterador (toma el valor de cada elemento al recorrer el bucle). Para guardar el resultado en la columna *enlacesLimpios*, debemos pasar del array a una cadena, con `.join("\n")`.

Con los enlaces finales, podemos copiar el contenido de la columna con el botón *edit* que aparece pasando el ratón. Tras copiar los enlaces, podemos
iniciar un nuevo proyecto que, usando las mismas expresiones vistas en 4.1 para extraer un poema de Delmira Agustini (resumidas en la Tabla 2), extraiga el contenido y metadatos de cada poema. El marcado HTML es muy similar en los poemas de Wikisource para Agustini y Storni, con lo que podemos usar las mismas expresiones para las dos.

<figure><img src="image_16.png" /><figcaption>Figura 16. Enlaces finales para los poemas</figcaption></figure>

OpenRefine permite exportar el proyecto (botón *Export* arriba a la derecha) en varios formatos, como CSV o las hojas de cálculo de tipo ODS y XLSX. Hay una función *Custom tabular exporter* que nos permite seleccionar las columnas a exportar (figura 14) y creará una salida en formato delimitado o en Excel (XLSX). Podemos elegir no exportar columnas con resultados intermedios como *enlacesTemp* en nuestro ejemplo. Tras seleccionar las columnas, desde la pestaña *Download* se efectúa la exportación.
<figure><img src="image_17.png" /><figcaption>**Figura 17.** Diálogo de exportación *Custom tabular exporter* de un proyecto OpenRefine: Selección de columnas. También se puede cambiar el orden de ellas aquí. Para exportar, ir a *Download*</figcaption></figure>

## 4.3. Generación de marcado a partir de los contenidos extraídos

Con OpenRefine hemos generado contenidos delimitados (el tipo de documento que se puede abrir con una hoja de cálculo). En nuestro caso, cada línea representa la información disponible para un texto, con lo que las columnas contienen el texto en sí además de sus metadatos (autora, título, URL original). Es un formato de uso amplio; no obstante, si nuestra herramienta de análisis textual no gestiona este formato, podríamos tener interés en transformar este formato a otros, como TEI. Para esto, en casos simples se podrían usar expresiones regulares o bien usar medios más específicos para cada formato de salida (como bibliotecas para análisis y generación de XML en el caso de TEI). Estas posibilidades se ven en el repositorio que acompaña al capítulo<fnref n="14" />.

<footnote n="14">14 https://github.com/HD-aula-Literatura/II-2-scraping/tree/main/03-marcado-automatico.</footnote>
## 5. APLICACIÓN DIDÁCTICA

Un primer ejercicio sería añadir a la extracción el íncipit (primer verso) de los poemas de Storni. En la figura 11 vemos su marcado (elementos <i>).

Como actividad más compleja en la que varias personas pueden colaborar, se puede trabajar con la plataforma de colección de citas Wikiquote en español: https://es.wikiquote.org/. Recientemente, la comunidad de estudios literarios computacionales ha mostrado interés por la representación de la literatura e historia literaria en plataformas colaborativas como Wikipedia, como muestra el llamado a contribuciones de la revista *Cultural Analytics* en 2021.<fnref n="15" /> Una de estas plataformas es Wikiquote, que tiene citas agrupadas por autoría o por obras literarias. Se puede asignar a diferentes personas el scraping de una autora o autor de literatura, con el objetivo final de tener texto de citas, acompañado de metadatos, que permita comparar el contenido de las citas según los metadatos. P. ej. comparar las citas disponibles para obras escritas por autoras vs. autores, o comparar por período. Los metadatos como el nombre de la personalidad a quien pertenece la cita, y en algunos casos sus fechas de nacimiento y muerte, se pueden extraer del HTML.

## REFERENCIAS BIBLIOGRÁFICAS

* OpenRefine. (2022). OpenRefine user manual. Consultado el 05/08/2022 en https://docs.openrefine.org/

* Román, J. (Manz). (2022). Lenguaje HTML5. Consultado el 11/08/2022 en https://lenguajehtml.com/html/

* Román, J. (Manz). (2022). Lenguaje CSS. Consultado el 11/08/2022 en https://lenguajecss.com/css/

* Ruiz Fabo, P. (2022). Tutorial de expresiones regulares: Manipulación de cadenas de texto. Zenodo. https://doi.org/10.5281/zenodo.6981766

* TEI Consortium. (2022). TEI P5: Guidelines for Electronic Text Encoding and Interchange (v4.4.0). Zenodo. https://doi.org/10.5281/zenodo.6482461

* Van Hooland, S, Verborgh, R, De Wilde, M. (2017). Limpieza de datos con OpenRefine (Colmenero-Ruiz, M.-J., Trad.) https://doi.org/10.46430/phes01017 (Original publicado en 2013)

* Williamson, E. P. (2017). Fetching and Parsing Data from the Web with Open-Refine. *Programming Historian* 6. https://doi.org/10.46430/phen0065

<footnote n="15">15 https://culturalanalytics.org/post/958-cfp-world-literature-and-wikipedia (consultado el 04/08/2022).</footnote>
<figure><img src="image_18.png" /></figure>