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-startse descarga desde Udemy y no está en la carpeta; por eso esta guía usa un archivowebsite.htmlde práctica creado para la adaptación, con la misma estructura que describe la profesora (unh1conid="name", títulosh3conclass="heading", una lista y un enlace dentro dep > 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.txty la copia archivada de la lista de Empire que usa el curso.
Tabla de contenidos
- Antes de empezar: cuando no hay API
- Analizar HTML y "hacer sopa"
- Buscar y seleccionar elementos con Beautiful Soup
- Hacer scraping de un sitio web en vivo
- ¿Es legal el web scraping? Ética y normas
- 🚀 Proyecto del día: las 100 películas que debes ver
- 🌟 Proyecto extra: el catálogo de la librería de práctica
- 🏆 Proyecto integrador del bloque de scraping y automatización (Días 45 a 51) — Etapa 1
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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:
- Convertir un texto HTML en un objeto de Python que se puede recorrer.
- Buscar elementos por etiqueta,
id,classy selectores CSS (los del Día 43). - Descargar una página en vivo con
requestsy extraer datos de ella. - 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
beautifulsoup4y se importa comobs4(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):
<!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>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 leerResultado 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-starthay unmain.pyvacío y unwebsite.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 usandowithyopen, antes de ver la solución. - Luego: importar
BeautifulSoupdesdebs4, crear la sopa con"html.parser"y probarsoup.title,soup.title.name,soup.title.string,soup.prettify(),soup.a,soup.liysoup.p.
🔍 Antes de programar
- ¿Qué necesita recibir
BeautifulSoup: el nombre del archivo o su contenido? - ¿Qué diferencia esperas entre
soup.titleysoup.title.string? - ¿Qué pasará con
soup.psi 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
titley su primerh2. - 🟡 Reto 2 — Ampliación: imprime el nombre de la primera etiqueta que está dentro del
body(pista: explorasoup.bodyy 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 (h1ah6) 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
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 elementosResultado 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:
- Obtener todas las etiquetas
ay luego todos lospconfind_all. - Imprimir el texto de cada enlace (con un ciclo
forygetText). - Imprimir el
hrefde cada enlace (conget). - Obtener el
h1que tieneid="name". - Obtener el
h3conclass="heading", su texto, su nombre y el valor de su atributoclass. - Obtener la URL de la empresa con
select_one("p a"). - Usar selectores de id (
#name) y de clase (.heading) conselect_oneyselect.
- Obtener todas las etiquetas
- 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
findy cuándofind_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 losa. - 🟡 Reto 2: obtén solo los
lide la lista ordenada (pista:select("ol li")). Luego, solo loslique contienen un enlace. - 🟠 Reto 3 — Depuración: este programa falla. Encuentra los tres errores:
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
h2y, debajo de cada uno, sush3) con la sangría correcta. ¿Cómo sabes a quéh2pertenece cadah3si 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.
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 NewsEl método del curso para encontrar los datos:
- Clic derecho sobre un título → Inspeccionar.
- Identificar la etiqueta y la clase (en el video: un
acon clasestorylink; los puntos, unspancon clasescore). - Buscar con
findel primero; luego confind_alltodos. - Extraer el texto, el enlace y los puntos en tres listas paralelas (el elemento 0 de cada lista corresponde al mismo artículo).
- Convertir los puntos (
"40 points") en números:int(score.getText().split()[0]). - 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 essoup.select("span.titleline > a")(30 resultados). Los puntos siguen enspan.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
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:
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:
- Imprimir el título del primer artículo.
- Obtener su enlace y sus puntos.
- Cambiar a
find_ally construir las listasarticle_texts,article_linksyarticle_upvotes(con puntos como enteros). - 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:
sortedconkey, ozipde 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?
4. ¿Es legal el web scraping? Ética y normas
(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:
- Los datos públicos y sin derechos de autor probablemente se pueden recolectar.
- El uso privado es poco problemático; el problema aparece al comercializar los datos de otros.
- El contenido con derechos de autor (videos de YouTube, artículos de Medium, música de Spotify) no se puede comercializar.
- Los datos detrás de un inicio de sesión tienen otra situación: al registrarte aceptaste unos términos que suelen prohibirlo.
- 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).
- 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)
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:
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.txtde 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 tieneCrawl-delay? - 🟡 Reto 2: escribe una función
puedo_raspar(url)que useRobotFileParsery devuelvaTrueoFalse, y úsala antes de cadarequests.getde 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)
- Tomar la URL de los recursos del curso (puede cambiar con el tiempo; no la copies del video).
- Descargarla con
requestsy analizarla con Beautiful Soup. - Encontrar con DevTools dónde están los títulos (en el video:
h3con clasetitle). - Extraer el texto de todos los títulos.
- Invertir la lista (en la página va de 100 a 1).
- 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
- Comprender: ¿qué hay en la página y qué necesito?
- Entradas: la URL.
- Proceso: descargar → analizar → buscar → extraer texto → invertir → escribir.
- Salida:
movies.txtcon 100 líneas. - Dividir el problema en esas funciones.
- 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 imprimelen(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:
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"→1y"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
- Extraer de cada libro: título, precio y calificación (de una a cinco estrellas).
- Recorrer varias páginas siguiendo el enlace "next" (no escribas las 50 URL a mano).
- Esperar al menos 1 segundo entre páginas.
- 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
- Escribe
lector.py, que abra con Beautiful Soup elindex.htmlde tu proyecto del bloque web (archivo local: sin peticiones a Internet). - 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 sualt. - Genera un reporte
reporte_sitio.txtcon esa información y con una alerta por cada imagen sinalto conaltvacío (así el programa revisa la accesibilidad de tu propia página). - Entregable:
lector.py, el reporte y una captura de la ejecución.
8. 🧩 Comprueba lo aprendido
Conceptuales
- ¿En qué casos se usa web scraping en lugar de una API?
- ¿Qué hace un analizador de HTML? Nombra dos analizadores que acepta Beautiful Soup.
- ¿Qué diferencia hay entre
findyfind_all? ¿Y entreselect_oneyselect? - ¿Por qué se escribe
class_y noclass? - ¿Qué indican
DisallowyCrawl-delayen unrobots.txt?
Análisis de código
Explica qué hace cada línea y qué tipo de dato produce:
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?
print(len(soup.select(".heading")))
print(soup.select_one("ol li").getText())
print(soup.find("h2"))Depuración
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.pycon 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.txty la funciónpuedo_raspar(url). movies.txtgenerado 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.pyyreporte_sitio.txt. - Cuaderno de errores:
classen lugar declass_,getTextsobre una lista,Nonepor un selector desactualizado, archivo leído sinencoding, listas paralelas desalineadas, peticiones sin pausa.