Saltar al contenido
Inicio 100 Días de Código en Python — Día 45: Web scraping con Beautiful Soup

100 Días de Código en Python — Día 45: Web scraping con Beautiful Soup

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 45 (nivel intermedio+).

Este tutorial adapta pedagógicamente el Día 45. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso (transcripciones de las lecciones 331 a 336). El proyecto inicial bs4-start se descarga desde Udemy y no está en la carpeta; por eso esta guía usa un archivo website.html de práctica creado para la adaptación, con la misma estructura que describe la profesora (un h1 con id="name", títulos h3 con class="heading", una lista y un enlace dentro de p > em > strong). Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 26 de septiembre de 2026 se ejecutaron los ejemplos con Python 3.14, requests 2.34 y beautifulsoup4 4.15, contra el archivo de práctica, Hacker News, su robots.txt y la copia archivada de la lista de Empire que usa el curso.


Tabla de contenidos

  1. Antes de empezar: cuando no hay API
  2. Analizar HTML y "hacer sopa"
  3. Buscar y seleccionar elementos con Beautiful Soup
  4. Hacer scraping de un sitio web en vivo
  5. ¿Es legal el web scraping? Ética y normas
  6. 🚀 Proyecto del día: las 100 películas que debes ver
  7. 🌟 Proyecto extra: el catálogo de la librería de práctica
  8. 🏆 Proyecto integrador del bloque de scraping y automatización (Días 45 a 51) — Etapa 1
  9. 🧩 Comprueba lo aprendido
  10. 🤖 Programar con apoyo de IA
  11. 🛠️ Correcciones técnicas al material original
  12. 📋 Matriz de trazabilidad
  13. 📦 Evidencias de aprendizaje del Día 45

0. Antes de empezar: cuando no hay API

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

Desde el Día 33 obtenemos datos con API: el sitio web nos entrega la información ordenada (en JSON) porque así lo decidió. Pero muchos sitios no tienen API, o su API no ofrece lo que necesitamos. Entonces queda otra opción: leer el código HTML de la página y extraer lo que nos interesa. Eso se llama web scraping (raspado web).

La profesora compara el HTML de una página real con una "sopa de espagueti": si abres Ver código fuente en la página de Google, verás miles de líneas enredadas. Para encontrar lo que buscas en esa sopa se usa un analizador de HTML (parser). El de hoy se llama, justamente, Beautiful Soup.

Lo nuevo hoy:

  1. Convertir un texto HTML en un objeto de Python que se puede recorrer.
  2. Buscar elementos por etiqueta, id, class y selectores CSS (los del Día 43).
  3. Descargar una página en vivo con requests y extraer datos de ella.
  4. Pensar en lo legal y lo ético antes de extraer datos ajenos.

Conexión con los Días 41 a 44: todo lo que aprendiste de HTML y CSS se vuelve ahora una herramienta de programación. Si no recuerdas qué es un id, una class o un selector como p a, repasa esos días.

Prerequisitos: abrir y escribir archivos (Día 24), comprensión de listas (Día 26), requests (Día 33), selectores CSS (Día 43).


1. Analizar HTML y "hacer sopa"

(Fuente: 332 - Parsing HTML and Making Soup.md)

🎯 Objetivo de aprendizaje

Crear un objeto BeautifulSoup a partir de un archivo HTML y acceder a sus etiquetas.

🧠 ¿Qué aprenderemos?

bs4, la clase BeautifulSoup, el analizador (html.parser o lxml), .title, .name, .string, .prettify() y el acceso a la primera etiqueta de un tipo.

📖 Explicación conceptual

  • Beautiful Soup es una librería para extraer datos de archivos HTML y XML. Se instala como beautifulsoup4 y se importa como bs4 (versión 4). Su documentación está traducida a varios idiomas, entre ellos el español.
  • Analizar (parse) es convertir un texto en una estructura que el programa entiende. Beautiful Soup necesita saber qué tipo de documento recibe. Por eso se le indica un analizador:
    • "html.parser": viene con Python; no requiere instalar nada.
    • "lxml": más rápido y, según la profesora, funciona en algunos sitios donde el otro da problemas. Se instala aparte (pip install lxml).
  • La sopa (soup) es un objeto que representa todo el documento: puedes navegarlo con código Python como si fuera un árbol.

🔎 Ejemplo explicado

Archivo de práctica website.html (creado para esta guía):

HTML
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8" /><title>Hoja de vida de Ana</title></head>
<body>
  <h1 id="name">Ana Pérez</h1>
  <p><em>Estudiante de grado décimo. Practicante en <strong><a href="https://www.colegio.edu.co/">Colegio Ejemplo</a></strong>.</em></p>
  <h3 class="heading">Libros favoritos</h3>
  <ol>
    <li>Cien años de soledad</li>
    <li>María</li>
  </ol>
  <h3 class="heading">Habilidades</h3>
  <ul>
    <li><a href="https://www.python.org/">Python</a></li>
    <li><a href="https://developer.mozilla.org/es/">HTML y CSS</a></li>
  </ul>
</body>
</html>
Python
from bs4 import BeautifulSoup

with open("website.html", encoding="utf-8") as file:
    contents = file.read()                        # el HTML como texto

soup = BeautifulSoup(contents, "html.parser")     # "hacer sopa"

print(soup.title)          # <title>Hoja de vida de Ana</title>  (la etiqueta completa)
print(soup.title.name)     # title                                (el nombre de la etiqueta)
print(soup.title.string)   # Hoja de vida de Ana                  (el texto de adentro)
print(soup.a)              # el PRIMER <a> del documento
print(soup.li.getText())   # Cien años de soledad                 (el texto del primer <li>)
print(soup.prettify())     # todo el HTML con sangría, fácil de leer

Resultado verificado: las líneas comentadas coinciden con la ejecución real.

¿Por qué with open(...)? Es la forma que vimos en el Día 24: abre el archivo y lo cierra automáticamente al terminar el bloque.

⚠️ Errores frecuentes

Error Qué ocurre Cómo identificarlo Solución
ModuleNotFoundError: No module named 'bs4' La librería no está instalada en el entorno del proyecto El mensaje de error al ejecutar Instala beautifulsoup4 (el nombre del paquete no es bs4) en el mismo entorno que usa PyCharm
open("website.html") sin encoding En Windows, Python 3.14 lee con la codificación del sistema (cp1252): "Pérez" aparece como "Pérez" (verificado) Tildes y eñes rotas en la salida open(..., encoding="utf-8")
FileNotFoundError El archivo no está en la carpeta desde donde se ejecuta el programa El mensaje de error Guarda main.py y website.html juntos, o usa la ruta correcta
Esperar todas las etiquetas con soup.a Solo devuelve la primera Imprime un solo enlace Usa find_all (sección 2)

📝 Ejercicio original del curso: leer el archivo y hacer sopa

  • Enunciado: en el proyecto bs4-start hay un main.py vacío y un website.html (una versión simplificada de la página de hoja de vida del Día 41). Reto de la profesora: leer el contenido del archivo como texto usando with y open, antes de ver la solución.
  • Luego: importar BeautifulSoup desde bs4, crear la sopa con "html.parser" y probar soup.title, soup.title.name, soup.title.string, soup.prettify(), soup.a, soup.li y soup.p.

🔍 Antes de programar

  • ¿Qué necesita recibir BeautifulSoup: el nombre del archivo o su contenido?
  • ¿Qué diferencia esperas entre soup.title y soup.title.string?
  • ¿Qué pasará con soup.p si hay varios párrafos?

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: analiza con Beautiful Soup la página de tu proyecto integrador del bloque web (Días 41 a 44). Imprime su title y su primer h2.
  • 🟡 Reto 2 — Ampliación: imprime el nombre de la primera etiqueta que está dentro del body (pista: explora soup.body y la documentación sobre "navegar el árbol").
  • 🟠 Reto 3 — Desafío: compara soup.prettify() con el archivo original. ¿Beautiful Soup "arregla" un HTML con errores (por ejemplo, un </li> que falta)? Prueba con una copia rota del archivo y explica lo que observas, relacionándolo con lo que vimos del navegador en el Día 41.
  • 🔴 Reto 4 — Pensamiento computacional: escribe una función resumen_html(ruta) que devuelva un diccionario con el título de la página, cuántos enlaces, cuántas imágenes y cuántos encabezados de cada nivel (h1 a h6) tiene. Pruébala con dos páginas distintas.

2. Buscar y seleccionar elementos con Beautiful Soup

(Fuente: 333 - Finding and Selecting Particular Elements with BeautifulSoup.md)

🎯 Objetivo de aprendizaje

Usar find, find_all, select_one y select para ubicar elementos por etiqueta, atributo, clase, id o selector CSS, y extraer su texto y sus atributos.

📖 Explicación conceptual

Método Devuelve Ejemplo
find_all(...) Lista con todos los que coinciden soup.find_all(name="a")
find(...) El primero que coincide (o None) soup.find(name="h1", id="name")
select(selector) Lista, usando un selector CSS soup.select(".heading")
select_one(selector) El primero, usando un selector CSS soup.select_one("p a")

Sobre cada elemento encontrado:

Expresión Da
etiqueta.getText() (o get_text()) El texto de adentro
etiqueta.get("href") El valor de un atributo
etiqueta.name El nombre de la etiqueta

¿Por qué class_ con guion bajo? class es una palabra reservada de Python (sirve para crear clases, Día 16). Para buscar por el atributo HTML class, Beautiful Soup usa el parámetro class_.

¿Cuándo usar selectores CSS? Cuando el elemento no tiene nada único, pero su ubicación sí lo es. En el ejemplo, el enlace de la empresa es el único a que está dentro de un p. Con find_all("a") tendrías que adivinar su posición; con select_one("p a") lo encuentras directamente. En sitios reales, muchos elementos están dentro de un div con id: primero apuntas al div y luego al elemento.

🔎 Ejemplo explicado

Python
all_anchor_tags = soup.find_all(name="a")
for tag in all_anchor_tags:
    print(tag.getText())       # Colegio Ejemplo / Python / HTML y CSS
    print(tag.get("href"))     # los tres enlaces

heading = soup.find(name="h1", id="name")
print(heading.getText())       # Ana Pérez

section_heading = soup.find(name="h3", class_="heading")
print(section_heading.getText())    # Libros favoritos (el primero)
print(section_heading.get("class")) # ['heading']  -> ¡una lista! un elemento puede tener varias clases

company_url = soup.select_one(selector="p a")
print(company_url.get("href"))      # https://www.colegio.edu.co/

name = soup.select_one("#name")          # id
headings = soup.select(".heading")       # clase -> lista de 2 elementos

Resultado verificado con el archivo de práctica.

⚠️ Errores frecuentes

Error Qué ocurre Solución
soup.find(name="h3", class="heading") SyntaxError: invalid syntax antes de ejecutar nada (verificado). No se puede atrapar con try porque el programa ni siquiera arranca class_="heading"
Llamar .getText() sobre el resultado de find_all AttributeError: una lista no tiene getText Recorre la lista con for o con una comprensión de listas
AttributeError: 'NoneType' object has no attribute 'getText' find o select_one no encontró nada y devolvió None Revisa el selector en DevTools; comprueba if elemento is not None: antes de usarlo
select("heading") Busca etiquetas <heading>, que no existen Las clases llevan punto: ".heading"; los id, almohadilla: "#name"
Confundir get("href") con getText() Obtienes el texto visible en vez del enlace, o al revés getText() = lo que se ve; get("atributo") = lo que está dentro de la etiqueta

📝 Ejercicio original del curso: explorar el archivo con find, find_all y select

  • Tareas mostradas en la lección:
    1. Obtener todas las etiquetas a y luego todos los p con find_all.
    2. Imprimir el texto de cada enlace (con un ciclo for y getText).
    3. Imprimir el href de cada enlace (con get).
    4. Obtener el h1 que tiene id="name".
    5. Obtener el h3 con class="heading", su texto, su nombre y el valor de su atributo class.
    6. Obtener la URL de la empresa con select_one("p a").
    7. Usar selectores de id (#name) y de clase (.heading) con select_one y select.
  • La profesora anuncia un cuestionario de práctica sobre búsqueda y selección (no está en la carpeta).

🔍 Antes de programar

  • ¿Cuántos elementos devolverá cada búsqueda? Escríbelo antes de ejecutar y luego compara.
  • ¿Qué tiene de único el enlace de la empresa respecto a los otros enlaces?
  • ¿Cuándo conviene find y cuándo find_all?

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: con una sola comprensión de listas, obtén una lista de tuplas (texto, enlace) de todos los a.
  • 🟡 Reto 2: obtén solo los li de la lista ordenada (pista: select("ol li")). Luego, solo los li que contienen un enlace.
  • 🟠 Reto 3 — Depuración: este programa falla. Encuentra los tres errores:
Python
  titulos = soup.find_all(name="h3", class_="heading").getText()
  enlace = soup.select_one("p > a")
  print(enlace.get(href))

(Pista para el segundo: en el archivo, ¿el a es hijo directo del p? Consulta qué significa > en un selector CSS.)

  • 🔴 Reto 4 — Pensamiento computacional: dada la página de tu proyecto integrador web, escribe un programa que produzca el índice de la página (todos los h2 y, debajo de cada uno, sus h3) con la sangría correcta. ¿Cómo sabes a qué h2 pertenece cada h3 si no están anidados en el HTML? (Pista: recorre los encabezados en orden.)

3. Hacer scraping de un sitio web en vivo

(Fuente: 334 - Scraping a Live Website.md)

🎯 Objetivo de aprendizaje

Descargar el HTML de una página real con requests, extraer datos repetidos y relacionarlos entre sí mediante índices.

📖 Explicación conceptual

El sitio del ejemplo es Hacker News (news.ycombinator.com), de Y Combinator, donde la gente publica noticias de tecnología que otros votan.

Python
import requests
from bs4 import BeautifulSoup

response = requests.get("https://news.ycombinator.com/")
yc_web_page = response.text                     # el mismo HTML que ves en "Ver código fuente"
soup = BeautifulSoup(yc_web_page, "html.parser")
print(soup.title.string)                        # Hacker News

El método del curso para encontrar los datos:

  1. Clic derecho sobre un título → Inspeccionar.
  2. Identificar la etiqueta y la clase (en el video: un a con clase storylink; los puntos, un span con clase score).
  3. Buscar con find el primero; luego con find_all todos.
  4. Extraer el texto, el enlace y los puntos en tres listas paralelas (el elemento 0 de cada lista corresponde al mismo artículo).
  5. Convertir los puntos ("40 points") en números: int(score.getText().split()[0]).
  6. Encontrar el mayor con max() y su posición con .index(), y usar esa posición en las otras listas.

🛠️ Nota técnica (verificado el 26/09/2026): Hacker News ya no usa la clase storylink: find_all("a", class_="storylink") devuelve una lista vacía. Hoy el título está en <span class="titleline"><a href="...">Título</a></span>, así que el selector que funciona es soup.select("span.titleline > a") (30 resultados). Los puntos siguen en span.score (30 resultados). Esta es la lección más importante del día: los sitios web cambian su HTML sin avisar, y un programa de scraping debe revisarse con DevTools cada vez que deja de funcionar.

🔎 Ejemplo explicado: de texto a número

Python
article_upvotes = [int(score.getText().split()[0]) for score in soup.find_all(name="span", class_="score")]

Se lee de adentro hacia afuera:

  • score.getText() → "40 points"
  • .split() → ["40", "points"] (divide por espacios)
  • [0] → "40"
  • int(...) → 40

Luego:

Python
largest_number = max(article_upvotes)
largest_index = article_upvotes.index(largest_number)
# articles_texts[largest_index] y article_links[largest_index] son el título y el enlace ganadores

🛠️ Nota técnica (análisis de esta adaptación): las listas paralelas solo funcionan si cada artículo tiene título y puntos. En Hacker News, las publicaciones de ofertas de empleo aparecen sin puntos: si una de ellas está en la portada, la lista de puntos tendrá menos elementos que la de títulos y los índices se desalinearán, así que el programa mostrará el título equivocado sin dar ningún error. Una solución más robusta es recorrer cada fila de noticia y buscar su título y sus puntos dentro de ella, o guardar cada artículo como un diccionario {"titulo": ..., "enlace": ..., "puntos": ...}.

⚠️ Errores frecuentes

Error Qué ocurre Solución
Copiar el selector de un video antiguo Lista vacía o None Inspecciona el sitio hoy
int("40 points") ValueError Separa primero el número con split()
Llamar .getText() sobre la lista de find_all AttributeError Comprensión de listas
Ejecutar el programa en un ciclo sin pausa Sobrecargas el servidor y pueden bloquearte Respeta el Crawl-delay (sección 4)
Listas de distinto tamaño Resultados equivocados sin mensaje de error Verifica len() de cada lista, o agrupa los datos por artículo

📝 Ejercicio original del curso: la noticia más votada de Hacker News

  • Retos de la lección, en orden:
    1. Imprimir el título del primer artículo.
    2. Obtener su enlace y sus puntos.
    3. Cambiar a find_all y construir las listas article_texts, article_links y article_upvotes (con puntos como enteros).
    4. Imprimir el título y el enlace de la noticia con más puntos de la portada, encontrando el índice del número mayor.
  • Idea de uso de la profesora: hacerlo todos los días y enviarse por correo o SMS la noticia más votada (Días 32 y 36).

🔍 Antes de programar

  • ¿Qué datos hay que obtener de cada noticia?
  • ¿Cómo se relacionan tres listas separadas? ¿Qué supuesto estás haciendo?
  • ¿Qué harías si dos noticias tuvieran el mismo número de puntos?

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: adapta el programa a la estructura actual de Hacker News e imprime las 5 noticias con más puntos, ordenadas (pista: sorted con key, o zip de las listas).
  • 🟡 Reto 2: reescribe el programa para que cada noticia sea un diccionario, buscando el título y los puntos dentro de cada fila. Si una noticia no tiene puntos, asígnale 0.
  • 🟠 Reto 3: guarda el resultado en un archivo CSV con fecha (datetime, Día 32) y ábrelo con pandas (Día 25) para calcular el promedio de puntos.
  • 🔴 Reto 4 — Pensamiento computacional: diseña un programa que se ejecute una vez al día y te avise solo cuando aparezca una noticia sobre un tema que te interese (por ejemplo, "Python" o "robot"). ¿Qué datos debe guardar entre ejecuciones para no avisarte dos veces lo mismo?

(Fuente: 335 - Is Web Scraping Legal-.md)

🎯 Objetivo de aprendizaje

Evaluar si un proyecto de scraping es legal, ético y respetuoso con el sitio, y leer un archivo robots.txt.

📖 Lo que plantea el curso

  • Los buscadores (Google, Bing) hacen scraping de toda la web constantemente para indexarla.
  • Casos famosos: Genius demandó a Google por mostrar letras de canciones tomadas de su sitio (y perdió). LinkedIn demandó a hiQ, una empresa que extraía datos públicos de perfiles.
  • Conclusión del curso:
    1. Los datos públicos y sin derechos de autor probablemente se pueden recolectar.
    2. El uso privado es poco problemático; el problema aparece al comercializar los datos de otros.
    3. El contenido con derechos de autor (videos de YouTube, artículos de Medium, música de Spotify) no se puede comercializar.
    4. Los datos detrás de un inicio de sesión tienen otra situación: al registrarte aceptaste unos términos que suelen prohibirlo.
    5. Que algo sea legal no significa que sea posible: muchos sitios usan CAPTCHA o reCAPTCHA para detectar bots (por ejemplo, analizando cómo se mueve el mouse antes de marcar la casilla).
    6. Una demanda cuesta mucho dinero aunque la ganes.
  • Ética, según la profesora: "si no quiero que me pase algo, trato de no hacérselo a los demás".
    • Si hay una API pública, úsala (y solicita el acceso si hace falta).
    • Respeta al dueño del sitio: no hagas miles de peticiones por segundo; eso puede tumbar el sitio y parecer un ataque DDoS.
    • Lee el robots.txt (https://sitio.com/robots.txt): indica qué rutas no quiere que visiten los bots (Disallow) y cuántos segundos esperar entre peticiones (Crawl-delay).
    • La recomendación de la profesora: no más de una petición por minuto si el sitio no dice nada.

🔎 Ejemplo explicado: el robots.txt de Hacker News (verificado el 26/09/2026)

Texto
User-Agent: *
Crawl-delay: 30
Disallow: /collapse?
Disallow: /login
Disallow: /reply?
Disallow: /vote?
...
  • User-Agent: * → las reglas aplican a todos los bots.
  • Crawl-delay: 30 → espera 30 segundos entre peticiones.
  • Disallow: /vote? → no automatices votos; /reply? → no automatices respuestas. Tiene sentido: el sitio quiere que personas, no robots, voten y comenten.

Python trae un lector de robots.txt:

Python
from urllib.robotparser import RobotFileParser

rp = RobotFileParser("https://news.ycombinator.com/robots.txt")
rp.read()
print(rp.can_fetch("*", "https://news.ycombinator.com/"))        # True: puedo leer la portada
print(rp.can_fetch("*", "https://news.ycombinator.com/vote?id=1"))  # False: no debo automatizar votos
print(rp.crawl_delay("*"))                                        # 30 segundos de espera

(Resultados verificados el 26/09/2026.)

📖 Adaptación para Colombia (no es contenido del curso; no es asesoría legal)

Norma Qué tiene que ver con el scraping
Ley 1581 de 2012 (protección de datos personales) Nombres, correos, fotos o teléfonos de personas son datos personales. Recolectarlos y usarlos requiere autorización del titular, aunque estén "a la vista" en Internet
Ley 23 de 1982 (derechos de autor) Textos, fotos, música y videos tienen autor. Copiarlos y publicarlos como propios, o comercializarlos, puede violar sus derechos
Ley 1273 de 2009 (delitos informáticos) Entrar sin autorización a un sistema protegido (por ejemplo, saltándose un inicio de sesión) puede configurar acceso abusivo a un sistema informático

Regla práctica para el aula: en este curso solo haremos scraping de (1) nuestros propios archivos, (2) sitios creados para practicar (los clones de App Brewery, books.toscrape.com, quotes.toscrape.com) y (3) páginas públicas, sin datos personales, respetando su robots.txt, con pocas peticiones y solo para aprender.

⚠️ Errores frecuentes de criterio

Error Por qué es un problema
"Si está en Internet, es gratis y puedo usarlo" Confunde acceso con permiso. Los derechos de autor y los datos personales siguen protegidos
Ejecutar el scraper en un while True sin pausas Carga innecesaria para el servidor; puede bloquear tu IP o la del colegio
Recolectar nombres y fotos de estudiantes de redes sociales "para un proyecto" Datos personales, muchos de menores de edad, sin autorización
Ignorar que existe una API La API es el camino que el dueño del sitio ofrece; el scraping debería ser la última opción

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: lee el robots.txt de tres sitios (por ejemplo, Wikipedia, un periódico colombiano y el sitio de tu colegio, si lo tiene). ¿Cuál es el más permisivo? ¿Alguno tiene Crawl-delay?
  • 🟡 Reto 2: escribe una función puedo_raspar(url) que use RobotFileParser y devuelva True o False, y úsala antes de cada requests.get de tus programas.
  • 🟠 Reto 3 — Debate: para cada caso, decide si es aceptable y justifica con los criterios de esta sección: (a) extraer el menú del restaurante escolar publicado en la página del colegio para mostrarlo en tu app; (b) extraer todos los correos de los docentes que aparecen en el sitio del colegio para enviarles publicidad; (c) copiar las reseñas de una tienda en línea para publicarlas en tu blog; (d) descargar datos del portal de datos abiertos del Estado (datos.gov.co).
  • 🔴 Reto 4 — Investigación: investiga cómo terminó el caso hiQ contra LinkedIn (hubo varias decisiones). Compara con la conclusión del video y escribe un párrafo sobre por qué "ganar" un juicio no es tan simple (ver la corrección técnica de la sección 10).

5. 🚀 Proyecto del día: las 100 películas que debes ver

(Fuente: 336 - 100 Movies that You Must Watch.md)

Problema

Es de noche, todos están frente al televisor y nadie sabe qué película ver. La revista Empire publicó una lista de las 100 mejores películas de todos los tiempos, numerada de la 100 a la 1. La profesora quiere verlas todas e ir tachándolas.

Contexto

El mismo problema aparece con listas de libros del plan lector, álbumes musicales o videojuegos. El scraping permite convertir una página en una lista de trabajo.

Objetivo

Crear un programa que extraiga los 100 títulos y los guarde en un archivo movies.txt, uno por línea, del 1 al 100.

Requisitos funcionales (originales del curso)

  1. Tomar la URL de los recursos del curso (puede cambiar con el tiempo; no la copies del video).
  2. Descargarla con requests y analizarla con Beautiful Soup.
  3. Encontrar con DevTools dónde están los títulos (en el video: h3 con clase title).
  4. Extraer el texto de todos los títulos.
  5. Invertir la lista (en la página va de 100 a 1).
  6. Escribir cada título en una línea de movies.txt.

Dato verificado el 26/09/2026: el curso usa una copia archivada de la página de Empire en Wayback Machine (web.archive.org). Con esa copia, find_all(name="h3", class_="title") devuelve 100 títulos, del "100) Stand By Me" al "1) The Godfather". Usar una copia archivada es una buena decisión: la página original de Empire cambió de diseño varias veces.

Dos formas de invertir la lista (las dos del curso):

Forma Código Comentario
Rebanada (slicing) movies = movie_titles[::-1] [inicio:fin:paso] con paso -1. Es la que elige la profesora
range hacia atrás for n in range(len(movie_titles) - 1, -1, -1): El fin de range no se incluye: para llegar al índice 0 hay que poner -1 como fin

Curiosidad del curso: la película 93 aparece con un error de escritura en la página original. "Como estamos recolectando datos, no podemos ser quisquillosos": el programa copia lo que hay. (Eso sí, podrías corregirlo tú en un paso de limpieza de datos.)

Restricciones técnicas

requests, bs4, comprensión de listas, rebanadas y escritura de archivos. Sin copiar a mano.

Planificación

  1. Comprender: ¿qué hay en la página y qué necesito?
  2. Entradas: la URL.
  3. Proceso: descargar → analizar → buscar → extraer texto → invertir → escribir.
  4. Salida: movies.txt con 100 líneas.
  5. Dividir el problema en esas funciones.
  6. Diseñar → 7. Implementar → 8. Probar → 9. Corregir → 10. Mejorar.

Construcción paso a paso

  • Versión 1 — Descargar: imprime soup.prettify() y busca un título en la salida.
  • Versión 2 — Extraer: all_movies = soup.find_all(...) e imprime len(all_movies). ¿Da 100?
  • Versión 3 — Texto: una comprensión de listas con getText().
  • Versión 4 — Ordenar: invierte la lista y comprueba que el primer elemento empieza por "1)".
  • Versión final — Guardar: with open("movies.txt", mode="w", encoding="utf-8") as file: y escribe cada título seguido de "\n".

Pruebas

Prueba Resultado esperado
len(movie_titles) 100
Primera línea de movies.txt 1) The Godfather
Última línea 100) Stand By Me
Abrir el archivo en el Bloc de notas Una película por línea, sin líneas vacías intermedias
Ejecutar dos veces El archivo se sobrescribe (modo "w"), no se duplica

🧪 Prueba y depuración

Este programa tiene cuatro errores. Explica el efecto de cada uno:

Python
import requests
from bs4 import BeautifulSoup

URL = "https://web.archive.org/web/20200518073855/https://www.empireonline.com/movies/features/best-movies-2/"
response = requests.get(URL)
soup = BeautifulSoup(response, "html.parser")
all_movies = soup.find_all(name="h3", class_="title")
movie_titles = [movie.getText for movie in all_movies]
movies = movie_titles[::1]
with open("movies.txt", mode="a") as file:
    for movie in movies:
        file.write(movie)

⭐ Extensión del proyecto

  • Guarda también si la película ya fue vista ([ ] o [x] al inicio de cada línea) y crea un programa que elija una al azar entre las no vistas (random, Día 4). Esa era la idea original de la profesora.
  • Separa el número del título ("1) The Godfather" → 1 y "The Godfather") y guarda un CSV.
  • Genera una página HTML (Días 41 a 44) con la lista ordenada (<ol>) de películas.

6. 🌟 Proyecto extra: el catálogo de la librería de práctica

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

Situación

El club de lectura quiere analizar precios y calificaciones de libros, pero sin sobrecargar la tienda de nadie. Existe books.toscrape.com, una librería ficticia creada para practicar scraping: tiene 1 000 libros en 50 páginas.

Requisitos

  1. Extraer de cada libro: título, precio y calificación (de una a cinco estrellas).
  2. Recorrer varias páginas siguiendo el enlace "next" (no escribas las 50 URL a mano).
  3. Esperar al menos 1 segundo entre páginas.
  4. Guardar un CSV y calcular: el libro más caro, el más barato, el precio promedio y cuántos libros hay de cada calificación.

Pistas verificadas (26/09/2026): cada libro está en un article.product_pod; el título completo está en el atributo title del enlace del h3; el precio, en p.price_color; la calificación es una clase del p.star-rating (por ejemplo ["star-rating", "Three"]); el enlace a la página siguiente está en li.next a. Usa response.content en lugar de response.text para que el símbolo de la libra (£) se lea bien.

Decisiones que debes tomar (y justificar)

  • ¿Cómo conviertes "Three" en el número 3?
  • ¿Cómo sabes que llegaste a la última página?
  • ¿Recorres las 50 páginas o solo algunas? ¿Por qué?

Casos de prueba

  • La primera página tiene 20 libros.
  • El precio se guarda como número (float), no como texto con £.
  • Si interrumpes el programa a mitad, ¿qué datos se pierden? Propón una mejora.

7. 🏆 Proyecto integrador del bloque de scraping y automatización (Días 45 a 51) — Etapa 1

(Proyecto creado para esta adaptación. La descripción completa, los criterios y la versión final están en el tutorial del Día 51.)

Situación problema

En el bloque web (Días 41 a 44) construiste el sitio de un grupo del colegio. Ahora vas a construir un asistente automático en Python que lea ese sitio (y fuentes públicas autorizadas), genere reportes, vigile cambios y automatice tareas repetitivas sin afectar a nadie.

Pregunta orientadora

¿Cómo puede un programa ahorrar tiempo a un grupo escolar recolectando y organizando información de la web de forma legal, ética y confiable?

Etapa 1 (hoy): leer tu propio sitio

  1. Escribe lector.py, que abra con Beautiful Soup el index.html de tu proyecto del bloque web (archivo local: sin peticiones a Internet).
  2. Extrae: el título de la página, todos los encabezados en orden, todos los enlaces (texto y href) y todas las imágenes con su alt.
  3. Genera un reporte reporte_sitio.txt con esa información y con una alerta por cada imagen sin alt o con alt vacío (así el programa revisa la accesibilidad de tu propia página).
  4. Entregable: lector.py, el reporte y una captura de la ejecución.

8. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿En qué casos se usa web scraping en lugar de una API?
  2. ¿Qué hace un analizador de HTML? Nombra dos analizadores que acepta Beautiful Soup.
  3. ¿Qué diferencia hay entre find y find_all? ¿Y entre select_one y select?
  4. ¿Por qué se escribe class_ y no class?
  5. ¿Qué indican Disallow y Crawl-delay en un robots.txt?

Análisis de código

Explica qué hace cada línea y qué tipo de dato produce:

Python
links = [a.get("href") for a in soup.select("ul li a")]
first = soup.find("h3", class_="heading")
total = len(soup.find_all("li"))

Predicción

Con el archivo website.html de esta guía, ¿qué imprime cada línea?

Python
print(len(soup.select(".heading")))
print(soup.select_one("ol li").getText())
print(soup.find("h2"))

Depuración

Python
soup = BeautifulSoup("website.html", "html.parser")
print(soup.title.string)

El programa muestra una advertencia MarkupResemblesLocatorWarning y luego termina con AttributeError: 'NoneType' object has no attribute 'string' (verificado). ¿Por qué? Lee con atención el texto de la advertencia: ¿qué recibió BeautifulSoup? ¿Por qué soup.title vale None?

Modificación

Modifica el programa de Hacker News para que muestre la noticia más votada y la menos votada de la portada.

Creación

Escribe un programa que analice quotes.toscrape.com (sitio de práctica) y muestre las citas de un autor que escriba el usuario con input(), recorriendo solo las primeras 3 páginas.


9. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende el código → pruébalo → compáralo con la documentación oficial (Beautiful Soup, requests) → modifícalo → explícalo con tus palabras.

Actividad 1 — Selectores desactualizados. Pídele a una IA "un programa en Python con Beautiful Soup que imprima los títulos de Hacker News". Muchas IA sugieren la clase storylink, que ya no existe. Ejecuta el código: si la lista sale vacía, usa DevTools para encontrar el selector actual y explica por qué la IA se equivocó (fue entrenada con páginas antiguas).

Actividad 2 — Comprobar una afirmación legal. Pregúntale a una IA "¿Es legal el web scraping en Colombia?". Evalúa la respuesta: ¿menciona la Ley 1581 de 2012?, ¿distingue entre datos públicos y datos personales?, ¿aclara que no es asesoría legal? Compara con fuentes oficiales (por ejemplo, la Superintendencia de Industria y Comercio).

Actividad 3 — Revisar la robustez. Muestra a una IA el programa del curso con las tres listas paralelas y pregúntale: "¿Qué puede salir mal?". ¿Detecta el problema de las publicaciones sin puntos? Comprueba su explicación con la nota técnica de la sección 3.


10. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
334 Los títulos de Hacker News son a con clase storylink Ya no existe: devuelve 0 resultados. Hoy: span.titleline > a (30 resultados, verificado el 26/09/2026) Nota técnica de la sección 3; Actividad 1 de IA
334 Tres listas paralelas (títulos, enlaces, puntos) Las publicaciones de empleo no tienen puntos: los índices pueden desalinearse sin error visible Nota técnica de la sección 3; Reto 2
332 open("website.html") sin codificación En Windows con Python 3.14 las tildes se leen mal (verificado); conviene encoding="utf-8" Tabla de errores de la sección 1
335 hiQ ganó y LinkedIn "terminó perdiendo" Los tribunales fallaron a favor de hiQ en el tema del acceso a datos públicos (2019 y 2022), pero a finales de 2022 un tribunal concluyó que hiQ había incumplido el acuerdo de usuario de LinkedIn, y el caso terminó con un acuerdo en que hiQ pagó a LinkedIn y se comprometió a dejar de extraer y a destruir los datos Reto 4 de la sección 4
335 "Raspar datos detrás de un inicio de sesión es bastante ilegal" Es una simplificación: depende de los términos aceptados y de la ley de cada país. En Colombia, entrar sin autorización puede configurar un delito (Ley 1273 de 2009) Tabla de la sección 4
335 El curso analiza la ley de Estados Unidos En Colombia aplican, entre otras, las leyes 1581 de 2012, 23 de 1982 y 1273 de 2009 Adaptación de la sección 4
334 El reto de "noticia más votada" hace una petición cada vez que se ejecuta El robots.txt de Hacker News pide Crawl-delay: 30 (verificado) Sección 4

11. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Scraping cuando no hay API, "sopa de espagueti", objetivo de las 100 películas 331 Sección 0 — — Sección 5
bs4-start, leer archivo con with open, BeautifulSoup(contents, "html.parser"), lxml, title, name, string, prettify, primera etiqueta 332 Sección 1 Leer el archivo y hacer sopa Retos 1–4 de la sección 1 Secciones 5, 6 y 7
find_all, find, getText, get("href"), id, class_, select_one, select, selectores CSS 333 Sección 2 Explorar el archivo (7 tareas) Retos 1–4 de la sección 2 Secciones 5, 6 y 7
requests.get, Hacker News, storylink, score, listas paralelas, split, int, max, index 334 Sección 3 Noticia más votada Retos 1–4 de la sección 3 Secciones 6 y 7
Buscadores, Genius contra Google, hiQ contra LinkedIn, derechos de autor, datos tras inicio de sesión, CAPTCHA, ética, API, DDoS, robots.txt, Crawl-delay 335 Sección 4 Revisar robots.txt Retos 1–4 de la sección 4 Secciones 5, 6 y 7
100 películas de Empire: URL de los recursos, h3.title, comprensión de listas, [::-1], range hacia atrás, error de la película 93, movies.txt 336 Sección 5 Proyecto del día 🧪 y ⭐ de la sección 5 Sección 5
(No existe en el curso original) — — — Catálogo de la librería de práctica Sección 6
(No existe en el curso original) — — — Asistente automático, etapa 1 Sección 7

12. 📦 Evidencias de aprendizaje del Día 45

  • main.py con los ejemplos de las secciones 1 y 2, comentados con tus palabras.
  • Tu predicción y el resultado real de cada búsqueda del ejercicio de la sección 2.
  • El programa de Hacker News adaptado al HTML actual, con la explicación de cómo encontraste el nuevo selector en DevTools.
  • Tu análisis de un robots.txt y la función puedo_raspar(url).
  • movies.txt generado por tu programa y la tabla de pruebas diligenciada.
  • El catálogo de la librería (proyecto extra) con sus estadísticas.
  • Etapa 1 del proyecto integrador: lector.py y reporte_sitio.txt.
  • Cuaderno de errores: class en lugar de class_, getText sobre una lista, None por un selector desactualizado, archivo leído sin encoding, listas paralelas desalineadas, peticiones sin pausa.