Hacer scrape es el proceso de extraer datos de sitios web.
Antes de realizar la extracción de datos de una página web, debes asegurarte de que el proveedor lo permita en sus términos de servicio. Además, deberías verificar si no puedes usar una API en su lugar.
Una extracción masiva de datos puede poner al servidor bajo un enorme esfuerzo, lo cual 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. Se asume que ya estás familiarizado con el lenguaje de programación de Python.
Como mínimo, debes entender la comprensión de listas, el administrador de contexto y las funciones. También debes saber cómo configurar un entorno virtual.
Ejecutaremos el código en tu máquina local para explorar algunos sitios web.
Qué aprenderás en este artículo
Al final de este artículo, sabrás cómo descargar una página web, analizarla en busca de información interesante y darle 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 usa JavaScript para representar (render) contenido (como React.js o Angular).
Pre-requisitos
Antes de comenzar, deseo asegurarme de que estemos listo para dar inicio. Por favor, establece un entorno virtual e instálale los siguientes paquetes:
- beautifulsoup4 (versión 4.9.0 al momento de estar escribiendo el artículo)
- requests (versión 2.23.0 al momento de estar escribiendo el artículo)
- wordcloud (versión 1.17.0 al momento de estar escribiendo el artículo, opcional)
- selenium (versión 3.141.0 al momento de estar escribiendo el artículo, opcional)
Puedes encontrar el código de este proyecto en este repositorio git en GitHub
Para este ejemplo, realizaremos una extracción (scrape) de la Ley Básica para la República Federal de Alemania. (No te preocupes, ya verifiqué los Términos de Servicios. Ofrecen una versión XML para procesamiento de máquina, pero esta página sirve como un ejemplo de procesamiento de HTML. Entonces debería estar bien.)
Paso 1: Descargar la fuente
Primero lo primero: creé un archivo urls.txt
que contiene todos los URLs que deseo descargar:
https://www.gesetze-im-internet.de/gg/art_1.html
https://www.gesetze-im-internet.de/gg/art_2.html
https://www.gesetze-im-internet.de/gg/art_3.html
https://www.gesetze-im-internet.de/gg/art_4.html
https://www.gesetze-im-internet.de/gg/art_5.html
https://www.gesetze-im-internet.de/gg/art_6.html
https://www.gesetze-im-internet.de/gg/art_7.html
https://www.gesetze-im-internet.de/gg/art_8.html
https://www.gesetze-im-internet.de/gg/art_9.html
https://www.gesetze-im-internet.de/gg/art_10.html
https://www.gesetze-im-internet.de/gg/art_11.html
https://www.gesetze-im-internet.de/gg/art_12.html
https://www.gesetze-im-internet.de/gg/art_12a.html
https://www.gesetze-im-internet.de/gg/art_13.html
https://www.gesetze-im-internet.de/gg/art_14.html
https://www.gesetze-im-internet.de/gg/art_15.html
https://www.gesetze-im-internet.de/gg/art_16.html
https://www.gesetze-im-internet.de/gg/art_16a.html
https://www.gesetze-im-internet.de/gg/art_17.html
https://www.gesetze-im-internet.de/gg/art_17a.html
https://www.gesetze-im-internet.de/gg/art_18.html
https://www.gesetze-im-internet.de/gg/art_19.html
Luego, escribí un poco de código en Python en un archivo llamado scraper.py
para descargar el HTML de estos archivos.
En un escenario real, esto sería demasiado costoso y, en su lugar, utilizarías una base de datos. Para simplificar las cosas, descargaré archivos en el mismo directorio 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, los puedo procesar localmente tanto como lo desee sin depender de unos servidos. Trata de ser un buen ciudadano web, ¿si?
Paso 2: Analizar la fuente
Ahora que he descargado los archivos, es tiempo de extraer información interesante. Por lo tanto me dirijo a alguna de las páginas que descargué, la abro en un navegador web, y aprieto Crtl-U para ver su código fuente. Al inspeccionarlo me mostrará su estructura HTML.
En mi caso, quería el texto de la ley sin ningún marcado. El elemento que lo envuelve tiene un id de container
. Usando BeautifulSoup puedo ver que una combinación de find
y get_text
hará 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('Mala respuesta para', 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 maneras:
- 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
- Con un argumento de
download
y una url para descargar:python scraper.py download https://www.gesetze-im-internet.de/gg/art_1.html
. Esto no procesará el archivo. - Con un argumento de
parse
y una ruta de archivo para analizar:python scraper.py art_1.html
. Esto omitirá el paso de descarga.
Con esto, solo falta una última cosa.
Paso 3: Dar formato a 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 de lo que trata un texto. Para ello, instala el paquete wordcloud
y actualiza 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 = 'https://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('Mala respuesta para', 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('Mala respuesta para', 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()
# Tira de espacios en blanco alrededor
stopwords = [stopword.strip() for stopword in stopwords]
# Extienda las stopwords con las propias, que se determinaron después de la primera ejecución stopwords = stopwords + STOPWORDS_ADDENDUM
try:
cloud = WordCloud(stopwords=stopwords).generate(text)
cloud.to_file(filename.replace('.html', '.png'))
except ValueError:
print('No se pudo generar la nube de palabras para', 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 vacías (stopwords) alemanas de GitHub. De esta manera, puedo eliminar las palabras más comunes del texto de la ley descargado.
Luego, creé 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, descubrí que la lista de palabras vacías está incompleta. Así que agregué palabras adicionales que quiero excluir de la imagen resultante.
Con esto, la parte principal de la extracción web está completa.
Bonus: ¿Y 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?
Usaremos el navegador. Con Selenium. Asegúrate de instalar también un controlador. Descarga el archivo .tar.gz y descomprímelo en la carpeta bin de tu entorno virtual para que Selenium lo encuentre. Ese es el directorio donde puedes encontrar el script de activación (en sistemas GNU / Linux).
Como ejemplo, estoy usando el sitio web de Angular aquí. Angular es un SPA-Framework popular escrito en JavaScript y se garantiza que será controlado por él por el momento.
Dado que el código será más lento, creé un nuevo archivo llamado crawler.py para él. El contenido tiene este aspecto:
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")
)
)
# Haz una copia de los datos relevantes, porque Selenium arrojará si
# intenta acceder a las propiedades después de que el controlador se cierre
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 = "https://angular.io/"
filepath = "angular.png"
elem = extract(url)
if elem is not None:
text = transform(elem)
load(text, filepath)
else:
print("Lo siento, no se pudieron extraer datos")
Aquí, Python abre una instancia de Firefox, navega por el sitio web y busca un elemento <article>
. Está copiando su texto en un diccionario, que se lee en el paso transform
y se convierte en WordCloud durante load
.
Cuando se trata de sitios con mucho JavaScript, a menudo es útil usar Waits y tal vez incluso ejecutar execute_script
para diferir a JavaScript si es necesario.
Resumen
¡Gracias por leer hasta aquí! Resumamos lo que hemos aprendido ahora:
- Cómo hacer una extracción (scrape) un sitio web con el paquete
requests
de Python. - Cómo traducirla en una estructura con sentido usando
beautifulsoup
. - Cómo procesar aún más esa estructura en algo con lo que puedas trabajar.
- Qué hacer si la página de destino se basa en JavaScript.
Otras lecturas
Si quieres saber más sobre mí, puedes seguirme en Twitter o visitar mi sitio web.
No soy el primero que escribió sobre Web Scraping aquí en freeCodeCamp. Yasoob Khalid y Dave Gray también lo hicieron en el pasado:
![0*AXQfWm6LMJwLwS2f](https://cdn-media-1.freecodecamp.org/images/0*AXQfWm6LMJwLwS2f.png)
![1*DiffPQdgEAjDK4M_unUd4Q](https://cdn-media-1.freecodecamp.org/images/1*DiffPQdgEAjDK4M_unUd4Q.jpeg)
Traducido del artículo de André Jaenisch - How Scrape Websites with Python 3