Ir al contenido principal

Scraping de Reddit con Python y BeautifulSoup 4

En este tutorial, aprenderás a obtener páginas web con requests, analizar páginas en el navegador y extraer información del HTML con BeautifulSoup.
Actualizado 17 sept 2026  · 13 min leer

Explorar con IA

ChatGPTClaudePerplexity

Scraping reddit illustration

Puedes encontrar un ejemplo final y funcional del script que vamos a escribir aquí.

¿Qué es el web scraping?

Bien, ¿en qué consiste exactamente el web scraping? Como su nombre sugiere, es un método para "raspar" o extraer datos de páginas web. Casi todo lo que ves en internet con tu navegador —incluido este tutorial— puede descargarse a tu disco duro.

El web scraping tiene muchísimos usos. En cualquier análisis de datos, el primer paso es la adquisición de datos. Internet es un enorme repositorio de la historia y el conocimiento de la humanidad, y tienes a tu alcance extraer lo que necesites y trabajar con esa información.

En este tutorial usaremos Python y el paquete BeautifulSoup 4 para obtener información de un subreddit. Nos interesa el subreddit datascience. Queremos obtener los primeros 1000 posts del subreddit y exportarlos a un archivo CSV. Nos interesa saber quién lo publicó, cuántos votos positivos tiene y cuántos comentarios.

Qué vamos a cubrir en el tutorial:

  • Obtener páginas web con requests
  • Analizar páginas web en el navegador para identificar información
  • Extraer información del HTML con BeautifulSoup

Nota: usaremos la versión antigua de la web de Reddit porque carga más rápido y, por tanto, es menos exigente para tu equipo.

Requisitos previos

Este tutorial asume que sabes lo siguiente:

  • Ejecutar scripts de Python en tu ordenador
  • Conocer la estructura básica de HTML

Puedes aprender estas habilidades en el curso para principiantes de Python de DataCamp. Aun así, los conceptos que usamos aquí son mínimos, y podrás seguirlo aunque tengas muy poca experiencia con Python.

Con esto listo, pasamos a la primera parte de nuestro scraper. De hecho, el primer paso al escribir cualquier script en Python: los imports.

En nuestro scraper usaremos los siguientes paquetes:

  • requests
  • beautifulsoup4

Puedes instalarlos con pip, así de simple:

pip install package_name

Cuando termines de descargarlos, impórtalos en tu código.

import requests
import csv
import time
from bs4 import BeautifulSoup4

Usaremos el módulo integrado csv de Python para escribir los resultados en un archivo CSV. Quizá te haya llamado la atención algo en el fragmento anterior: hemos descargado un paquete llamado beautifulsoup4, pero importamos desde un módulo llamado bs4. Esto es válido en Python; aunque suele desaconsejarse por estilo, no es "ilegal".

Nota importante sobre las políticas de la plataforma

Antes de usar las técnicas de scraping que verás a continuación, asegúrate de revisar los términos de uso vigentes de Reddit o de cualquier otro sitio que planees scrapear. Las políticas pueden cambiar con el tiempo. El acuerdo de usuario de Reddit de septiembre de 2024 prohíbe el scraping sin consentimiento previo por escrito (ver sección 7). Verifica siempre que cumples con las directrices de uso más recientes del sitio antes de continuar.

Primeros pasos para rastrear Reddit

Ya tenemos el entorno listo. Ahora necesitamos la URL de la página que queremos scrapear. Para este tutorial, usaremos el subreddit "datascience" de Reddit. Antes de escribir el script, toca un poco de trabajo de campo: abre un navegador y ve al subreddit en cuestión.

Usaremos la versión antigua del subreddit. La nueva oculta parte de la información clave en las tripas de la página. También es posible extraerla de la nueva versión, pero para simplificar, usaremos la versión antigua, que lo muestra todo de forma más directa.

Al abrir el enlace, te encontrarás con un montón de información. ¿Qué necesitamos exactamente? Si exploras los enlaces, verás que los posts de Reddit son de dos tipos:

  • Inbound
  • Outbound

Los enlaces inbound apuntan a contenido dentro del propio Reddit; los outbound, justo a lo contrario. Esto es importante porque solo queremos los inbound: publicaciones con texto escrito por usuarios, no solo enlaces a otras webs.

Bien, ya sabemos qué queremos. ¿Cómo lo identificamos? Si miras el título de los posts, verás que va seguido de un texto entre paréntesis. Los que nos interesan vienen seguidos de "(self.datascience)". Lógicamente, podemos asumir que "self" se refiere a la raíz de Reddit y ".datascience" al subreddit.

Genial, ya tenemos una forma de distinguir posts inbound y outbound. Ahora toca identificarlo en el DOM. ¿Podemos localizar estos identificadores buscando etiquetas, clases o ids en la estructura del DOM? ¡Claro que sí!

En tu navegador, haz clic derecho sobre cualquier enlace con "self.datascience" y selecciona "inspeccionar elemento". La mayoría de navegadores modernos incluyen esta fantástica herramienta que permite recorrer dinámicamente las partes relevantes del código fuente de la página. Si usas Safari, activa las herramientas para desarrolladores en sus preferencias. Verás un panel desplazado a la parte del código responsable de ese identificador "(self.datascience)".

En el panel, verás que el identificador está dentro de una etiqueta de enlace (anchor).

<a href="/r/datascience">self.datascience</a>

Pero todo ello va envuelto en un span.

<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>

Este span contiene el texto que vemos en la página, es decir, "(self.datascience)". Como puedes ver, tiene la clase "domain". Si revisas otros enlaces, verás que siguen el mismo formato.

Obtener la página con BeautifulSoup

Ya sabemos qué queremos de la página, pero ¿cómo hacemos que Python lea su contenido? Funciona de forma muy parecida a como lo haría una persona en el navegador.

Primero, debemos solicitar la página web con la librería requests.

url = "https://old.reddit.com/r/datascience"
# Cabeceras para imitar una visita del navegador
headers = {'User-Agent': 'Mozilla/5.0'}

# Devuelve un objeto requests.models.Response
page = requests.get(url, headers=headers)

Ahora tenemos un objeto Response que contiene el texto bruto de la página. De momento no podemos hacer gran cosa con él: básicamente es una cadena enorme con todo el HTML. Para interpretarlo, necesitamos BeautifulSoup 4.

Las cabeceras nos permiten imitar una visita de navegador. Como la respuesta a un bot puede diferir de la que recibe un navegador, y nuestro punto de referencia es el navegador, nos interesa obtener esa respuesta.

BeautifulSoup nos permite buscar etiquetas concretas combinando clases, ids o nombres de etiqueta. Lo hace creando un árbol de sintaxis, pero esos detalles no nos hacen falta ahora.

Vamos a crear ese árbol.

soup = BeautifulSoup(page.text, 'html.parser')

"soup" es un objeto de BeautifulSoup creado a partir del código fuente en bruto. Ten en cuenta que debemos especificar el parser de HTML, ya que BeautifulSoup también puede parsear XML.

Encontrar nuestras etiquetas

Ya sabemos qué etiquetas queremos (spans con la clase "domain") y ya tenemos el soup. Lo siguiente es recorrer el soup y encontrar todas las instancias. Te sorprenderá lo sencillo que es con BeautifulSoup.

domains = soup.find_all("span", class_="domain")

¿Qué acabamos de hacer? Llamamos al método find_all sobre el objeto soup, que busca dentro de sí todas las etiquetas con los parámetros proporcionados. Solo pasamos una restricción (la clase debe ser "domain"), pero podríamos combinarla con otras o usar un id.

Usamos "class_=" porque "class" es una palabra reservada en Python para definir clases, etc.

Si quisieras pasar más de un parámetro, basta con convertir el segundo argumento en un diccionario con los atributos deseados, así:

soup.find_all("span", {"class": "domain", "height", "100px"})

Ya tenemos todos los span en nuestra lista "domains", pero lo que queremos son los que muestran "(self.datascience)".

for domain in domains:
    if domain != "(self.datascience)":
        continue

    print(domain.text)

Ahora deberías ver un listado de los tipos de posts de la página, excluyendo los que no son "(self.datascience)". Con esto tenemos referencias a todos los posts inbound.

Encontrar nuestra información

Está bien imprimir unas cuantas líneas de "(self.datascience)", pero ¿y ahora qué? Recuerda nuestro objetivo: obtener el título del post, el autor, los likes y el número de comentarios.

Para ello volvemos al navegador. Si vuelves a inspeccionar nuestro identificador, verás que el span está anidado dentro de un div… que a su vez está dentro de otro div, y así sucesivamente. Si sigues subiendo, llegarás al div padre de todo el post.

<div class=" thing id-t3_8qccuv even  link self" ...>
    ...
    <div class="entry unvoted">
        <div class="top-matter">
            <p class="title">
                ...
                <span class="domain">
                ...
            </p>
        </div>
    </div>
</div>

Como ves, el padre común está 4 niveles por encima en el DOM. Los puntos suspensivos (…) representan contenido irrelevante. Ahora necesitamos una referencia al div del post para cada elemento de nuestra lista de domains. Podemos encontrar el padre de cualquier elemento en nuestro soup usando los métodos de BeautifulSoup.

for domain in soup.find_all("span", class_="domain"):
    if domain != "(self.datascience)":
        continue

    parent_div = domain.parent.parent.parent.parent
    print(parent_div.text)

Al ejecutar el script, se imprimirá el texto de todos los posts inbound. Puede que pienses que este código es frágil, y tienes razón. Nunca es buena idea depender solo de la estructura del DOM: es una solución propensa a romperse con cualquier cambio.

En su lugar, echemos un vistazo al div padre de cada post para ver si podemos distinguir inbound y outbound desde ahí. Cada div padre tiene un atributo llamado "data-domain", cuyo valor es justo lo que buscamos. Todos los posts inbound tienen "data-domain" igual a "self.datascience".

Como comentamos antes, podemos buscar etiquetas con una combinación de atributos usando BeautifulSoup. Y por suerte, Reddit clasifica el div padre de cada post con la clase "thing".

attrs = {'class': 'thing', 'data-domain': 'self.datascience'}

for post in soup.find_all('div', attrs=attrs):
    print(post.attrs['data-domain'])

La variable "attrs" es un diccionario con los atributos y valores que queremos buscar. Este enfoque es más seguro porque reduce las piezas móviles. Como ya filtramos por "self.datascience" al buscar, no necesitamos usar un if para saltarnos iteraciones: solo recibiremos posts con ese atributo.

Ahora que ya tenemos localizados los posts, solo nos falta extraer la información de sus elementos hijos. Y sí, es tan sencillo como parece.

Extraer la información de Reddit

De cada post necesitamos 4 datos:

  • Título
  • Autor
  • Likes
  • Comentarios

Si revisamos la estructura del div del post, podemos encontrarlo todo.

El título

Este es el más sencillo. En cada div del post hay un párrafo anidado a través de un par de divs. Es fácil de encontrar porque tiene la clase "title".

title = post.find('p', class_="title").text

El objeto "post" está dentro de nuestro bucle for anterior. También es un objeto de BeautifulSoup, pero contiene solo la estructura del DOM dentro del div del post. El método "find" devuelve un único objeto (a diferencia de "find_all", que devuelve una lista). Después de encontrar la etiqueta, queremos la cadena con el título, así que usamos su propiedad "text". También podemos leer otros atributos con "objeto.attrs['atributo']".

El autor

También es sencillo: inspecciona el nombre del autor bajo el título. Verás que está en una etiqueta de enlace con la clase "author".

author = post.find('a', class_='author').text

Los comentarios

Aquí hay un pequeño paso extra, pero sigue siendo fácil. Podemos encontrar los comentarios igual que el título y el autor.

comments = post.find('a', class_='comments').text

Al ejecutar esto obtendrás algo como "49 comments". Está bien, pero mejor quedarnos solo con el número. Para ello tiramos de un poco de Python.

Si usamos str.split() nos devolverá una lista con los elementos de la cadena separados por espacios. En este caso, obtendremos ["49", "comments"]. ¡Perfecto! Solo tenemos que quedarnos con el primer elemento.

comments = post.find('a', class_='comments').text.split()[0]

Aún falta un detalle: a veces no obtenemos un número, sino "comment". Pasa cuando no hay comentarios en un post. Como lo sabemos, comprobamos si el resultado es "comment" y lo sustituimos por "0".

if comments == "comment":
    comments = 0

Los likes

Encontrar el número de likes es pan comido y sigue la misma lógica.

likes = post.find("div", attrs={"class": "score likes"}).text

Fíjate en que hemos usado una combinación de dos clases. Es porque hay otros divs con la clase "score" o "likes", pero solo uno con la combinación "score likes".

Si el número de likes es 0, obtenemos 0. Pero si el post es tan reciente que aún no tiene votos, aparece "•". Vamos a sustituirlo por "None" para evitar confusiones en los resultados.

if likes == "•":
    likes = "None"

Escribir los resultados en CSV

Hasta ahora tenemos un bucle que extrae título, autor, likes y comentarios para cada post de la página. Python incluye un módulo estupendo para leer y escribir CSV llamado "csv". Solo tenemos que añadir una línea al final del bloque del bucle para añadir los datos al archivo CSV.

counter = 1
for post in posts:
    ...
    post_line = [counter, title, author, likes, comments]
    with open('output.csv', 'a') as f:
        writer = csv.writer(f)
        writer.writerow(post_line)

    counter += 1

Directo y claro, ¿verdad? "post_line" es un array con los elementos que irán separados por comas. La variable "counter" sirve para llevar la cuenta de cuántos posts hemos registrado.

Pasar a la siguiente página

Como contamos cuántos posts guardamos, basta con encerrar toda la lógica en otro bucle que solicite nuevas páginas hasta alcanzar cierta cantidad de posts.

counter = 1

while (counter <= 100):
    for post in posts:
        # Obtener título, autor, ...
        # Escribir en CSV e incrementar counter...

    next_button = soup.find("span", class_="next-button")
    next_page_link = next_button.find("a").attrs['href']
    time.sleep(2)
    page = requests.get(next_page_link, headers=headers)
    soup = BeautifulSoup(page.text, 'html.parser')

Hemos hecho varias cosas. Primero, movimos la variable "counter" fuera del bucle for para que pueda usarla el while.

Luego, ya sabemos qué hace el for, pero ¿y el resto? "next_button" localiza y guarda el botón "next". Después, buscamos el enlace dentro y obtenemos el atributo "href", que guardamos en "next_page_link".

Con ese enlace solicitamos la siguiente página, la guardamos en "page" y generamos de nuevo el soup con BeautifulSoup.

El fragmento anterior se detiene en 100, pero puedes fijar el límite que quieras.

Scrapear con responsabilidad

La línea de la que no hemos hablado es "time.sleep(2)". Merece su propia sección. Cualquier servidor web tiene recursos limitados; depende de nosotros no acapararlos. Además de que pueden banearte si haces cientos de peticiones en segundos, tampoco es correcto.

Recuerda que es un detalle por parte del sitio permitirte scrapear: podrían detectar bots tras 10 o 20 peticiones, o identificarte por el objeto de la solicitud que envía Python. Si te permiten scrapear sin necesidad de rotar IP, compórtate y ralentiza tu bot.

Puedes consultar la política de rastreo de un sitio en su archivo robots.txt. Suele estar en la raíz (p. ej., http://www.reddit.com/robots.txt).

¿Y ahora qué?

Lo que quieras. Como acabamos de ver, todo lo que ves en la web puede scrapearse y almacenarse localmente. Puedes aprovechar la información que hemos obtenido para múltiples fines. Con solo estos cuatro datos ya podemos sacar muchas conclusiones.

Analizando más a fondo, puedes descubrir qué tipo de posts de Reddit reciben más likes, qué palabras contienen, quién los publica. O, a la inversa, construir un "calculador de controversia" analizando la proporción de likes y comentarios.

Piensa en un post que recibe muchos comentarios pero pocos likes. Probablemente toca un tema que hace reaccionar a la gente, pero no necesariamente les gusta.

Las posibilidades son enormes, y está en tu mano decidir cómo usar la información.

Si te ha gustado este tutorial, echa un vistazo a nuestra guía para scrapear Amazon con Python y a nuestra colección de cursos de Python.

Temas
Python

Aprende más sobre Python

Curso

Introducción a Python

4 h
7M
Domina los fundamentos del análisis de datos con Python en cuatro horas y descubre sus paquetes más usados.
Ver detallesRight Arrow
Iniciar Curso
Ver másRight Arrow
Relacionado

blog

Cómo aprender Python desde cero en 2026: Una guía experta

Descubre cómo aprender Python en 2026, sus aplicaciones y la demanda de conocimientos de Python. Comienza hoy mismo tu aventura con Python. ​con nuestra guía completa.
Matt Crabtree's photo

Matt Crabtree

15 min

Tutorial

APIs Web, Peticiones en Python y Realización de una Petición HTTP en el Tutorial de Python

Conoce los fundamentos de HTTP y también la biblioteca de peticiones en Python para realizar distintos tipos de peticiones.
Kurtis Pykes 's photo

Kurtis Pykes

15 min

Tutorial

21 herramientas esenciales de Python

Conozca las herramientas esenciales de Python para el desarrollo de software, raspado y desarrollo web, análisis y visualización de datos y aprendizaje automático.
Abid Ali Awan's photo

Abid Ali Awan

6 min

Tutorial

Aprendizaje automático de datos categóricos con el tutorial de Python

Aprenda los trucos más comunes para manejar datos categóricos y preprocesarlos para construir modelos de aprendizaje automático.
Moez Ali's photo

Moez Ali

14 min

Tutorial

Tutorial de FastAPI: Introducción al uso de FastAPI

Explore el marco FastAPI y descubra cómo puede utilizarlo para crear API en Python.
Moez Ali's photo

Moez Ali

13 min

Tutorial

Desarrollo backend con Python: guía completa para principiantes

Esta guía completa te enseña los fundamentos del backend con Python. Aprende conceptos básicos, frameworks y buenas prácticas para empezar a crear aplicaciones web.
Oluseye Jeremiah's photo

Oluseye Jeremiah

15 min

Ver MásVer Más