martes, 24 de marzo de 2026

Automatizacion con Python - Cap3 - parte 3

Capítulo 3 // Parte 3 de 3
Python Automation Cookbook

Selenium, Auth y Scraping Concurrente

Control real de navegadores, páginas protegidas y descargas paralelas con futures

En esta tercera y última parte del capítulo dominamos las técnicas más avanzadas: Selenium para controlar un navegador Chrome real, autenticación HTTP básica para páginas protegidas con contraseña, y concurrent.futures para acelerar las descargas ejecutándolas en paralelo.

// Contenido del capítulo completo
  • Descargar páginas web
  • Analizar HTML
  • Rastrear la web (crawling)
  • Suscribirse a feeds RSS
  • Acceder a APIs web
  • Interactuar con formularios
  • Selenium para interacción avanzada
  • Páginas protegidas por contraseña
  • Acelerar el web scraping
RECETA_07

Selenium: Automatización de Navegador

A veces, nada menos que la herramienta real funcionará. Selenium permite controlar un navegador web completo programáticamente. Está concebido para pruebas automáticas pero también es perfecto para automatizar interacciones con sitios que dependen de JavaScript o tienen comportamientos complejos.

Preparación

Instala chromedriver para tu versión de Chrome y el módulo Python:

terminal
# chromedriver en: https://sites.google.com/a/chromium.org/chromedriver/
$ echo "selenium==3.141.0" >> requirements.txt
$ pip install -r requirements.txt

Cómo hacerlo

selenium_formulario.py
from selenium import webdriver

# Paso 1: Iniciar Chrome y cargar la página
browser = webdriver.Chrome()
browser.get('https://httpbin.org/forms/post')
# Se abre una ventana de Chrome con un banner indicando control automático

# Paso 2: Ingresar texto en un campo
custname = browser.find_element_by_name("custname")
custname.clear()
custname.send_keys("Sean O'Connell")

# Paso 3: Seleccionar radio button (tamaño mediano)
for size_el in browser.find_elements_by_name("size"):
    if size_el.get_attribute('value') == 'medium':
        size_el.click()

# Paso 4: Marcar checkboxes múltiples
for topping in browser.find_elements_by_name('topping'):
    if topping.get_attribute('value') in ['bacon', 'cheese']:
        topping.click()

# Paso 5: Enviar el formulario
browser.find_element_by_tag_name('form').submit()
# La página se actualiza mostrando el JSON de respuesta

# Paso 6: Cerrar el navegador
browser.quit()

Selectores disponibles en Selenium

selectores selenium
# Elemento único (lanza error si no existe)
browser.find_element_by_id('mi-id')
browser.find_element_by_name('mi-nombre')
browser.find_element_by_class_name('mi-clase')
browser.find_element_by_tag_name('input')
browser.find_element_by_css_selector('div.mi-clase > input')

# Lista de elementos (devuelve [] si no existen)
browser.find_elements_by_name('topping')

# Interacción con elementos
elemento.send_keys('texto a ingresar')  # Escribir
elemento.click()                          # Clic / toggle checkbox
elemento.submit()                         # Enviar formulario
elemento.clear()                          # Limpiar campo
elemento.get_attribute('value')          # Leer atributo HTML
elemento.text                              # Texto visible del elemento

# Captura de pantalla
browser.save_screenshot('captura.png')
La diferencia clave con requests: Selenium ejecuta JavaScript, maneja cookies de sesión reales, espera a que carguen elementos dinámicos y simula interacciones humanas reales. Úsalo cuando la página requiere JS para funcionar.
RECETA_08

Páginas Protegidas por Contraseña

La autenticación básica HTTP es un método simple donde el nombre de usuario y la contraseña se envían con cada solicitud. Es compatible con prácticamente todos los servidores web y requests lo soporta de forma nativa con el parámetro auth.

Cómo hacerlo

autenticacion_basica.py
import requests

URL = 'https://httpbin.org/basic-auth/user/psswd'

# Método 1: Parámetro auth=(usuario, contraseña)
response = requests.get(URL, auth=('user', 'psswd'))
print(response.status_code)    # 200

# Credenciales incorrectas → 401 Unauthorized
response = requests.get(URL, auth=('user', 'wrong'))
print(response.status_code)    # 401

# Método 2: Credenciales embebidas en la URL
response = requests.get('https://user:psswd@httpbin.org/basic-auth/user/psswd')
print(response.status_code)    # 200

# Método 3: Sesión con autenticación persistente
# (Ideal para múltiples páginas del mismo sitio)
session = requests.Session()
session.auth = ('user', 'psswd')
response = session.get(URL)
print(response.status_code)    # 200
# Todas las siguientes llamadas a session.get() usarán las mismas credenciales
python autenticacion_basica.py
200
401
200
200
Usa siempre HTTPS al enviar credenciales. Con HTTP, la contraseña viaja en texto claro por internet y puede ser capturada por cualquier elemento que esté escuchando el tráfico de red.
El objeto Session de requests mantiene cookies, cabeceras y autenticación entre múltiples solicitudes. Es la forma más eficiente cuando necesitas acceder a varias páginas del mismo sitio autenticado.
RECETA_09

Acelerar el Web Scraping

La mayor parte del tiempo en web scraping se pasa esperando respuestas del servidor. Las computadoras pueden hacer otras cosas mientras esperan, incluyendo hacer más solicitudes simultáneas. Usaremos concurrent.futures para descargar páginas en paralelo.

Concepto: Futures y Executors

Un future es un objeto que representa la promesa de un valor. Recibes el objeto inmediatamente mientras el código se ejecuta en segundo plano. Solo cuando pides .result() el código espera a que esté listo.

Analogía: un future es como un ticket de lavandería. Dejas la ropa, recibes el ticket y puedes hacer otras cosas. Al volver, si la ropa ya está lista, la recoges inmediatamente. Si no, esperas el tiempo restante.

Benchmark: Workers vs Tiempo

Sin concurrencia
12.2
segundos
1 worker
16.4
segundos
2 workers
10.3
segundos
5 workers
6.2
segundos

Cómo hacerlo

speed_up_step1.py (partes clave)
import concurrent.futures, argparse

def process_link(source_link, text):
    # Igual que antes pero ahora devuelve (link, nuevos_links)
    ...
    return source_link, get_links(parsed_source, page)

def main(base_url, to_search, workers):
    checked_links = set()
    to_check = [base_url]
    max_checks = 10

    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
        while to_check:
            # Crear futures para todas las URLs pendientes en paralelo
            futures = [executor.submit(process_link, url, to_search)
                       for url in to_check]
            to_check = []

            # Procesar resultados conforme van terminando
            for data in concurrent.futures.as_completed(futures):
                link, new_links = data.result()
                checked_links.add(link)
                for link in new_links:
                    if link not in checked_links and link not in to_check:
                        to_check.append(link)
                max_checks -= 1
                if not max_checks:
                    return

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-w', type=int, default=4, help='Número de workers')
    args = parser.parse_args()
    main(args.u, args.p, args.w)
time python speed_up_step1.py -w 5
... [salida del crawler] ...
real    0m6.234s
user    0m0.171s
sys     0m0.040s

Alternativas de concurrencia

alternativas.py
# Opción 1: requests-futures (integrado con la API de requests)
# https://github.com/ross/requests-futures
from requests_futures.sessions import FuturesSession
session = FuturesSession()
future = session.get('https://ejemplo.com')   # No bloquea
response = future.result()                     # Espera aquí

# Opción 2: aiohttp + asyncio (programación asíncrona)
# https://aiohttp.readthedocs.io/
import aiohttp, asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'https://ejemplo.com')
        print(html)
¿Cuántos workers usar? Para operaciones I/O (web scraping), puedes usar más workers que núcleos de CPU. Sin embargo, demasiados workers añaden overhead de gestión y pueden hacer el proceso más lento. Experimenta con valores entre 4 y 20 y mide con time.
Las solicitudes concurrentes pueden terminar en diferente orden entre ejecuciones. Si necesitas un orden específico de los resultados, ordénalos después de que todos los futures completen, no asumas que el orden de completación es predecible.

Resumen Completo // Capítulo 3

RECETA_01

Descargar páginas

requests.get() · status_code · response.text · cabeceras HTTP

RECETA_02

Analizar HTML

BeautifulSoup · .find() · .find_all() · next_elements · parsers

RECETA_03

Web Crawling

urlparse · seguir enlaces · filtrar dominio · límites de seguridad

RECETA_04

Feeds RSS

feedparser · rss.entries · título · link · summary · published

RECETA_05

APIs REST

GET/POST/PATCH/PUT/DELETE · .json() · json= argumento · 201 Created

RECETA_06

Formularios

POST data · URL-encoded · inspeccionar campos · tokens CSRF

RECETA_07

Selenium

Chrome real · find_element · send_keys · click · submit · quit

RECETA_08

Autenticación

auth=() · credenciales en URL · requests.Session() · HTTPS

RECETA_09

Concurrencia

ThreadPoolExecutor · futures · as_completed · workers paralelos

Basado en Python Automation Cookbook, 2nd Edition // Jaime Buelta