Cómo raspar sitios web con Python 3

El web scraping es el proceso de extraer datos de sitios web.

Antes de intentar raspar un sitio web, debe asegurarse de que el proveedor lo permita en sus términos de servicio. También debe verificar si puede usar una API en su lugar.

El raspado masivo puede poner a un servidor bajo mucho estrés, lo que puede resultar en una denegación de servicio. Y no quieres eso.

¿Quién debería leer esto?

Este artículo es para lectores avanzados. Asumirá que ya está familiarizado con el lenguaje de programación Python.

Como mínimo, debe comprender la comprensión de listas, el administrador de contexto y las funciones. También debe saber cómo configurar un entorno virtual.

Ejecutaremos el código en su máquina local para explorar algunos sitios web. Con algunos ajustes, también podría ejecutarlo en un servidor.

Qué aprenderá en este artículo

Al final de este artículo, sabrá cómo descargar una página web, analizarla en busca de información interesante y formatearla en un formato utilizable para su posterior procesamiento. Esto también se conoce como ETL.

Este artículo también explicará qué hacer si ese sitio web utiliza JavaScript para representar contenido (como React.js o Angular).

Prerrequisitos

Antes de que pueda comenzar, quiero asegurarme de que estemos listos para comenzar. Configure un entorno virtual e instale los siguientes paquetes en él:

  • beautifulsoup4 (versión 4.9.0 al momento de escribir este artículo)
  • solicitudes (versión 2.23.0 al momento de escribir este artículo)
  • wordcloud (versión 1.17.0 al momento de escribir este artículo, opcional)
  • selenio (versión 3.141.0 al momento de escribir este artículo, opcional)

Puede encontrar el código para este proyecto en este repositorio de git en GitHub.

Para este ejemplo, vamos a eliminar la Ley Básica de la República Federal de Alemania. (No se preocupe, verifiqué sus Condiciones de servicio. Ofrecen una versión XML para procesamiento automático, pero esta página sirve como ejemplo de procesamiento de HTML. Por lo tanto, debería estar bien).

Paso 1: descarga la fuente

Lo primero es lo primero: creo un archivo que urls.txtcontiene todas las URL que quiero descargar:

//www.gesetze-im-internet.de/gg/art_1.html //www.gesetze-im-internet.de/gg/art_2.html //www.gesetze-im-internet.de/gg/art_3.html //www.gesetze-im-internet.de/gg/art_4.html //www.gesetze-im-internet.de/gg/art_5.html //www.gesetze-im-internet.de/gg/art_6.html //www.gesetze-im-internet.de/gg/art_7.html //www.gesetze-im-internet.de/gg/art_8.html //www.gesetze-im-internet.de/gg/art_9.html //www.gesetze-im-internet.de/gg/art_10.html //www.gesetze-im-internet.de/gg/art_11.html //www.gesetze-im-internet.de/gg/art_12.html //www.gesetze-im-internet.de/gg/art_12a.html //www.gesetze-im-internet.de/gg/art_13.html //www.gesetze-im-internet.de/gg/art_14.html //www.gesetze-im-internet.de/gg/art_15.html //www.gesetze-im-internet.de/gg/art_16.html //www.gesetze-im-internet.de/gg/art_16a.html //www.gesetze-im-internet.de/gg/art_17.html //www.gesetze-im-internet.de/gg/art_17a.html //www.gesetze-im-internet.de/gg/art_18.html //www.gesetze-im-internet.de/gg/art_19.html

A continuación, escribo un poco de código Python en un archivo llamado scraper.pypara descargar el HTML de estos archivos.

En un escenario real, esto sería demasiado caro y utilizaría una base de datos en su lugar. Para simplificar las cosas, descargaré archivos en el mismo directorio junto a la tienda y usaré su nombre como nombre de archivo.

from os import path from pathlib import PurePath import requests with open('urls.txt', 'r') as fh: urls = fh.readlines() urls = [url.strip() for url in urls] # strip `\n` for url in urls: file_name = PurePath(url).name file_path = path.join('.', file_name) text = '' try: response = requests.get(url) if response.ok: text = response.text except requests.exceptions.ConnectionError as exc: print(exc) with open(file_path, 'w') as fh: fh.write(text) print('Written to', file_path)

Al descargar los archivos, puedo procesarlos localmente tanto como quiera sin depender de un servidor. Intenta ser un buen ciudadano de la web, ¿de acuerdo?

Paso 2: analizar la fuente

Ahora que he descargado los archivos, es hora de extraer sus características interesantes. Por lo tanto, voy a una de las páginas que descargué, la abro en un navegador web y presiono Ctrl-U para ver su fuente. Inspeccionarlo me mostrará la estructura HTML.

En mi caso, pensé que quería el texto de la ley sin ningún marcado. El elemento que lo envuelve tiene un id container. Usando BeautifulSoup puedo ver que una combinación de findy get_textharé lo que quiero.

Como tengo un segundo paso ahora, voy a refactorizar un poco el código poniéndolo en funciones y agregando una CLI mínima.

from os import path from pathlib import PurePath import sys from bs4 import BeautifulSoup import requests def download_urls(urls, dir): paths = [] for url in urls: file_name = PurePath(url).name file_path = path.join(dir, file_name) text = '' try: response = requests.get(url) if response.ok: text = response.text else: print('Bad response for', url, response.status_code) except requests.exceptions.ConnectionError as exc: print(exc) with open(file_path, 'w') as fh: fh.write(text) paths.append(file_path) return paths def parse_html(path): with open(path, 'r') as fh: content = fh.read() return BeautifulSoup(content, 'html.parser') def download(urls): return download_urls(urls, '.') def extract(path): return parse_html(path) def transform(soup): container = soup.find(id='container') if container is not None: return container.get_text() def load(key, value): d = {} d[key] = value return d def run_single(path): soup = extract(path) content = transform(soup) unserialised = load(path, content.strip() if content is not None else '') return unserialised def run_everything(): l = [] with open('urls.txt', 'r') as fh: urls = fh.readlines() urls = [url.strip() for url in urls] paths = download(urls) for path in paths: print('Written to', path) l.append(run_single(path)) print(l) if __name__ == "__main__": args = sys.argv if len(args) is 1: run_everything() else: if args[1] == 'download': download([args[2]]) print('Done') if args[1] == 'parse': path = args[2] result = run_single(path) print(result) 

Ahora puedo ejecutar el código de tres formas:

  1. Sin ningún argumento para ejecutar todo (es decir, descargar todas las URL y extraerlas, luego guardarlas en el disco) a través de: python scraper.py
  2. Con un argumento de downloady una URL para descargar python scraper.py download //www.gesetze-im-internet.de/gg/art_1.html. Esto no procesará el archivo.
  3. Con un argumento de parsey una ruta de archivo de análisis sintáctico: python scraper.py art_1.html. Esto omitirá el paso de descarga.

Con eso, falta una última cosa.

Paso 3: formatee la fuente para su posterior procesamiento

Digamos que quiero generar una nube de palabras para cada artículo. Esta puede ser una forma rápida de tener una idea sobre el tema de un texto. Para ello, instale el paquete wordcloudy actualice el archivo así:

from os import path from pathlib import Path, PurePath import sys from bs4 import BeautifulSoup import requests from wordcloud import WordCloud STOPWORDS_ADDENDUM = [ 'Das', 'Der', 'Die', 'Diese', 'Eine', 'In', 'InhaltsverzeichnisGrundgesetz', 'im', 'Jede', 'Jeder', 'Kein', 'Sie', 'Soweit', 'Über' ] STOPWORDS_FILE_PATH = 'stopwords.txt' STOPWORDS_URL = '//raw.githubusercontent.com/stopwords-iso/stopwords-de/master/stopwords-de.txt' def download_urls(urls, dir): paths = [] for url in urls: file_name = PurePath(url).name file_path = path.join(dir, file_name) text = '' try: response = requests.get(url) if response.ok: text = response.text else: print('Bad response for', url, response.status_code) except requests.exceptions.ConnectionError as exc: print(exc) with open(file_path, 'w') as fh: fh.write(text) paths.append(file_path) return paths def parse_html(path): with open(path, 'r') as fh: content = fh.read() return BeautifulSoup(content, 'html.parser') def download_stopwords(): stopwords = '' try: response = requests.get(STOPWORDS_URL) if response.ok: stopwords = response.text else: print('Bad response for', url, response.status_code) except requests.exceptions.ConnectionError as exc: print(exc) with open(STOPWORDS_FILE_PATH, 'w') as fh: fh.write(stopwords) return stopwords def download(urls): return download_urls(urls, '.') def extract(path): return parse_html(path) def transform(soup): container = soup.find(id='container') if container is not None: return container.get_text() def load(filename, text): if Path(STOPWORDS_FILE_PATH).exists(): with open(STOPWORDS_FILE_PATH, 'r') as fh: stopwords = fh.readlines() else: stopwords = download_stopwords() # Strip whitespace around stopwords = [stopword.strip() for stopword in stopwords] # Extend stopwords with own ones, which were determined after first run stopwords = stopwords + STOPWORDS_ADDENDUM try: cloud = WordCloud(stopwords=stopwords).generate(text) cloud.to_file(filename.replace('.html', '.png')) except ValueError: print('Could not generate word cloud for', key) def run_single(path): soup = extract(path) content = transform(soup) load(path, content.strip() if content is not None else '') def run_everything(): with open('urls.txt', 'r') as fh: urls = fh.readlines() urls = [url.strip() for url in urls] paths = download(urls) for path in paths: print('Written to', path) run_single(path) print('Done') if __name__ == "__main__": args = sys.argv if len(args) is 1: run_everything() else: if args[1] == 'download': download([args[2]]) print('Done') if args[1] == 'parse': path = args[2] run_single(path) print('Done')

¿Qué cambió? Por un lado, descargué una lista de palabras irrelevantes alemanas de GitHub. De esta manera, puedo eliminar las palabras más comunes del texto de ley descargado.

Luego, creo una instancia de WordCloud con la lista de palabras vacías que descargué y el texto de la ley. Se convertirá en una imagen con el mismo nombre de base.

Después de la primera ejecución, descubro que la lista de palabras vacías está incompleta. Así que agrego palabras adicionales que quiero excluir de la imagen resultante.

Con eso, la parte principal del web scraping está completa.

Bono: ¿Qué pasa con los SPA?

Los SPA, o aplicaciones de página única, son aplicaciones web donde toda la experiencia está controlada por JavaScript, que se ejecuta en el navegador. Como tal, descargar el archivo HTML no nos lleva muy lejos. ¿Qué deberíamos hacer en su lugar?

We'll use the browser. With Selenium. Make sure to install a driver also. Download the .tar.gz archive and unpack it in the bin folder of your virtual environment so it will be found by Selenium. That is the directory where you can find the activate script (on GNU/Linux systems).

As an example, I am using the Angular website here. Angular is a popular SPA-Framework written in JavaScript and guaranteed to be controlled by it for the time being.

Since the code will be slower, I create a new file called crawler.py for it. The content looks like this:

from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from wordcloud import WordCloud def extract(url): elem = None driver = webdriver.Firefox() driver.get(url) try: found = WebDriverWait(driver, 10).until( EC.visibility_of( driver.find_element(By.TAG_NAME, "article") ) ) # Make a copy of relevant data, because Selenium will throw if # you try to access the properties after the driver quit elem = { "text": found.text } finally: driver.close() return elem def transform(elem): return elem["text"] def load(text, filepath): cloud = WordCloud().generate(text) cloud.to_file(filepath) if __name__ == "__main__": url = "//angular.io/" filepath = "angular.png" elem = extract(url) if elem is not None: text = transform(elem) load(text, filepath) else: print("Sorry, could not extract data")

Here, Python is opening a Firefox instance, browsing the website and looking for an element. It is copying over its text into a dictionary, which gets read out in the transform step and turned into a WordCloud during load.

When dealing with JavaScript-heavy sites, it is often useful to use Waits and perhaps run even execute_scriptto defer to JavaScript if needed.

Summary

Thanks for reading this far! Let's summarise what we've learned now:

  1. How to scrape a website with Python's requests package.
  2. How to translate it into a meaningful structure using beautifulsoup.
  3. How to further process that structure into something you can work with.
  4. What to do if the target page is relying on JavaScript.

Further reading

If you want to find more about me, you can follow me on Twitter or visit my website.

I'm not the first one who wrote about Web Scraping here on freeCodeCamp. Yasoob Khalid and Dave Gray also did so in the past:

Una introducción al web scraping con lxml y Python por Timber.io Una introducción al web scraping con lxml y PythonPhoto por Fabian Grohs [// unsplash.com/photos/dC6Pb2JdAqs?utm_source=unsplash&utm_medium=referral [&utm_content=creditCopyText] en Unsplashplash .com / search / photos / web? utm_source = unsplash & utm_medium = referral & utm_content = creditCopyText… freeCodeCamp.org freeCodeCamp.org Mejor web scraping en Python con Selenium, Beautiful Soup y pandas de Dave Gray Web Scraping Usando el lenguaje de programación Python, es posible "Extraer" datos de la web de una manera rápida y eficiente. El web scraping se define como:> una herramienta para convertir los datos no estructurados en la web en datos estructurados, legibles por máquina y listos para el análisis. (sou… freeCodeCamp.org freeCodeCamp.org