<jupyter_start><jupyter_text>Web scrapingCuando necesitamos extraer información publicada en internet, lo ideal es consultar una API, porque:* Las respuestas contienen información estructurada* En general, el propio servicio nos da documentación sobre cómo hacer peticiones y qué tipo de información podemos solicitarPero muchas veces nos encontramos con información en páginas web (en formato [HTML](https://es.wikipedia.org/wiki/HTML)) que nos gustaría obtener, pero sin API disponible.Estas páginas `HTML` tienen cierta estructura, aunque con ciertos contras:* Es más compleja, puede tener muchos niveles de anidamiento* Es inestable. Están diseñadas para que se vean bien desde el explorador, no para guardar una estructura de consulta. De un día para otro, puede verse alterada por la incorporación de nuevos elementos visuales u otros motivos.* Puede ser modificada por código cliente (javascript) en diferentes momentos: al cargar la página, al interaccionar con algún elemento, ...  Scraping de elementos htmlLa librería que vamos a utilizar es [Beautiful Soup](https://pypi.org/project/beautifulsoup4/). Nos permite buscar elementos y navegar por la estructura del html fácilmente.Vemos dos ejemplos, uno sobre milanuncios y otro sobre spotahome, por si nos _banean_. milanunciosImaginemos que queremos comparar precios de un determinado modelo de motocicleta de segunda mano. P.e. con [esta búsqueda](https://www.milanuncios.com/motos-de-carretera/duke-390.htm) en milanuncios.La mayor parte de las webs con contenido interesante (que hacen negocio gracias a su contenido) intentan protegerlas para evitar que les hagan scraping. Hay varias formas de simular que nuestro script es humano en lugar de un bot, algunas más básicas y otras más complejas. Por ahora, vamos a sobrescribir nuestro _user agent_. Es una cabecera que va en las peticiones diciendo quiénes somos (p.e. qué tipo de explorador usamos). Por defecto, la librería `requests` que vamos a usar, avisa que somos un bot. Vamos a sobrescribir esta cabecera para _disimular_ un poco. Podemos copiar uno popular de (aquí)[https://developers.whatismybrowser.com/useragents/explore/software_type_specific/web-browser/].<jupyter_code>import requests
from bs4 import BeautifulSoup
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
}<jupyter_output><empty_output><jupyter_text>Ahora, nos descargamos el html con `requests`.<jupyter_code>page = requests.get('https://www.milanuncios.com/motos-de-carretera/duke-390.htm', headers=headers)
page<jupyter_output><empty_output><jupyter_text>Podemos ver el contenido examinando la propiedad `content`.<jupyter_code>page.content<jupyter_output><empty_output><jupyter_text>Este contenido es solo texto, no tiene estructura. Aún no podemos hacer búsquedas ni navegar por él.Para hacerlo, creamos una instancia de `Beautiful Soup` y lo parseamos<jupyter_code>soup = BeautifulSoup(page.content, 'html.parser')<jupyter_output><empty_output><jupyter_text>SOS: ¡me han baneado!Si estamos haciendo esto en clase, es muy probable que a la mayoría, nos detecten como bots y nos baneen. Esto pasa porque somos 30 o 40 personas, haciendo la misma petición desde la misma IP y con el mismo user agent.Puedes usar el html offline que hay en `dat` para seguir con el ejercicio. Descomenta el siguiente código y sigue adelante<jupyter_code># Descomenta esto si te han baneado
# with open('dat/milanuncios.html') as f:
#     soup = BeautifulSoup(f, 'html.parser')<jupyter_output><empty_output><jupyter_text>Sobre esto, podemos hacer búsquedas con `find` y `find_all` (o `select_one` y `select` si prefieres utilizar [selectores css](https://en.wikipedia.org/wiki/Cascading_Style_SheetsSelector)). Sobre nuestro ejemplo, vamos a buscar todos los precios. Examinando el código fuente, vemos que son etiquetas `div` con clase `aditem-price`.<jupyter_code>div_precios = soup.find_all('div', class_='aditem-price')
div_precios<jupyter_output><empty_output><jupyter_text>`find_all` devuelve una lista de elementos. Sobre ellos, podemos hacer:`children` para sacar el listado de todos los hijos.<jupyter_code>list(div_precios[0].children)<jupyter_output><empty_output><jupyter_text>`get_text()` para sacar el texto de todos los hijos<jupyter_code>div_precios[0].get_text()<jupyter_output><empty_output><jupyter_text>Por tanto, para sacar el listado de todos los precios podemos hacer:<jupyter_code>[list(div_precio.children)[0] for div_precio in div_precios]<jupyter_output><empty_output><jupyter_text>Tienes más funciones útiles con pequeños ejemplos [aquí](http://akul.me/blog/2016/beautifulsoup-cheatsheet/)  Scraping de tablasA menudo, la información que nos interesa descargar está en tablas y nuestro objetivo es importarlas en tablas de Pandas. Esta conversión suele exigir la manipulación del texto, números y fechas contenidas en la tabla original, lo que nos obligará a repasar cómo realizar esas operaciones y aplicarlas a filas y columnas de las tablas.La estructura que suelen tener la tablas en `html` es:```                        Columna A            Columna B                                    A1            B1                            A2            B2               ``` Necesitaremos los siguientes módulos además de `requests` y `BeautifulSoup` importados anteriormente:<jupyter_code>import pandas as pd
import re<jupyter_output><empty_output><jupyter_text>Primero, hacemos una petición para descargar la página de interés (que contiene las cotizaciones de las acciones del IBEX 35 en tiempo _casi_ real).<jupyter_code>base_url = "https://www.eleconomista.es/indice/IBEX-35"
res = requests.get(base_url)
contenido = res.content<jupyter_output><empty_output><jupyter_text>La siguiente línea procesa el HTML de la página que hemos descargado:<jupyter_code>soup = BeautifulSoup(contenido, "html.parser")<jupyter_output><empty_output><jupyter_text>Una vez procesado el HTML, es posible buscar elementos dentro de él. En particular, podemos buscar los elementos de tipo `table`, es decir, tablas.<jupyter_code>tablas = soup.find_all('table')<jupyter_output><empty_output><jupyter_text>El objeto `tablas` contiene todas las tablas presentes en la página. Hay que tener cuidado con dichas tablas porque muchas páginas utilizan elementos de tipo `table` para estructurar el contenido. Por eso, en algunas páginas, aunque parezca haber una única tabla, puede haber otras con una información no interesante que toca descartar.<jupyter_code>len(tablas)<jupyter_output><empty_output><jupyter_text>Podemos extraer las filas de todas estas tablas<jupyter_code>lineas = [x for tabla in tablas for x in tabla.find_all('tr')]<jupyter_output><empty_output><jupyter_text>para luego extraer los contenidos de cada fila individualmente haciendo<jupyter_code>datos = [[x.text for x  in linea.find_all('td')] for linea in lineas]<jupyter_output><empty_output><jupyter_text>Podemos inspeccionar parte del objeto resultante:<jupyter_code>datos[0:3]<jupyter_output><empty_output><jupyter_text>Vemos que hay filas que contienen la información de interés junto con otras que contienen cabeceras y otra información irrelevante. En general, la situación puede ser más complicada y se hace necesario estudiar el objeto `tablas` para seleccionar la de interés.En nuestro caso, podemos filtrar las líneas menos relevantes así:<jupyter_code>datos = [x for x in datos if len(x) > 0]<jupyter_output><empty_output><jupyter_text>Finalmente, podemos crear una tabla de Pandas:<jupyter_code>datos = pd.DataFrame(datos)
datos<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07403517983722761, 'fraction_numerical': 0.007744814912050407, 'mean_word_length': 5.730565371024735, 'pattern_counts': {'":': 0, '<': 77, '<?xml version=': 0, '>': 78, 'https://': 7, 'lorem ipsum': 0, 'www.': 3, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '209320', 'license': 'MIT', 'n_tokens_mistral': 2594, 'n_tokens_neox': 2546, 'n_words': 828, 'path': 'notebooks/05_web_scraping.ipynb', 'repo_name': 'jftknow/jftknow.github.io', 'size': '7618'}