Saltar al contenido
Inicio 100 Días de Código en Python — Día 53: Proyecto final de scraping — de la web a la hoja de cálculo

100 Días de Código en Python — Día 53: Proyecto final de scraping — de la web a la hoja de cálculo

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 53 (nivel intermedio+).

Este tutorial adapta pedagógicamente el Día 53, el proyecto final (capstone) del bloque de web scraping. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso (lecciones 385 a 387). La solución parcial (Beautiful Soup) y la completa (con Selenium) están en los gists enlazados en la lección 387. Los retos 🟢🟡🟠🔴, el análisis de los datos, el proyecto extra y la etapa del proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14, requests 2.34 y Beautiful Soup 4.15, se raspó el clon de Zillow (appbrewery.github.io/Zillow-Clone/): tiene 44 anuncios, con 44 precios, 44 direcciones y dos enlaces idénticos por anuncio (88 en total, 44 distintos); todos los enlaces son absolutos. Se encontraron formatos de precio que las pistas del curso no mencionan (sección 3). No se envió ningún Formulario de Google.

🏫 Para el aula: el formulario lo crea cada estudiante con la cuenta de Google del colegio. Si tu institución no usa Google, la sección 4 propone un formulario local (el del Día 48) que cumple el mismo papel.


Tabla de contenidos

  1. Antes de empezar: el trabajo de "entrada de datos"
  2. Los requisitos del proyecto
  3. Parte 1: raspar los anuncios con Beautiful Soup
  4. Limpieza de datos: el 80 % del trabajo
  5. Parte 2: llenar el formulario con Selenium
  6. 🚀 Proyecto del día: el asistente de búsqueda de vivienda
  7. 🌟 Proyecto extra: el comparador de útiles escolares
  8. 🧩 Comprueba lo aprendido
  9. 🤖 Programar con apoyo de IA
  10. 🛠️ Correcciones técnicas al material original
  11. 📋 Matriz de trazabilidad
  12. 📦 Evidencias de aprendizaje del Día 53

0. Antes de empezar: el trabajo de "entrada de datos"

(Fuente: 385 - Day 53 Goals- what you will make by the end of the day.md)

📝 Contenido original del curso

Hoy se repasa todo lo del bloque de scraping (Días 45 a 52). La profesora se inspiró en una publicación del subreddit r/Python donde alguien preguntaba si otras personas habían automatizado su trabajo por completo. Muchos respondieron que sí, sobre todo en trabajos de entrada de datos: pasar información de un formato a otro (de un papel, un PDF o un sitio web a una hoja de cálculo).

El encargo: un cliente quiere una lista de apartamentos en alquiler en San Francisco que cuesten hasta 3000 dólares al mes y tengan al menos una habitación. Zillow ya permite filtrar así, pero los sitios reales cambian su HTML, sus clases y sus ventanas emergentes, así que el curso creó un clon estable de Zillow con esa búsqueda ya aplicada: https://appbrewery.github.io/Zillow-Clone/.

El plan:

  1. Con Beautiful Soup, obtener de cada anuncio el precio, la dirección y el enlace.
  2. Con Selenium, llenar un Formulario de Google por cada anuncio.
  3. Desde el formulario, generar una hoja de cálculo de Google con todas las respuestas y entregársela al cliente.

Como es un proyecto final, "no va a haber mucha orientación": hay que perseverar y resolver los problemas por cuenta propia.

Prerequisitos: Beautiful Soup y requests (Días 45 a 47), Selenium con formularios (Días 48 y 49), listas y zip() (Días 4 y 26), métodos de cadenas (Día 2).

📖 Automatizar un trabajo: ¿y la honestidad? (adaptación)

En el video se sugiere que alguien podría automatizar el 70 % de un trabajo remoto y seguir cobrando el sueldo completo sin contarlo. Vale la pena discutirlo en clase:

  • Automatizar tu trabajo es una habilidad valiosa: te hace más productivo y reduce errores.
  • Ocultarlo a tu empleador puede violar tu contrato (por ejemplo, si prohíbe usar herramientas no autorizadas o compartir datos con servicios externos) y te priva de lo mejor: que te reconozcan y te asignen trabajo más interesante.
  • Una buena práctica profesional es proponer la automatización, mostrar cuánto tiempo ahorra y documentarla.

1. Los requisitos del proyecto

(Fuente: 386-web-scraping-and-data-entry-capstone-project-requirements.md)

📝 Ejercicio original del curso

El curso recuerda que, a medida que avanza, los retos se parecen más al trabajo real: "Como desarrollador, pasarás la mayor parte del tiempo averiguando cómo hacer las cosas con Google y Stack Overflow".

Configura tu Formulario de Google

  1. Ve a https://docs.google.com/forms/ y crea un formulario.
  2. Agrega 3 preguntas de respuesta corta (dirección, precio por mes, enlace).
  3. Pulsa Enviar y copia el enlace del formulario: lo usará tu programa.

Explora el clon de Zillow

  1. Abre https://appbrewery.github.io/Zillow-Clone/ y estudia su estructura.

Requisitos de Beautiful Soup

  • Con BeautifulSoup y Requests, raspar todos los anuncios del clon.
  • Crear una lista de enlaces.
  • Crear una lista de precios, limpia: debe quedar "$1,234" y no "$1,234+ /mo".
  • Crear una lista de direcciones, limpia: sin saltos de línea, sin el símbolo | y sin espacios sobrantes.

Requisitos de Selenium

  • Llenar el formulario una vez por cada anuncio con su precio, dirección y enlace.
  • Al terminar, en la pestaña Respuestas, pulsar el ícono de Hojas de cálculo para crear una hoja con todas las respuestas.

🎯 Objetivo: una hoja de cálculo con la dirección, el precio y el enlace de cada propiedad.

🔍 Antes de programar

  • ¿Qué información tenemos? Una página HTML con 44 tarjetas de anuncios.
  • ¿Qué necesitamos obtener? Tres listas del mismo largo, alineadas: el elemento i de cada lista corresponde al mismo anuncio.
  • ¿Qué proceso se hace? Descargar → analizar → extraer → limpiar → enviar uno por uno.
  • ¿Qué resultado esperamos? 44 respuestas en el formulario y una hoja con 44 filas.
  • ¿Qué condiciones hay? El precio y la dirección deben quedar limpios; hay que esperar a que el formulario cargue antes de escribir.

Divide el proyecto en dos programas independientes que se comunican por un archivo: primero raspar.py produce anuncios.csv; luego llenar_formulario.py lo lee. Así puedes probar la parte de Selenium sin volver a descargar, y revisar los datos antes de enviarlos.


2. Parte 1: raspar los anuncios con Beautiful Soup

(Fuente: 386 y 387-hints-y-solution.md)

🎯 Objetivo de aprendizaje

Localizar en el HTML el contenedor de cada anuncio y extraer tres datos alineados.

📝 Pistas originales del curso

  • Pista 1: envía tu User-Agent y los idiomas aceptados en una cabecera, como en el proyecto de Amazon del Día 47.
  • Si te quedas atascado con la limpieza y Beautiful Soup, hay una solución parcial (primera parte del proyecto) y una solución completa (Beautiful Soup + Selenium).

📖 Explicación conceptual

Paso 1 — Inspecciona una tarjeta. En Chrome, clic derecho sobre un precio → Inspeccionar. Sube por el árbol hasta el elemento que contiene todo el anuncio. Anota:

  • dónde está el enlace (<a href="...">),
  • dónde está el precio (un <span>),
  • dónde está la dirección (una etiqueta <address>).

En el clon, esos elementos tienen además un atributo data-test (verificado): property-card-link, property-card-price y property-card-addr. Los atributos data-test los ponen los desarrolladores para las pruebas automáticas, así que suelen ser más estables que las clases.

Paso 2 — Extrae:

Python
import requests
from bs4 import BeautifulSoup

URL_ZILLOW = "https://appbrewery.github.io/Zillow-Clone/"
CABECERAS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "Accept-Language": "es-CO,es;q=0.9",
}

respuesta = requests.get(URL_ZILLOW, headers=CABECERAS, timeout=30)
respuesta.raise_for_status()
sopa = BeautifulSoup(respuesta.text, "html.parser")

enlaces = [a.get("href") for a in sopa.select('[data-test="property-card-link"]')]
precios_crudos = [span.get_text() for span in sopa.select('[data-test="property-card-price"]')]
direcciones_crudas = [d.get_text() for d in sopa.select('[data-test="property-card-addr"]')]

print(len(enlaces), len(precios_crudos), len(direcciones_crudas))   # 88 44 44  ← ¡sorpresa!
  • select() usa selectores CSS (Día 45). [data-test="..."] selecciona por atributo.
  • raise_for_status() detiene el programa con un error claro si la página no respondió 200.
  • La última línea es una prueba, y aquí cumple su función: hay 88 enlaces para 44 anuncios. Al inspeccionar una tarjeta se ve por qué (verificado): cada anuncio tiene dos enlaces con el mismo href, uno en la foto y otro en el texto. Sin esa prueba, zip() emparejaría el enlace repetido del anuncio 1 con el precio del anuncio 2, y todo quedaría desalineado sin ningún mensaje de error.

Dos formas de corregirlo:

Python
# a) Un selector más específico: solo el enlace que está en el área de texto de la tarjeta
enlaces = [a.get("href") for a in sopa.select(".StyledPropertyCardDataWrapper a")]   # 44

# b) Quitar repetidos conservando el orden (dict.fromkeys mantiene el primer orden de aparición)
enlaces = list(dict.fromkeys(a.get("href") for a in sopa.select('[data-test="property-card-link"]')))   # 44

La opción (b) funciona aquí porque no hay dos anuncios con el mismo enlace (verificado), pero depende de esa suposición. La alternativa siguiente evita el problema de raíz.

🛠️ Nota técnica (verificado): el clon está alojado en GitHub Pages y responde igual sin cabeceras. La pista 1 es un buen hábito (Zillow real sí bloquea peticiones sin User-Agent), pero no es la causa si tu código falla aquí.

Alternativa: recorrer tarjeta por tarjeta. En lugar de tres listas separadas, puedes buscar las tarjetas y, dentro de cada una, sus tres datos. Es más seguro: si un anuncio no tuviera precio, sabrías cuál es, en lugar de que todas las listas se "corran" una posición.

Python
anuncios = []
for tarjeta in sopa.select('[data-test="property-card"]'):   # 44 tarjetas (verificado)
    enlace = tarjeta.select_one('[data-test="property-card-link"]')   # select_one toma solo el primero de los dos
    precio = tarjeta.select_one('[data-test="property-card-price"]')
    direccion = tarjeta.select_one('[data-test="property-card-addr"]')
    if enlace and precio and direccion:
        anuncios.append({"enlace": enlace.get("href"), "precio": precio.get_text(), "direccion": direccion.get_text()})
Enfoque Ventaja Riesgo
Tres listas + zip() Corto y es lo que pide el curso Si falta o sobra un dato en una tarjeta (como el enlace doble), todo se desalinea sin avisar
Tarjeta por tarjeta Cada anuncio es un diccionario completo Un poco más largo; debes encontrar el contenedor correcto

⚠️ Errores frecuentes

Error Causa Solución
Listas vacías Selector mal escrito o copiado de Zillow real Verifica el selector en la consola de Chrome con document.querySelectorAll('...')
AttributeError: 'NoneType' object has no attribute 'get_text' select_one no encontró nada Comprueba if elemento: antes de usarlo
Enlaces relativos (/b/...) Algunos sitios no ponen el dominio Completa con urllib.parse.urljoin(URL_ZILLOW, href) (en el clon todos son absolutos)
Listas de distinto largo Se seleccionó un elemento que aparece más de una vez por tarjeta Busca un selector más específico o recorre tarjeta por tarjeta

3. Limpieza de datos: el 80 % del trabajo

(Fuente: 386 y 387-hints-y-solution.md)

🎯 Objetivo de aprendizaje

Diseñar funciones de limpieza que conviertan texto desordenado en datos uniformes, y probarlas con casos reales.

📝 Pistas originales del curso

  • Pista 2: las direcciones pueden ser muy desordenadas. Una forma de limpiarlas es con .replace() y .strip() para quitar saltos de línea, espacios y el símbolo |.
  • Pista 3: los anuncios de una propiedad tienen precios como $1,234/mo o $1,234+/mo; los de varias propiedades incluyen las habitaciones: $1,234+ 1bd. Hay que limpiar ambos.

📖 Lo que realmente hay en el clon (verificado)

Al revisar los 44 precios aparecen más formatos que los de la pista 3:

Formato encontrado Ejemplo real ¿Lo menciona el curso?
$X,XXX/mo $2,450/mo Sí
$X,XXX+/mo $2,895+/mo Sí
$X,XXX+ 1bd $2,798+ 1bd Sí
$X,XXX+ 1 bd (con espacio) $2,809+ 1 bd No (13 anuncios)
Sin coma de miles $1914+ 1 bd No (1 anuncio)

Y en las direcciones:

Texto
'\n                                  Parkmerced | 3711 19th Ave, San Francisco, CA\n\n                                '

Lección: las pistas describen algunos casos; los datos reales siempre traen sorpresas. Por eso se revisan todos los valores, no solo los primeros.

📖 Funciones de limpieza

Python
def limpiar_precio(texto):
    """'$2,895+/mo' -> '$2,895'   |   '$2,809+ 1 bd' -> '$2,809'"""
    return texto.replace("/mo", "").split("+")[0].strip()


def limpiar_direccion(texto):
    """Quita el '|', los saltos de línea y los espacios repetidos."""
    return " ".join(texto.replace("|", "").split())

Cómo funciona limpiar_precio: quita /mo; luego parte el texto en el + y se queda con la primera parte (todo lo que venía después, como 1 bd, se descarta); strip() quita espacios sobrantes.

Cómo funciona limpiar_direccion: split() sin argumentos corta en cualquier cantidad de espacios, tabulaciones o saltos de línea y descarta los vacíos; " ".join(...) lo vuelve a unir con un espacio. Es más robusto que encadenar varios replace("\n", "").

Pruébalas antes de usarlas:

Python
assert limpiar_precio("$2,895+/mo") == "$2,895"
assert limpiar_precio("$2,450/mo") == "$2,450"
assert limpiar_precio("$2,798+ 1bd") == "$2,798"
assert limpiar_precio("$2,809+ 1 bd") == "$2,809"
assert limpiar_direccion("\n   Parkmerced | 3711 19th Ave, San Francisco, CA\n\n ") == "Parkmerced 3711 19th Ave, San Francisco, CA"
print("Todas las pruebas pasaron")

🛠️ Nota técnica: con estas funciones, "$1914+ 1 bd" queda como "$1914", sin coma. El curso pide el formato "$1,234". Para uniformar todos los precios conviene convertirlos a número y volver a darles formato (Reto 2).

📖 Mirar los datos antes de entregarlos (adaptación)

Con los precios como números (verificado): mínimo 1745, máximo 2999, promedio ≈ 2633 dólares. Todos cumplen el tope de 3000. Pero hay una sorpresa: el primer anuncio es "747 Geary Street… Oakland, CA", que no está en San Francisco. Un buen analista de datos lo reporta al cliente en lugar de entregarlo en silencio.

⚠️ Errores frecuentes

Error Causa Solución
Quedan precios como $2,809 1 bd Se quitó el + con replace pero no lo que venía después Usa split("+")[0]
ValueError al convertir "$1,234" a int El texto tiene $ y , int(precio.replace("$", "").replace(",", ""))
Direcciones con dobles espacios Solo se usó strip(), que limpia los extremos " ".join(texto.split())
Probar solo con el primer anuncio Los formatos raros están más abajo Imprime todos los valores distintos o usa assert

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: imprime los formatos distintos de precio que existen (pista: los últimos 4 caracteres de cada precio en un set).
  • 🟡 Reto 2 — Ampliación: escribe precio_a_numero(texto) que devuelva un int y formatear_precio(n) que devuelva "$1,914" con f"${n:,}". Úsalas para que todos los precios queden uniformes.
  • 🟠 Reto 3 — Desafío: agrega una columna habitaciones con el número de habitaciones cuando aparezca (1bd o 1 bd) y "?" cuando no.
  • 🔴 Reto 4 — Pensamiento computacional: el cliente pidió "San Francisco". Diseña una regla para marcar los anuncios dudosos (por ejemplo, los que no contienen "San Francisco"), pruébala y explica en un párrafo por qué marcar es mejor que borrar sin avisar.

4. Parte 2: llenar el formulario con Selenium

(Fuente: 386-web-scraping-and-data-entry-capstone-project-requirements.md)

🎯 Objetivo de aprendizaje

Repetir un proceso de entrada de datos en un formulario web, una vez por registro, con esperas y verificación.

📖 Explicación conceptual

El ciclo: para cada anuncio → abrir el formulario → esperar a que carguen los campos → escribir los tres datos → pulsar Enviar → esperar la confirmación.

Python
import time
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

URL_FORMULARIO = "https://docs.google.com/forms/d/e/TU_FORMULARIO/viewform"

driver = webdriver.Chrome()
espera = WebDriverWait(driver, 10)

for direccion, precio, enlace in zip(direcciones, precios, enlaces):
    driver.get(URL_FORMULARIO)
    campos = espera.until(
        EC.presence_of_all_elements_located((By.XPATH, '//input[@type="text"]'))
    )
    campos[0].send_keys(direccion)
    campos[1].send_keys(precio)
    campos[2].send_keys(enlace)

    driver.find_element(By.XPATH, '//div[@role="button"]//span[text()="Enviar"]').click()
    espera.until(EC.url_contains("formResponse"))   # la página de "Se registró tu respuesta"
    time.sleep(1)

driver.quit()

Qué hace cada parte:

  • zip() recorre las tres listas en paralelo: en cada vuelta toma el elemento i de cada una.
  • En los Formularios de Google los campos de respuesta corta son <input type="text">, en el mismo orden que las preguntas.
  • El botón Enviar no es un <button>, sino un div con role="button". Su texto depende del idioma de la cuenta ("Enviar" o "Submit").
  • url_contains("formResponse") confirma que el envío llegó antes de pasar al siguiente anuncio.

🛠️ Nota técnica: Google cambia el HTML de sus formularios de vez en cuando; por eso es mejor seleccionar por tipo (@type="text") y por rol y texto (@role="button") que por clases como whsOnd zHQkBf. Si tu cuenta está en inglés, cambia "Enviar" por "Submit".

🛠️ Nota técnica: si el formulario está configurado para "Restringir a usuarios de tu organización" o para recopilar correos verificados, exige iniciar sesión, y el Chrome que abre Selenium no tiene tu sesión. Para este proyecto, desactiva esas opciones en Configuración → Respuestas.

🏫 Alternativa para el aula: formulario local

Si no hay cuentas de Google, usa el formulario inscripcion.html del Día 48 (o crea uno con tres campos <input> con id propios). Guarda las respuestas con un pequeño script, o simplemente verifica con Selenium que se llenaron. El Día 54 aprenderás a crear con Flask un servidor que reciba formularios de verdad.

⚠️ Errores frecuentes

Error Causa Solución
IndexError: list index out of range en campos[2] El formulario aún no había cargado todos los campos, o tiene otro tipo de pregunta Espera presence_of_all_elements_located y usa preguntas de respuesta corta
El texto se escribe en el campo equivocado El orden de las preguntas no coincide Revisa el orden en el formulario
NoSuchElementException con el botón Enviar Idioma distinto ("Submit") Ajusta el texto o busca //div[@role="button"][.//span] y elige el correcto
Llegan menos respuestas que anuncios Se abrió el siguiente formulario antes de terminar de enviar Espera la confirmación (formResponse)
La hoja tiene respuestas duplicadas Ejecutaste el programa dos veces Borra las respuestas antes de la ejecución final

5. 🚀 Proyecto del día: el asistente de búsqueda de vivienda

(Proyecto original del curso: lecciones 385 a 387.)

Problema

Un cliente necesita una lista de apartamentos que cumplan sus condiciones, con dirección, precio y enlace, en una hoja de cálculo que pueda filtrar.

Contexto

Es un trabajo real de entrada de datos: tomar información de un sitio web y pasarla a otro formato. Hacerlo a mano con 44 anuncios toma más de una hora y se cometen errores de copia.

Objetivo

Un programa que raspe el clon de Zillow, limpie los datos y llene un formulario por anuncio, del que se genera la hoja de cálculo final.

Requisitos funcionales

  1. Listas de enlaces, precios y direcciones del mismo largo (44).
  2. Precios con el formato "$1,234"; direcciones sin |, saltos de línea ni espacios sobrantes.
  3. Un envío del formulario por anuncio, con verificación de que llegó.
  4. Una hoja de cálculo con todas las respuestas.

Restricciones técnicas

  • Beautiful Soup para raspar; Selenium solo para el formulario (como pide el curso).
  • Funciones de limpieza probadas con assert.
  • Esperas con condiciones; nada de enviar a ciegas.

Planificación

  1. Comprender el encargo del cliente. 2. Entradas: la URL del clon y la del formulario. 3. Procesos: raspar → limpiar → guardar en CSV → enviar. 4. Salidas: anuncios.csv y la hoja de Google. 5. Dividir en dos programas. 6. Diseñar las funciones de limpieza con los casos de la tabla de la sección 3. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: raspar.py imprime el número de enlaces, precios y direcciones.
  • Versión 2: limpieza con funciones y pruebas assert.
  • Versión 3: guardar anuncios.csv con el módulo csv o con pandas (Día 25).
  • Versión 4: llenar_formulario.py envía solo los 3 primeros anuncios para probar.
  • Versión final: envío de todos, verificación del número de respuestas y creación de la hoja.

Pruebas

Caso Resultado esperado
Largo de las tres listas 44, 44 y 44
limpiar_precio("$2,809+ 1 bd") "$2,809"
limpiar_direccion con | y saltos de línea Una sola línea, un espacio entre palabras
Precio sin coma ($1914+ 1 bd) Con el Reto 2: "$1,914"
Envío de 3 anuncios de prueba 3 respuestas nuevas en el formulario
Envío final 44 respuestas y 44 filas en la hoja

🧪 Prueba y depuración

Python
precios = [p.get_text().replace("+/mo", "") for p in sopa.select('[data-test="property-card-price"]')]
direcciones = [d.get_text().strip() for d in sopa.select("address")]
for i in range(len(precios)):
    driver.get(URL_FORMULARIO)
    campos = driver.find_elements(By.XPATH, '//input[@type="text"]')
    campos[0].send_keys(direcciones[i])
    campos[1].send_keys(precios[i])
    campos[2].send_keys(enlaces[i])
    driver.find_element(By.XPATH, '//span[text()="Enviar"]').click()

Encuentra cuatro problemas: la limpieza del precio no cubre /mo sin + ni los formatos con bd; strip() no quita el | ni los espacios internos; se buscan los campos sin esperar a que carguen; no se verifica que el envío terminó antes de abrir el siguiente formulario.

⭐ Extensión del proyecto

  • Ordenar los anuncios por precio antes de enviarlos.
  • Enviar al cliente un correo (servidor local, Día 47) con el enlace a la hoja y un resumen: cuántos anuncios, precio mínimo, máximo y promedio.
  • Reemplazar Selenium por pandas para escribir directamente un anuncios.xlsx y comparar: ¿qué ventaja tiene cada camino?

6. 🌟 Proyecto extra: el comparador de útiles escolares

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

Al comenzar el año, las familias de un curso quieren saber dónde comprar más barata la lista de útiles. La personera estudiantil propone publicar una comparación.

Requisitos

  1. Crea tú mismo (en HTML, Días 41 a 44) dos o tres páginas de "papelerías" ficticias, cada una con una tabla o tarjetas de productos con formatos de precio distintos (por ejemplo, $ 3.500, 3500 COP, $3,500 c/u).
  2. Un programa que raspe las páginas locales, limpie los precios a enteros y los una en un solo CSV con columnas producto, papeleria, precio.
  3. Para cada producto de una lista de útiles (lista_utiles.txt), la papelería más barata y el costo total de la lista en cada papelería.
  4. Un formulario (de Google o local) donde el programa registre el resultado final, una fila por papelería.

Decisiones que debes tomar (y justificar)

  • En Colombia, 3.500 usa el punto como separador de miles. ¿Cómo lo distingues de un decimal?
  • ¿Qué haces si un producto no está en una papelería?
  • ¿Cómo haces coincidir "Cuaderno 100 hojas" con "cuaderno cuadriculado 100h"?

Casos de prueba

  • limpiar_cop("$ 3.500"), limpiar_cop("3500 COP") y limpiar_cop("$3,500 c/u") devuelven 3500.
  • Un producto que falta en una papelería no rompe el total: se reporta como "no disponible".

7. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué el curso usa un clon de Zillow en lugar del sitio real?
  2. ¿Qué ventaja tienen los atributos data-test como selectores?
  3. ¿Por qué es peligroso trabajar con tres listas separadas si un anuncio no tiene precio?
  4. ¿Por qué conviene separar el raspado y el llenado del formulario en dos programas?
  5. ¿Qué hace zip() cuando las listas tienen distinto largo?

Análisis de código

Python
texto = "\n   Parkmerced | 3711 19th Ave, San Francisco, CA\n\n  "
print(repr(texto.strip()))
print(repr(texto.replace("|", "").strip()))
print(repr(" ".join(texto.replace("|", "").split())))

¿Qué imprime cada línea? ¿Cuál cumple el requisito del curso?

Predicción

Python
for p in ["$2,450/mo", "$2,895+/mo", "$2,798+ 1bd", "$1914+ 1 bd"]:
    print(p.replace("/mo", "").split("+")[0].strip())

Depuración

Python
precios_numeros = [int(p) for p in ["$2,450", "$2,895"]]

¿Qué error aparece y cómo lo corriges?

Modificación

Cambia limpiar_precio para que devuelva None si el texto no empieza por $, y ajusta el programa para que esos anuncios se reporten y no se envíen.

Creación

Escribe una función resumen(precios) que reciba la lista de precios como enteros y devuelva un diccionario con el mínimo, el máximo, el promedio (redondeado) y la cantidad de anuncios por debajo de 2500.


8. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — Limpieza con casos reales. Pide a una IA una función para limpiar precios como "$1,234+/mo". Pruébala con los cinco formatos de la tabla de la sección 3. ¿Cuántos pasan? Corrige la función y explica qué caso no había previsto la IA.

Actividad 2 — Selectores del formulario. Pide a una IA el código para llenar un Formulario de Google con Selenium. Revisa: ¿usa clases como whsOnd?, ¿espera a que cargue?, ¿verifica el envío? Compara con la sección 4 y con la documentación de Selenium sobre esperas explícitas.

Actividad 3 — Verificar una afirmación. Pregunta a una IA si str.split() sin argumentos y str.split(" ") hacen lo mismo. Compruébalo en Python con "a b\n c" y contrasta con la documentación oficial de str.split.


9. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
387 Pista 3: los precios con varias propiedades son como $1,234+ 1bd En el clon también hay $2,809+ 1 bd (con espacio, 13 casos) y $1914+ 1 bd (sin coma) (verificado el 27/09/2026) Sección 3: tabla de formatos y Reto 2
386 "Crea una lista de enlaces de todos los anuncios" Cada tarjeta del clon tiene dos enlaces con el mismo href; un selector general devuelve 88 y desalinea las listas (verificado) Sección 2: prueba de largos y dos correcciones
387 Pista 1: enviar User-Agent y Accept-Language Buen hábito, pero el clon responde igual sin cabeceras (verificado) Nota técnica de la sección 2
385 La búsqueda es de apartamentos en San Francisco Un anuncio del clon está en Oakland (verificado) Sección 3: "Mirar los datos antes de entregarlos" y Reto 4
386 Llenar el formulario con Selenium Si el formulario exige iniciar sesión (restringido a la organización o con correos verificados), el Chrome de Selenium no puede enviarlo; el botón cambia con el idioma Notas técnicas de la sección 4
385 Automatizar el 70 % del trabajo y seguir cobrando sin contarlo Puede violar el contrato; es mejor proponer y documentar la automatización Sección 0

10. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Proyecto final del bloque de scraping, trabajos de entrada de datos, r/Python, encargo del cliente (San Francisco, ≤ 3000 USD, ≥ 1 habitación), clon de Zillow, plan Beautiful Soup + Selenium + hoja de Google 385 Sección 0 — Discusión sobre automatizar con honestidad Sección 5
Crear el Formulario de Google (3 preguntas cortas), estudiar el clon, requisitos de Beautiful Soup (enlaces, precios y direcciones limpios), requisitos de Selenium, crear la hoja de cálculo 386 Secciones 1, 2 y 4 Requisitos del proyecto Dos programas y CSV intermedio; formulario local Sección 5
Pista 1 (cabeceras), pista 2 (replace y strip en direcciones), pista 3 (formatos de precio), solución parcial y completa 387 Secciones 2 y 3 Pistas del proyecto Retos 1–4 de la sección 3 Sección 5
(No existe en el curso original) — Sección 3 — Análisis de los datos (mínimo, máximo, promedio, anuncio fuera de la ciudad) Sección 5
(No existe en el curso original) — — — Comparador de útiles escolares Sección 6

11. 📦 Evidencias de aprendizaje del Día 53

  • raspar.py y anuncios.csv con los 44 anuncios limpios.
  • Las funciones de limpieza con sus pruebas assert pasando.
  • llenar_formulario.py y la captura de la hoja de cálculo con 44 filas (o la evidencia del formulario local).
  • Un informe corto para el cliente: cuántos anuncios, rango de precios y los anuncios marcados como dudosos.
  • El comparador de útiles escolares (proyecto extra).
  • Cuaderno de errores: selectores copiados de otro sitio, listas desalineadas, formatos de precio no previstos, int("$2,450"), campos que no habían cargado, botón con texto en otro idioma, formulario que exige iniciar sesión.