Cómo hacer Web scraping básico con Java

¿Qué es el Web Scraping?

Web scraping es la técnica que se utiliza para extraer información de páginas web de forma automática. Consiste en leer el código de una página para obtener datos en bruto y transformarlos en datos estructurados que se pueden guardar en bases de datos u hojas de cálculo para analizar y extraer aquello que nos interesa.
Esta técnica se puede utilizar para recolectar datos de forma masiva con lo que podríamos decir que es parte de lo que conocemos como Big Data.

Según la Wikipedia, el web scraping también se puede utilizar para:

  • comparar precios en tiendas
  • monitorizar datos relacionados con el clima de cierta región
  • detectar cambios en sitios webs
  • integrar datos en sitios webs.

En mi caso más de una vez (y de dos) me he encontrado visitando la misma página varios días esperando un cambio, una actualización de información o una noticia que no acababa de llegar. Por eso se me ocurrió hace unos días crear un pequeño ejemplo de aplicación Java que hiciera este trabajo por mi.

La idea es que, sabiendo más o menos como se maqueta una web determinada y en qué URL se va a publicar la información que queremos, la aplicación detecte automáticamente cuando ocurre ese cambio y nos avise.

En este artículo solo voy a desarrollar el código de la aplicación (Lo puedes revisar o descargar completo desde aquí). La otra parte sería programar la ejecución de esta aplicación cuando a nosotros nos interese: al inicio de sesión, cada hora, cada 5 minutos o solo los viernes 13 a las 12 de la noche.
Si no sabes por donde empezar con esta tarea, aquí te dejo un artículo sobre cómo ejecutar programas al inicio de sesión en Linux con una pequeña introducción sobre como programarlos periódicamente con Cron.

Antes de empezar

Asumo que tienes unos conocimientos básicos de HTML. No hay que ser experto, pero si saber que existen etiquetas, clases, id’s y atributos y cómo utilizarlos.

Voy a utilizar una librería para extraer y manejar el código HTML. No es imprescindible, pero facilita mucho las cosas. He escogido Jsoup, pero cualquier otro parser HTML te puede servir.

Voy a proponer una situación ficticia en la que queremos comprobar todos los días si hay nuevos artículos en una tienda. Seguramente se te ocurran mejores utilidades para el web scraping, o te puede parece absurdo, porque tardamos menos en mirarlos por nuestra cuenta o directamente contactando con la tienda para que nos avise cuando tenga algo nuevo, pero como ejemplo nos va a servir.

Imaginemos que queremos observar una tienda online de ropa y queremos saber cuando hay nuevos abrigos. Para saber si hay nuevos artículos en algún momento, la primera vez tendremos que guardar lo que hay. Para hacerlo fácil, voy a escribir las descripciones de artículos y los enlaces en un documento txt que después utilizaré para hacer la comparación.

Al turrón

La URL para ver los abrigos es la siguiente (no los conozco, no penséis mal):

http://www.esprit.es/chaquetas-abrigos-hombre

Ahora tenemos que pararnos un poco a analizar. Primero tenemos observamos cómo se forma la URL para cada página de artículos que pueda tener. En este caso simplemente añade -page2 al final:

http://www.esprit.es/chaquetas-abrigos-hombre-page2

Pero es habitual que se añadan parámetros para identificar la página y otras cosas, por ejemplo:

http://www.tolflou.com?cid=4210&via=top#parentID=-1&pge=1&pgeSize=36&sort=-1

Aquí el parámetro que nos interesaría sería “pge”.

Ahora tenemos que identificar mediante ID’s o clases, las partes de la página que nos interesan.
Las páginas se suelen organizar en cajitas, que se definen con etiquetas <div > unas dentro de otras y normalmente hay una cajita que contiene todos los artículos que se muestran en la pantalla.
Para empezar a buscar nuestra caja maestra, basta con hacer click derecho encima de uno de los artículos y seleccionar “inspeccionar elemento”. Esto nos marcará dentro de una ventanita, el HTML el elemento que hemos seleccionado. Ahora vamos pasando el ratón por encima del código de otros elementos y vemos que nos lo señala en pantalla. Sabiendo que las cajas suelen estar anidadas, solo nos queda ir seleccionando una caja de nivel superior, hasta encontrar la que contiene todos los elementos que queremos observar.

recvisar-codigo-html

Cuidado con subir demasiado, porque puedes acabar seleccionando la etiqueta <body> que incluye todo lo que se ve en pantalla. No es que sea malo, pero vas a leer información de más y luego vas a tener que indagar más hasta que llegues a donde quieres realmente.

Volviendo al caso de los abrigos, el id de la caja que contiene todos los abrigos es id=”styleoverview” y tiene una forma parecida a esta:

...
 

(abrigo1) ...

Ya tenemos identificada la etiqueta y la URL. Empezamos con el código que va a leer la web

Al turrón, duro

Jsoup permite hacer peticiones HTTP y obtener el “status code” (404 no encontrado, 200 ok, etc…), así que comprobamos primero que la petición a nuestra URL funciona correctamente:

Response response = null;
	    try {
	    	response = Jsoup.connect(url).userAgent("Mozilla/5.0").timeout(100000).ignoreHttpErrors(true).execute();
	    } catch (IOException ex) {
	    	System.out.println("Excepción al obtener el Status Code: " + ex.getMessage());
	    }
	    return response.statusCode()

Necesitamos obtener el código de la web. Podemos hacerlo por nuestra cuenta, leyendo el html fila a fila con un bucle, o más fácil y rápido utilizando el siguiente método:

Document doc = Jsoup.connect(url).userAgent("Mozilla/5.0").timeout(100000).get();

Así obtenemos el objeto Document con el dódigo HMTL completo con el que vamos a empezar a buscar y extraer información con los métodos de la librería Jsoup.
De todo el archivo HTML vamos haciendo nuestra selección. Recogemos el primer div que contenía todos los artículos:

 
Element primerDiv = doc.getElementById("styleoverview");

Ahora recogemos todos los div con la clase “style”:


Elements articulos = primerDiv.getElementsByClass("style");

Y por último seleccionamos el texto que queríamos extraer en cada elemento:

String  documentoProcesado = "";
for (Element articulo : articulos){
    //El selector span:nth-child(x) busca al padre de span y elige al elemento hijo en la posición x
    documentoProcesado += "\n"+articulo.select("p.style-name span:nth-child(2)").text()
    +" -- "+articulo.getElementsByTag("a").attr("href");
}

Si no te convencen los selectores que he utilizado, en este enlace puedes ver como se utilizan por si quieres hacerlo de otra manera.

La variable “documentoProcesado” guarda el texto que queríamos extraer y que luego voy a a guardar en un archivo de texto.

Lo siguiente es guardar el contenido de esta variable en un documento de texto:

public static Boolean escribirArchivo(String textoProcesado, File archivo){
	FileWriter escritor = null;
	Boolean escribeBien;
	try{
		escritor = new FileWriter(archivo);
		escritor.write(textoProcesado);
		escribeBien = true;
	}catch(FileNotFoundException e){
		System.out.println("No existe el fichero o la carpeta");
		JOptionPane.showMessageDialog(new JFrame(), "Ha ocurrido algún error creando el nuevo fichero"
				+" \n\nEn la carpeta:\n"+carpetaFicheros);
		escribeBien = false;
	}catch(IOException e){
		JOptionPane.showMessageDialog(new JFrame(), "Ha ocurrido algún error creando el nuevo fichero"
				+" \n\nEn la carpeta:\n"+carpetaFicheros);
		escribeBien = false;
	}finally{
		if (escritor != null){
			try {
				escritor.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	return escribeBien;
}

También tenemos que ver como podríamos leer el archivo de texto y compararlo. Recojo las lineas que sean diferentes en un ArrayList, para poder mostrar solo los nuevos abrigos:

public static ArrayList<String[]> compararArchivoYCodigo(File fil, String textoProcesado){
	FileReader archivo;
	BufferedReader lectorArchivo = null;
	BufferedReader lectorTextoPro = null;
	ArrayList<String[]> diferencia = new ArrayList();		
	try{
		archivo = new FileReader(fil);
		lectorArchivo = new BufferedReader(archivo);
		
		String lineaArchivo, lineaTextoPro;
		Boolean repetida;
		/*No podemos comparar linea a linea, porque si se introduce un nuevo abrigo
		 * en primera posición, no coincidiría ninguno y mostraría todos como nuevos
		 */
		while ((lineaArchivo = lectorArchivo.readLine()) != null){
			repetida = false;
			lectorTextoPro = new BufferedReader(new StringReader(textoProcesado));
			while((lineaTextoPro=lectorTextoPro.readLine()) != null){
				if(lineaArchivo.equals(lineaTextoPro)){
					repetida = true;
					break;
				}
			}
			if(!repetida){
				String[] datos= lineaArchivo.split("--");
				diferencia.add(datos);
			}	
		}	
	}catch(FileNotFoundException e){
		e.getMessage();
	}catch (IOException e) {
		e.getMessage();
	}finally{
		if (lectorArchivo != null){
			try {
				lectorArchivo.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}else if (lectorTextoPro != null){
			try {
				lectorTextoPro.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	return diferencia;
}

Ya solo nos falta organizarlo todo. Aquí dejo el enlace al proyecto completo en Github para que veas una forma de hacerlo. Cualquier duda o sugerencia es bienvenida.

 

Fuentes:

Documentación de OracleDocumentación de Mozilla (para abrir nuevas pesatañas)
Mkyiong.com
Documentación de Jsoup
Jarroba (Los métodos getStatusConnectionCode y getHtmlDocument están cogido de este tutorial. Tienen un blog muy recomendable sobre programación)
Artículo sobre el tema en Wikipedia