En este módulo, exploraremos cómo funciona «tornet_scraper». Ya has aprendido a extraer datos de los foros «tornet» y «clearnet», pero para agilizar este proceso, necesitamos un rastreador web avanzado equipado con funciones para el rastreo de datos a gran escala.
Este tema puede parecer difícil para algunos, pero en la era de la IA, en la que gran parte de la programación se puede automatizar, y con este curso que aprovecha la IA para la ciberdefensa, crear una aplicación web para la extracción de datos es ahora más accesible.
Todas las aplicaciones web se basan en una pila tecnológica, una colección de herramientas de software que se utilizan para desarrollar los diferentes componentes de la aplicación. Por ejemplo, sitios web como Facebook cuentan con una interfaz de usuario (front-end) y una lógica del lado del servidor (back-end) que hace que las páginas sean accesibles. En algunos casos, el front-end y el back-end pueden utilizar el mismo lenguaje de programación. Por ejemplo, las aplicaciones web basadas en JavaScript pueden utilizar Vue para el front-end y Nuxt para el back-end, lo que simplifica el desarrollo, ya que ambos están escritos en JavaScript.
Para nuestros fines, necesitamos una pila tecnológica escalable y compatible con API que se pueda personalizar de forma independiente. Este módulo no profundizará en los conceptos de programación, ya que puedes copiar y pegar el código en tu herramienta de IA preferida para obtener una explicación personalizada que se adapte a tu estilo de aprendizaje.
Si prefieres saltarte este módulo y empezar con un proyecto práctico, puedes hacerlo, pero te perderás el aprendizaje del funcionamiento del proyecto, que será simplemente otra herramienta más. El proyecto final está disponible aquí:
https://github.com/CyberMounties/tornet_scraper
Los temas de esta sección incluyen lo siguiente:
- Cómo funcionan las plantillas
- Cómo funciona
main.py - Cómo funcionan las bases de datos
- Cómo funcionan las rutas
- Cómo funcionan los scrapers
- Cómo funcionan los servicios
Cómo funcionan las plantillas
Las aplicaciones web modernas utilizan plantillas, como una plantilla predeterminada o básica, para definir los elementos que aparecen en todas las páginas, como un menú de navegación o una barra de navegación. Aunque se podría copiar y pegar la barra de navegación en todas las páginas, cualquier cambio requeriría actualizar cada página individualmente.
Para agilizar este proceso, utilizamos un motor de plantillas. En las aplicaciones web de Python, Jinja2 nos permite crear una plantilla base con bloques de contenido para lograr un diseño coherente y eficiente.
Abre app/templates/base.html:
<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en" data-theme="nord">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %} - Tornet Scraper</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body class="min-h-screen bg-base-200">
<!-- Navbar -->
<div class="navbar bg-base-100 shadow-xl">
<div class="navbar-start">
<a class="btn btn-ghost text-xl">Tornet Scraper</a>
</div>
<div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1">
<li><a href="/" class="btn btn-ghost">Dashboard</a></li>
<li><a href="/proxy-gen" class="btn btn-ghost">Proxy Gen</a></li>
<li><a href="/manage-api" class="btn btn-ghost">API Management</a></li>
<li><a href="/bot-profile" class="btn btn-ghost">Bot Profile</a></li>
<li><a href="/marketplace-scan" class="btn btn-ghost">Marketplace Scan</a></li>
<li><a href="/posts-scans" class="btn btn-ghost">Posts Scans</a></li>
<li><a href="/watchlist" class="btn btn-ghost">Watchlist</a></li>
</ul>
</div>
<div class="navbar-end">
<div class="dropdown dropdown-end lg:hidden">
<label tabindex="0" class="btn btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</label>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
<li><a href="/">Dashboard</a></li>
<li><a href="/proxy-gen">Proxy Gen</a></li>
<li><a href="/manage-api">API Management</a></li>
<li><a href="/bot-profile">Bot Profile</a></li>
<li><a href="/marketplace-scan">Marketplace Scan</a></li>
<li><a href="/posts-scans" class="btn btn-ghost">Posts Scans</a></li>
<li><a href="/watchlist" class="btn btn-ghost">Watchlist</a></li>
</ul>
</div>
</div>
</div>
<!-- Main content -->
<main class="container mx-auto p-4 mt-6 bg-base-300 rounded-box">
<!-- Flash Messages Container -->
<div class="mb-4" id="flash-messages">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.category | default('info') }} shadow-lg mb-4 flex justify-between items-center flash-message">
<div>
<span>{{ message.text }}</span>
</div>
<button class="btn btn-sm btn-circle btn-ghost" onclick="this.parentElement.remove()">✕</button>
</div>
{% endfor %}
{% endif %}
</div>
{% block content %}
{% endblock %}
</main>
<script>
// Automatically remove flash messages after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
function removeFlashMessages() {
const flashMessages = document.querySelectorAll('.flash-message');
flashMessages.forEach(function(message) {
// Skip if already fading
if (message.style.opacity === '0') return;
setTimeout(function() {
message.style.transition = 'opacity 0.5s ease';
message.style.opacity = '0';
setTimeout(function() {
message.remove();
}, 500);
}, 5000);
});
}
removeFlashMessages();
const flashContainer = document.querySelector('#flash-messages');
if (flashContainer) {
const observer = new MutationObserver(function() {
removeFlashMessages();
});
observer.observe(flashContainer, { childList: true });
}
});
</script>
</body>
</html>
La plantilla base.html es una plantilla base de Jinja2 que proporciona un diseño coherente para todas las páginas. Incluye una barra de navegación, mensajes flash y marcadores de posición para el título y el contenido que las plantillas secundarias pueden sobrescribir. A continuación se ofrece una explicación técnica de cómo funciona y su mecanismo de extensión.
-
Estructura HTML:
- Define un documento HTML5 estándar con un atributo
data-theme=«nord»para el estilo DaisyUI. - Incluye recursos externos: CSS de DaisyUI, CSS de Tailwind y jQuery para el estilo y la interactividad.
- Define un documento HTML5 estándar con un atributo
-
Bloque de título:
- La etiqueta
<title>contiene un bloque Jinja2{% block title %}{% endblock %} - Tornet Scraper. - Las plantillas secundarias anulan el bloque
titlepara establecer un título específico de la página, al que se añade « - Tornet Scraper». - Ejemplo: una plantilla secundaria con
{% block title %}Dashboard{% endblock %}da como resultado<title>Dashboard - Tornet Scraper</title>.
- La etiqueta
-
Barra de navegación:
- Una barra de navegación adaptativa con un logotipo («Tornet Scraper») y enlaces de navegación (Panel de control, Generador de proxies, etc.).
- En pantallas grandes (
lg:flex), los enlaces se muestran horizontalmente; en pantallas más pequeñas, un menú desplegable (activado por un icono de hamburguesa) muestra los enlaces verticalmente.
-
Mensajes flash:
- Un
<div id="flash-messages">muestra los mensajes enviados desde el backend (por ejemplo, notificaciones de éxito o error). - Utiliza Jinja2:
{% if messages %}recorremessages(una lista de objetos contextycategory). - Cada mensaje tiene el estilo de una
alertade DaisyUI con una clase dinámica (alert-{{ message.category }}, cuyo valor predeterminado esinfo). - Un botón de cierre (
✕) elimina el mensaje al hacer clic. - JavaScript desvanece y elimina automáticamente los mensajes flash después de 5 segundos utilizando una transición CSS (
opacity). - Un
MutationObservergarantiza que los nuevos mensajes flash (añadidos dinámicamente) también se eliminen automáticamente.
- Un
-
Bloque de contenido:
- La sección
<main>contiene un marcador de posición{% block content %}{% endblock %}. - Las plantillas secundarias anulan este bloque para insertar contenido específico de la página en el contenedor principal, con el estilo de las clases Tailwind/DaisyUI (
container,mx-auto, etc.).
- La sección
-
Extensión en plantillas secundarias:
- Las plantillas secundarias utilizan
{% extends “base.html” %}para heredar la estructura base. - Anulan
{% block title %}para el título de la página y{% block content %}para el contenido principal. - Esto renderiza el diseño completo de
base.htmlcon «Dashboard» en el título y el contenido especificado en la sección<main>.
- Las plantillas secundarias utilizan
Esta configuración garantiza una interfaz de usuario coherente, títulos dinámicos, navegación reutilizable y un manejo de mensajes flash fácil de usar en todas las páginas.
dashboard.html
El archivo ubicado en app/templates/dashboard.html proporciona un ejemplo claro de cómo se extiende base.html. Aunque este archivo no es estrictamente necesario, he adquirido el hábito de incluirlo. A continuación se muestra el código que contiene:
<!-- app/templates/dashboard.html -->
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold text-base-content mb-4">Dashboard</h1>
{% endblock %}
¿Te fijas cómo se extiende «base.html»? El contenido dentro de «{% block content %}» se inserta en el contenedor de contenido principal definido en «base.html»:
<!-- Main content -->
<main class="container mx-auto p-4 mt-6 bg-base-300 rounded-box">
{% block content %}
{% endblock %}
</main>
El título también se define:
{% block title %}Dashboard{% endblock %}
En «base.html», la estructura del título es:
<title>{% block title %}{% endblock %} - Tornet Scraper</title>
Al ver el código fuente de la página del panel de control, el título aparece como:
<title>Dashboard - Tornet Scraper</title>
En lugar de reescribir el título en cada página, puedes definirlo en base.html y luego solo definir el nombre de la página para todas las demás páginas.
Cómo funciona «main.py»
Puedes encontrar «main.py» en «app/main.py».
El archivo «main.py» es el núcleo de una aplicación web FastAPI, ya que define las rutas, las plantillas y las interacciones con la base de datos para una interfaz de scraping web. A continuación se ofrece una explicación técnica de las variables globales, los enrutadores incluidos y las funciones de controlador de rutas.
Variables globales
-
app:- Propósito: La instancia principal de la aplicación FastAPI.
- Detalles: Inicializa el marco FastAPI para gestionar las solicitudes y respuestas HTTP.
-
BASE_DIR:- Propósito: Almacena la ruta absoluta del directorio raíz del proyecto.
- Detalles: Se deriva utilizando
os.pathpara localizar el directorioapp/templatespara las plantillas Jinja2.
-
TEMPLATES_DIR:- Propósito: Especifica el directorio para las plantillas Jinja2.
- Detalles: Se establece en
app/templatesdentro de la raíz del proyecto para renderizar plantillas HTML.
-
templates:- Propósito: Instancia del motor de plantillas Jinja2.
- Detalles: Configurado para cargar plantillas desde
TEMPLATES_DIRpara renderizar HTML dinámico.
Enrutadores incluidos
-
proxy_gen_router:- Propósito: Registra rutas para la funcionalidad de generación de proxy.
- Detalles: Importado desde
app.routes.proxy_gen, gestiona los puntos finales relacionados con el proxy.
-
manage_api_router:- Propósito: Registra rutas para la gestión de API.
- Detalles: Importado desde
app.routes.manage_api, gestiona las claves y la configuración de la API.
-
bot_profile_router:- Finalidad: Registra rutas para la gestión de perfiles de bots.
- Detalles: Importado desde
app.routes.bot_profile, gestiona la configuración de los bots.
-
marketplace_api_router:- Finalidad: Registra rutas para el escaneo del mercado.
- Detalles: Importado desde
app.routes.marketplace, gestiona la recuperación de datos del mercado.
-
posts_api_router:- Finalidad: Registra rutas para el escaneo de publicaciones.
- Detalles: Importado desde
app.routes.posts, gestiona las operaciones de datos relacionadas con las publicaciones.
-
watchlist_api_router:- Finalidad: Registra rutas para la gestión de la lista de seguimiento.
- Detalles: Importado desde
app.routes.watchlist, gestiona los elementos de la lista de seguimiento y los escaneos.
Funciones del controlador de rutas
-
dashboard:- Propósito: Renderiza la página del panel de control.
- Parámetros clave:
request: ObjetoRequestde FastAPI para el contexto de la plantilla.db:Sessionde SQLAlchemy para acceder a la base de datos (a través deDepends(get_db)).
- Devuelve:
TemplateResponseque renderizadashboard.htmlcon mensajes flash de la sesión.
-
proxy_gen:- Propósito: Muestra la página de generación de proxies con una lista de proxies.
- Parámetros clave:
request: objetoRequestde FastAPI.db:Sessionde SQLAlchemy para consultar proxies.
- Devuelve:
TemplateResponseque renderizaproxy_gen.htmlcon datos de proxy (nombre del contenedor, IP, nodo de salida de Tor, marca de tiempo, estado de ejecución) y mensajes flash.
-
manage_api:- Propósito: Muestra la página de gestión de API con los detalles de la API.
- Parámetros clave:
request: ObjetoRequestde FastAPI.db:Sessionde SQLAlchemy para consultar las API.
- Devuelve:
TemplateResponseque renderizamanage_api.htmlcon datos de la API (ID, nombre, proveedor, clave, modelo, tokens máximos, mensaje, marca de tiempo, estado activo) y mensajes flash.
-
bot_profile:- Propósito: Muestra la página de gestión del perfil del bot.
- Parámetros clave:
request: objeto FastAPIRequest.db: SQLAlchemySessionpara consultar perfiles de bots y la última URL onion.
- Devuelve:
TemplateResponseque renderizabot_profile.htmlcon datos del perfil del bot (ID, nombre de usuario, contraseña enmascarada, propósito, proxy Tor, marca de tiempo), la última URL onion y mensajes flash.
-
marketplace:- Propósito: Renderiza la página de escaneo del mercado con paginación y datos de escaneo de publicaciones.
- Parámetros clave:
request: ObjetoRequestde FastAPI.db:Sessionde SQLAlchemy para consultar escaneos.
- Devuelve:
TemplateResponseque muestramarketplace.htmlcon escaneos paginados, escaneos publicados y mensajes mostrados.
-
posts_scans:- Propósito: Muestra la página de resumen de escaneos publicados.
- Parámetros clave:
request: objetoRequestde FastAPI.db: SQLAlchemySessionpara acceder a la base de datos.
- Devuelve:
TemplateResponseque renderizaposts_scans.htmlcon mensajes flash.
-
posts_scan_result:- Propósito: Muestra los resultados de un escaneo de publicaciones específico.
- Parámetros clave:
scan_id: ID entero del escaneo.request: objetoRequestde FastAPI.db:Sessionde SQLAlchemy para acceder a la base de datos.name: nombre opcional del escaneo (cadena, por defecto vacío).
- Devuelve:
TemplateResponseque renderizaposts_scan_result.htmlcon el ID del escaneo, el nombre y los mensajes emergentes.
-
watchlist:- Propósito: Representa la página de resumen de la lista de seguimiento.
- Parámetros clave:
request: ObjetoRequestde FastAPI.db:Sessionde SQLAlchemy para acceder a la base de datos.
- Devuelve:
TemplateResponseque representawatchlist.htmlcon mensajes flash.
-
watchlist_profile:- Propósito: Muestra el perfil de un elemento específico de la lista de seguimiento y los escaneos asociados.
- Parámetros clave:
target_id: ID entero del elemento de la lista de seguimiento.request: objetoRequestde FastAPI.db:Sessionde SQLAlchemy para consultar el elemento de la lista de seguimiento y los escaneos.
- Devuelve:
TemplateResponseque renderizawatchlist_profile.htmlcon el elemento de la lista de seguimiento, los datos del escaneo y los mensajes flash; lanza unaHTTPException(404 si falta el elemento, 500 si hay errores del servidor).
Cómo funcionan las bases de datos
En nuestra aplicación FastAPI, db.py gestiona las configuraciones del motor de la base de datos, mientras que models.py define el esquema y las tablas de la base de datos. El uso de un archivo db.py independiente permite cambiar fácilmente entre tipos de bases de datos. Aunque tornet_scraper utiliza SQLite3 para la creación de prototipos y las pruebas, no es ideal para el scraping de datos a gran escala. Para entornos de producción, recomendamos pasar a una base de datos más robusta, como PostgreSQL.
También puedes implementar lógica en db.py para cambiar dinámicamente entre SQLite3 y un servidor de base de datos en función de las condiciones o el entorno (por ejemplo, desarrollo, pruebas o producción).
db.py
El archivo db.py configura la conexión a la base de datos y la gestión de sesiones para una aplicación FastAPI que utiliza SQLAlchemy. A continuación se ofrece una explicación concisa de sus componentes y funcionalidad.
Puede encontrar db.py en app/database/db.py.
-
Propósito:
- Configura una conexión a la base de datos SQLite, crea tablas y proporciona una dependencia para las sesiones de la base de datos en las rutas FastAPI.
-
Variables globales:
BASE_DIR:- Propósito: Define el directorio raíz del proyecto.
- Detalles: Utiliza
Path(__file__).resolve().parent.parent.parentpara calcular la ruta absoluta a la raíz del proyecto, asegurando que el archivo de la base de datos se encuentra en la ubicación correcta.
DATABASE_URL:- Propósito: Especifica la cadena de conexión a la base de datos.
- Detalles: Configurado para SQLite como
sqlite:///{BASE_DIR}/tornet_scraper.db, apuntando a un archivotornet_scraper.dben la raíz del proyecto.
engine:- Propósito: Motor SQLAlchemy para interacciones con la base de datos.
- Detalles: Creado con
create_engine(DATABASE_URL, connect_args={«check_same_thread»: False})para gestionar las conexiones SQLite. El argumentocheck_same_threadpermite utilizar SQLite en un entorno FastAPI multihilo.
SessionLocal:- Finalidad: Fábrica para crear sesiones de base de datos.
- Detalles: Configurado con
sessionmaker(autocommit=False, autoflush=False, bind=engine)para crear sesiones vinculadas al motor, con control manual de confirmación y vaciado.
-
Funciones:
get_db:- Propósito: Proporciona una sesión de base de datos para las rutas FastAPI.
- Parámetros clave: Ninguno (función de dependencia).
- Devuelve: Devuelve una sesión SQLAlchemy (
db) y garantiza que se cierre después de su uso. - Detalles: Se utiliza como dependencia FastAPI (a través de
Depends(get_db)) para inyectar una sesión en los controladores de rutas. El bloquetry/finallygarantiza que la sesión se cierre, evitando fugas de recursos.
init_db:- Propósito: Inicializa la base de datos creando tablas.
- Parámetros clave: Ninguno.
- Devuelve: Ninguno.
- Detalles: Importa
Basedesdemodels.pyy llama aBase.metadata.create_all(bind=engine)para crear todas las tablas definidas (por ejemplo,proxies,apis) en la base de datos SQLite si no existen.
-
Uso:
- Propósito: Habilita las operaciones de la base de datos en la aplicación.
- Detalles:
init_dbse llama enmain.pypara configurar el esquema de la base de datos al inicio.get_dbse utiliza en los controladores de rutas (por ejemplo,/proxy-gen,/manage-api) para consultar o modificar datos en las tablas definidas enmodels.py. La sesión garantiza la integridad de las transacciones y la limpieza adecuada de los recursos.
models.py
El archivo models.py define el esquema de la base de datos para una aplicación FastAPI utilizando SQLAlchemy, especificando tablas, columnas y relaciones. A continuación se ofrece una explicación concisa de sus componentes y funcionalidad.
Puede encontrar models.py en app/database/models.py.
-
Descripción general de SQLAlchemy:
- Propósito: SQLAlchemy es una biblioteca ORM (mapeo objeto-relacional) para Python que permite la interacción con una base de datos relacional utilizando objetos Python.
- Detalles: Mapea clases de Python a tablas de bases de datos, lo que permite operaciones CRUD a través de una sintaxis orientada a objetos. Utiliza
declarative_basepara crear una clase base (Base) para las definiciones de modelos.
-
Columnas:
- Propósito: Representan campos en una tabla de base de datos.
- Detalles: Se definen utilizando la clase
Columnde SQLAlchemy, especificando atributos como el tipo de datos (por ejemplo,Integer,String,DateTime), restricciones (por ejemplo,primary_key,unique,nullable) y valores predeterminados (por ejemplo,datetime.utcnow). Las columnas almacenan los datos de cada registro de una tabla.
-
Tablas:
- Finalidad: Representar tablas de bases de datos que almacenan datos estructurados.
- Detalles: Cada clase (por ejemplo,
Proxy,APIs) hereda deBasey define una tabla mediante__tablename__. Las tablas incluyen columnas y restricciones opcionales (por ejemplo,UniqueConstraintpara combinaciones únicas de campos). Algunos ejemplos sonproxies,apis,bot_profiles, etc.
-
Relaciones:
- Finalidad: Definen asociaciones entre tablas para consultas relacionales.
- Detalles: Se establecen mediante columnas
ForeignKey(por ejemplo,MarketplacePost.scan_idhace referencia amarketplace_post_scans.id). Las relaciones permiten unir tablas, como vincularMarketplacePostaMarketplacePostScana través descan_id. SQLAlchemy las maneja como referencias de objetos en las consultas.
-
Enums:
- Propósito: definir vocabularios controlados para campos específicos.
- Detalles: Utiliza
enum.Enumde Python (por ejemplo,BotPurpose,ScanStatus) para restringir los valores de las columnas a opciones predefinidas (por ejemplo,SCRAPE_MARKETPLACE,RUNNING). Se almacenan como cadenas en la base de datos medianteEnum.
-
Componentes clave:
Base: La clase base de SQLAlchemy para todos los modelos, utilizada para generar esquemas de tablas.- Definiciones de tablas: Clases como
Proxy,APIs,BotProfile, etc., definen tablas para almacenar detalles de proxies, configuraciones de API, perfiles de bots, URL de onion, escaneos de marketplaces, publicaciones y listas de seguimiento. - Restricciones: Las restricciones únicas (por ejemplo,
UniqueConstraintenMarketplacePost) garantizan la integridad de los datos al evitar registros duplicados basados en combinaciones específicas de columnas. - Marcas de tiempo: Muchas tablas incluyen una columna
timestamp(por defectodatetime.utcnow) para realizar un seguimiento de las horas de creación/actualización de los registros.
-
Uso:
- Finalidad: La aplicación FastAPI utiliza los modelos para interactuar con la base de datos.
- Detalles: Las rutas
main.pyconsultan estos modelos (a través de sesiones SQLAlchemy) para recuperar o almacenar datos, que luego se pasan a las plantillas Jinja2 para su representación o se procesan en la lógica de la API. Por ejemplo, los datosProxyse consultan en la ruta/proxy-genpara mostrar los detalles del proxy.
Cómo funcionan las rutas
En el desarrollo de API, es fundamental organizar los puntos finales de la API de forma eficaz, ya que no es práctico enrutar todas las solicitudes a través de un único punto final.
Por ejemplo, el formato /api/posts/get-post indica que /api/posts/* está dedicado a operaciones relacionadas con las publicaciones, como ver, eliminar o modificar publicaciones. Todas las llamadas a la API relacionadas tienen el prefijo /api/posts/.
Dado que nuestra aplicación incluye funciones como la generación de proxies, la gestión de bots, el scraping de mercados, el scraping de publicaciones, la supervisión de amenazas y mucho más, es esencial organizar correctamente los puntos finales.
Puedes encontrar todas las rutas en app/routes/*.py, que contiene seis archivos Python. A continuación se muestra un ejemplo de bot_profile.py:
# app/routes/bot_profile.py
import logging
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.database.models import BotProfile, OnionUrl, BotPurpose, APIs
from app.database.db import get_db
from typing import Optional
from app.services.tornet_forum_login import login_to_tor_website
from app.services.gen_random_ua import gen_desktop_ua
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
bot_profile_router = APIRouter(prefix="/api/bot-profile", tags=["API", "Bot Profile Management"])
# Pydantic models for validation
class BotProfileCreate(BaseModel):
username: str
password: str
purpose: str
tor_proxy: Optional[str] = None
session: Optional[str] = None
class BotProfileUpdate(BaseModel):
username: Optional[str] = None
password: Optional[str] = None
purpose: Optional[str] = None
tor_proxy: Optional[str] = None
user_agent: Optional[str] = None
session: Optional[str] = None
class OnionUrlCreate(BaseModel):
url: str
# Get all bot profiles
@bot_profile_router.get("/list")
async def get_bot_profiles(db: Session = Depends(get_db)):
try:
profiles = db.query(BotProfile).all()
return [
{
"id": p.id,
"username": p.username,
"password": "********",
"actual_password": p.password,
"purpose": p.purpose.value,
"tor_proxy": p.tor_proxy,
"has_session": bool(p.session and len(p.session) > 0),
"session": p.session,
"user_agent": p.user_agent,
"timestamp": p.timestamp.isoformat()
} for p in profiles
]
except Exception as e:
logger.error(f"Error fetching bot profiles: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
Utilizamos el registro para realizar un seguimiento de los eventos dentro de la aplicación. El registro es fundamental para comprender lo que está sucediendo en su aplicación web; sin él, estará operando a ciegas. Aunque los registros se pueden guardar en un archivo, yo suelo evitarlo durante el desarrollo local, pero lo recomiendo para entornos de producción.
Sin embargo, el aspecto más crítico es la creación de enrutadores:
bot_profile_router = APIRouter(prefix="/api/bot-profile", tags=["API", "Bot Profile Management"])
Este enrutador establece un prefijo de punto final, /api/bot-profile, que utilizarán todos los puntos finales relacionados.
Por ejemplo, la siguiente función recupera todos los perfiles de bot de la tabla BotProfile:
@bot_profile_router.get("/list")
async def get_bot_profiles(db: Session = Depends(get_db)):
Cuando utilizas @bot_profile_router.get(«/list»), el punto final se antepone con /api/bot-profile, lo que da como resultado /api/bot-profile/list.
Esta estructura permite organizar de forma eficiente cientos o miles de rutas API. El bot_profile_router se importa a main.py y se registra.
Cómo funcionan los scrapers
Todos los scrapers se almacenan en app/scrapers/*.py.
Estos archivos suelen importarse como módulos en otras partes del código base. Por ejemplo, dentro de marketplace_scraper.py, la función create_pagination_batches toma como entrada la URL de una página web, como por ejemplo:
http://site.url/pagination?page=1
Crea un lote de paginaciones como este:
http://site.url/pagination?page=1
http://site.url/pagination?page=2
http://site.url/pagination?page=3
--- snip ---
http://site.url/pagination?page=10
Este enfoque es eficaz para la distribución de tareas, como asignar cada lote de 10 páginas de paginación a un solo bot para su scraping.
Cómo funcionan los servicios
Todos los servicios se encuentran en app/services/*.py.
Los servicios también se utilizan como módulos, pero tienen una finalidad distinta, ya que se encargan de tareas como:
- Gestionar Docker para los proxies
- Generar proxies
- Realizar inicios de sesión
- Omitir CAPTCHAs
Como aprenderás más adelante, utilizamos contenedores Docker para crear proxies Tor localmente. Cuando un proxy ya no es necesario, debe eliminarse. Para verificar si los proxies se están ejecutando, puedes utilizar una función como container_running de container_status.py para comprobar el estado del contenedor Docker.