En esta sección explicaré cómo podemos realizar un scraping a gran escala de las publicaciones recopiladas. El trabajo preliminar ya está hecho, los detalles de las publicaciones se han extraído, pero su contenido no, por lo que en la fase final extraemos el contenido y lo traducimos si es necesario.
Esta es la fase más interesante porque estamos llegando al final del curso. Identificar las ventas de IAB era nuestro objetivo inicial, que es para lo que estás aquí, pero esta sección puede ser la más complicada de todas, al menos a primera vista.
Es compleja porque estamos creando una solución para el escaneo de datos a gran escala, lo que obviamente va a ser complicado, con muchas partes móviles, pero es la realidad de la monitorización de amenazas a gran escala.
Los temas de esta sección incluyen lo siguiente:
- Componentes del rastreador de datos
- Modelos de bases de datos
- Plantillas para gestionar y mostrar los escaneos
- Plantilla para mostrar el resultado de cada escaneo
- Rutas de backend
- Pruebas
Componentes del rastreador de datos
Nuestro rastreador de datos consta de varios componentes, incluidos módulos diseñados para tareas como rastrear detalles de publicaciones, traducir contenido cuando es necesario y clasificar datos.
Los componentes principales se encuentran en app/scrapers/post_scraper.py.
-
scrape_post_details:- Finalidad: extrae detalles (título, marca de tiempo, autor, contenido) de una URL de publicación específica utilizando técnicas de extracción web.
- Parámetros clave:
post_link: URL de la publicación que se va a extraer.session_cookie: cookie de autenticación para acceder a la publicación.tor_proxy: Dirección proxy opcional para el enrutamiento Tor.user_agent: Cadena de agente de usuario para los encabezados de solicitud.timeout: Duración del tiempo de espera de la solicitud (por defecto: 30 segundos).
- Devuelve: Cadena JSON que contiene los detalles de la publicación extraída o información sobre el error si la solicitud falla.
-
translate_string:- Propósito: Detecta el idioma de una cadena de entrada y la traduce al inglés (o al idioma de destino especificado) utilizando la API de DeepL si aún no está en inglés.
- Parámetros clave:
input_string: Texto a analizar y posiblemente traducir.auth_key: Clave de autenticación de la API de DeepL.target_lang: Idioma de destino para la traducción (por defecto: EN-US).
- Devuelve: Cadena JSON con el texto original, el idioma detectado y el texto traducido (si procede) o los detalles del error.
-
iab_classify:- Finalidad: Clasifica una publicación utilizando el modelo Claude de Anthropic para determinar si se refiere a la venta de acceso inicial, artículos no relacionados o advertencias/quejas.
- Parámetros clave:
api_key: Clave API de Anthropic para la autenticación.model_name: Nombre del modelo Claude que se va a utilizar (por ejemplo, «claude-3-5-sonnet-20241022»).prompt: Texto que contiene la publicación que se va a clasificar.max_tokens: Número máximo de tokens de salida (por defecto: 100).
- Devuelve: Cadena JSON con el resultado de la clasificación, las puntuaciones o la información de error si la clasificación falla.
Función scrape_post_details
El foro Tornet requiere una sesión iniciada para leer las publicaciones, lo cual es habitual en la mayoría de los foros. Para solucionar esto, he desarrollado una función que toma un enlace a una publicación y recupera sus datos.
Este enfoque es lógico, ya que la tabla marketplace_posts almacena todos los detalles y enlaces de las publicaciones. Al cargar estos datos, podemos pasar cada enlace de publicación a una función como scrape_post_details para extraer la información necesaria.
Función translate_string
En la función translate_string, utilizamos DeepL para la traducción de datos. Sin embargo, en app/routes/posts.py, primero empleamos la biblioteca langdetect para identificar el idioma de una publicación. Si la detección del idioma falla o el idioma detectado no es el inglés, pasamos la publicación a la función translate_string.
Una ventaja clave es que si se proporciona una cadena que contiene saltos de línea:
Venta de acceso a Horizon Logistics\nIngresos: 1200 millones de dólares\nAcceso: RDP con DA\nPrecio: 0,8 BTC\nDM para más detalles
La función los conserva en la salida traducida:
Sale of access to Horizon Logistics\nRevenue: $1.2 billion\nAccess: RDP with DA\nPrice: 0.8 BTC\nDM for more details
Función iab_classify
En iab_classify, nuestra temperatura está establecida por defecto en 0,1, pero puedes cambiarla si lo deseas.
En las interacciones con IA o LLM, la temperatura es un hiperparámetro que controla la aleatoriedad o la creatividad de la salida del modelo:
- Propósito: ajusta la distribución de probabilidad de las posibles salidas del modelo (por ejemplo, palabras o tokens) durante la generación.
- Cómo funciona:
- Temperatura baja (por ejemplo, 0,1): hace que el modelo sea más determinista, favoreciendo los resultados de alta probabilidad. Da lugar a respuestas más centradas, predecibles y conservadoras.
- Temperatura alta (por ejemplo, 1,0 o superior): aumenta la aleatoriedad, dando más posibilidades a los resultados con menor probabilidad. Da lugar a respuestas más creativas, diversas o inesperadas.
- Ejemplo en código: en la función
iab_classifyproporcionada, se utilizatemperature=0.1para que los resultados de clasificación del modelo Claude sean más coherentes y menos aleatorios. - Rango: Normalmente entre 0 y 1, aunque algunos modelos permiten valores más altos para una aleatoriedad extrema.
Modelos de base de datos
Para extraer publicaciones, el tamaño de los lotes de publicaciones y almacenar datos, necesitamos dos tablas. Así es como se ven tus modelos:
class PostDetailScan(Base):
__tablename__ = "post_detail_scans"
id = Column(Integer, primary_key=True, index=True)
scan_name = Column(String, nullable=False, unique=True)
source_scan_name = Column(String, ForeignKey("marketplace_post_scans.scan_name"), nullable=False)
start_date = Column(DateTime(timezone=True), default=datetime.utcnow)
completion_date = Column(DateTime(timezone=True), nullable=True)
status = Column(Enum(ScanStatus), default=ScanStatus.STOPPED, nullable=False)
batch_size = Column(Integer, nullable=False)
site_url = Column(String, nullable=False)
timestamp = Column(DateTime(timezone=True), default=datetime.utcnow)
class MarketplacePostDetails(Base):
__tablename__ = "marketplace_post_details"
id = Column(Integer, primary_key=True, index=True)
scan_id = Column(Integer, ForeignKey("post_detail_scans.id"), nullable=False)
batch_name = Column(String, nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
timestamp = Column(String, nullable=False)
author = Column(String, nullable=False)
link = Column(String, nullable=False)
original_language = Column(String, nullable=True)
original_text = Column(Text, nullable=True)
translated_language = Column(String, nullable=True)
translated_text = Column(Text, nullable=True)
is_translated = Column(Boolean, default=False)
sentiment = Column(String, nullable=True)
positive_score = Column(Float, nullable=True)
negative_score = Column(Float, nullable=True)
neutral_score = Column(Float, nullable=True)
timestamp_added = Column(DateTime(timezone=True), default=datetime.utcnow)
__table_args__ = (UniqueConstraint('scan_id', 'timestamp', 'batch_name', name='uix_scan_timestamp_batch'),)
post_detail_scans
La tabla post_detail_scans se utiliza para crear escaneos que recuperan datos de la tabla marketplace_post_scans. También almacena el tamaño del lote, ya que muchos sitios imponen límites de velocidad, como restricciones en el número de publicaciones que se pueden leer en 24 horas. Para gestionar esto, dividimos las publicaciones en lotes de 10 o 20 y asignamos estos lotes a bots configurados con el propósito scrape_post.
La tabla marketplace_post_scans almacena los metadatos de las publicaciones, incluyendo el título, el enlace, la marca de tiempo y el autor, pero excluye el contenido detallado.
marketplace_post_details
Esta tabla almacena los resultados de cada escaneo. Iniciamos los escaneos en «post_detail_scans», los cargamos en el rastreador y comenzamos a recopilar datos. Una vez recopilados, los datos se guardan en la tabla «marketplace_post_details».
Realizamos un seguimiento de todos los detalles, incluyendo el texto original, el idioma original, el texto traducido, el sentimiento, las puntuaciones de confianza, el autor, la marca de tiempo y mucho más.
Plantillas para gestionar y mostrar los escaneos
Necesitamos dos plantillas: una para gestionar los escaneos y otra para mostrar los resultados de cada escaneo. La plantilla para gestionar los escaneos es posts_scans.html. Aquí tienes una vista previa de su interfaz:

El código correspondiente se encuentra en app/templates/posts_scans.html.
-
Creación e inicio del escaneo de detalles de la publicación:
- Propósito: Crea e inicia un nuevo escaneo de detalles de la publicación.
- Interacción con el backend:
- El botón «Nuevo escaneo de detalles de la publicación» abre un modal (
newScanModal) con campos para el nombre del escaneo, el escaneo de la publicación de origen (menú desplegable de escaneos completados), el tamaño del lote y la URL del sitio. - El envío del formulario activa una solicitud AJAX POST a
/api/posts-scanner/create(gestionada porposts_api_router) con los datos del formulario, seguida de una solicitud POST a/api/posts-scanner/{id}/startpara iniciar el escaneo. - El backend crea un registro «PostDetailScan», lo vincula a un «MarketplacePostScan» y comienza el rastreo. Los datos del formulario se almacenan en «sessionStorage» para su reutilización en «startScan()». Si se realiza correctamente, se muestra una alerta de éxito, se cierra el modal y «refreshScans()» actualiza la tabla. Los errores activan una alerta con el mensaje de error.
- El botón «Nuevo escaneo de detalles de la publicación» abre un modal (
-
Listado y actualización del escaneo de detalles de publicaciones:
- Propósito: muestra y actualiza una tabla con los escaneos de detalles de publicaciones.
- Interacción con el backend:
- La función
refreshScans(), llamada al cargar la página y al pulsar el botón «Refresh Scans», envía una solicitud AJAX GET a/api/posts-scanner/list(gestionada porposts_api_router). - El backend devuelve una lista de registros
PostDetailScan(ID, nombre del escaneo, nombre del escaneo de origen, fechas de inicio/finalización, publicaciones extraídas, estado). La tabla se rellena con insignias de estado (por ejemplo,badge-successpara completadas). Si no hay escaneos, se muestra el mensaje «No hay escaneos disponibles». Los errores activan una alerta.
- La función
-
Iniciar un escaneo detallado de una publicación:
- Objetivo: Inicia un escaneo detallado de una publicación existente.
- Interacción con el backend:
- El botón «Iniciar» de cada fila de la tabla (desactivado para los escaneos en ejecución) llama a
startScan(scanId), enviando una solicitud AJAX POST a/api/posts-scanner/{scanId}/start(gestionada porposts_api_router) conbatch_sizeysite_urldesessionStorage. - El backend inicia el escaneo y actualiza el estado de
PostDetailScan. Si se realiza correctamente, se muestra una alerta de éxito yrefreshScans()actualiza la tabla. Los errores activan una alerta.
- El botón «Iniciar» de cada fila de la tabla (desactivado para los escaneos en ejecución) llama a
-
Visualización de los resultados del escaneo:
- Finalidad: Redirige a una página de resultados para un escaneo específico.
- Interacción con el backend:
- El botón «Ver» de cada fila de la tabla llama a
viewResults(scanId, scanName), redirigiendo a/posts-scan-result/{scanId}?name={scanName}(gestionado pormain.py::posts_scan_result). - El backend renderiza una plantilla con los detalles del escaneo, recuperando los registros
MarketplacePostDetailsasociados. Aquí no se produce ninguna llamada AJAX directa, pero la redirección depende de los datos del backend.
- El botón «Ver» de cada fila de la tabla llama a
-
Eliminar un escaneo de detalles de una publicación:
- Propósito: Elimina un escaneo de detalles de una publicación.
- Interacción con el backend:
- El botón «Eliminar» de cada fila de la tabla llama a
deleteScan(scanId)tras la confirmación del usuario, enviando una solicitud AJAX DELETE a/api/posts-scanner/{scanId}(gestionado porposts_api_router). - El backend elimina el registro
PostDetailScan. Si se realiza correctamente, se muestra una alerta de éxito yrefreshScans()actualiza la tabla. Los errores activan una alerta.
- El botón «Eliminar» de cada fila de la tabla llama a
-
Rellenar el menú desplegable de escaneos de publicaciones de origen:
- Objetivo: Rellena el menú desplegable de escaneos de origen con los escaneos de publicaciones completados.
- Interacción del backend:
- Al cargar la página, una solicitud AJAX GET a
/api/posts-scanner/completed-post-scans(gestionada porposts_api_router) obtiene una lista de nombresMarketplacePostScancompletados. - El backend devuelve los nombres de los escaneos, que se añaden como opciones al menú desplegable del nuevo modal de escaneo. Los errores se registran en la consola.
- Al cargar la página, una solicitud AJAX GET a
Plantilla para mostrar el resultado de cada escaneo
Al mostrar el resultado de cada escaneo del mercado, utilizamos modales para mostrarlos. Pero aquí, debido a que hay mucha información, búsquedas y filtros, necesitamos una plantilla diferente solo para mostrar los resultados.
Así es como utilizo la búsqueda y la clasificación de sentimientos para filtrar los resultados positivos que hablan de IAB y tienen la palabra clave «shell» en el título:

Como analista, algo así te resulta extremadamente útil porque puedes filtrar los resultados, ver solo lo que consideras importante para tu investigación y descargarlo en formato JSON.
La plantilla utilizada para mostrar el resultado de cada escaneo se encuentra en: app/templates/posts_scan_result.html.
-
Carga de los resultados del escaneo:
- Propósito: muestra los resultados de un escaneo detallado de una publicación específica en una tabla.
- Interacción con el backend:
- Al cargar la página, la función
loadResults()extrae elscanIdde la URL y envía una solicitud AJAX GET a/api/posts-scanner/{scanId}/results(gestionada porposts_api_router). - El backend consulta la tabla
MarketplacePostDetailspara obtener los resultados del escaneo (ID, título, marca de tiempo, autor, nombre del lote, puntuaciones de sentimiento, idioma, texto traducido) y los devuelve como JSON. - La tabla se rellena con filas, cada una de las cuales muestra una casilla de verificación, el nombre del lote, el título truncado, la marca de tiempo, el autor, las puntuaciones de sentimiento (positivo, negativo, neutro), el sentimiento dominante (calculado en el lado del cliente como la puntuación más alta), el idioma y el estado de la traducción. Los errores activan una alerta.
- Al cargar la página, la función
-
Búsqueda y filtrado por sentimiento:
- Objetivo: Filtra la tabla de resultados por título de la publicación y sentimiento.
- Interacción con el backend:
- La función
filterTable(), activada por la tecla#searchInputy el cambio en#sentimentFilter, filtra las filas de la tabla en el lado del cliente en función del término de búsqueda (título) y el sentimiento seleccionado (all,positive,negative,neutral). - No se realizan llamadas directas al backend; el filtrado utiliza el atributo
data-sentimentestablecido duranteloadResults(). Las filas se muestran u ocultan en función de las coincidencias, lo que garantiza actualizaciones dinámicas sin solicitudes adicionales.
- La función
-
Visualización de los detalles de la publicación:
- Finalidad: muestra información detallada de una publicación seleccionada en una ventana modal.
- Interacción con el backend:
- El botón «Ver» de cada fila de la tabla rellena el «viewModal» con los datos almacenados en los atributos «data-*» del botón (título, marca de tiempo, autor, lote, puntuaciones de sentimiento, sentimiento, idioma, estado de la traducción, enlace, contenido original/traducido) de la respuesta inicial «loadResults()».
- No se requiere ninguna llamada adicional al backend; el modal muestra campos y áreas de texto de solo lectura. El botón «Cerrar» oculta el modal sin interacción con el backend.
-
Descarga de los resultados seleccionados:
- Finalidad: Exporta los resultados de las publicaciones seleccionadas como un archivo JSON.
- Interacción con el backend:
- El botón «Descargar seleccionados» (
#downloadSelected) recopila los ID de las filas marcadas (filtradas por visibilidad) y envía una solicitud AJAX POST a/api/posts-scanner/{scanId}/download(gestionada porposts_api_router) con la matrizpost_ids. - El backend recupera los registros
MarketplacePostDetailscorrespondientes y los devuelve como JSON. El cliente crea un archivo JSON descargable (scan_{scanId}_results.json) utilizando unBlob. Si no se selecciona ninguna fila o se produce un error, se muestra una alerta.
- El botón «Descargar seleccionados» (
Rutas del backend
El código del backend se encuentra en app/routes/posts.py. A continuación se explican las funciones clave.
get_post_scans:
- Propósito: recupera todos los escaneos de detalles de publicaciones de la base de datos, incluidos detalles como el ID del escaneo, el nombre, el nombre del escaneo de origen, las fechas de inicio y finalización, el estado y el recuento de publicaciones extraídas.
- Características principales:
- Consulta
PostDetailScany se une aMarketplacePostDetailspara contar las publicaciones extraídas. - Devuelve una respuesta JSON con los detalles del escaneo.
- Gestiona los errores con un código de estado 500 si la consulta falla.
- Consulta
get_completed_post_scans:
- Finalidad: Obtiene los nombres de los escaneos
MarketplacePostScancompletados para utilizarlos en un menú desplegable. - Características principales:
- Filtra los escaneos con estado
COMPLETEDy una fecha de finalización que no sea nula. - Devuelve una lista JSON de nombres de escaneos.
- Genera un error 500 si la consulta falla.
- Filtra los escaneos con estado
create_post_scan:
- Finalidad: Crea un nuevo escaneo de detalles de publicación basado en una configuración proporcionada.
- Características principales:
- Valida que el nombre del escaneo sea único y que el escaneo de origen esté completado.
- Crea un registro
PostDetailScancon estadoSTOPPEDy almacena el tamaño del lote y la URL del sitio. - Devuelve una respuesta JSON con el ID del escaneo y un mensaje de éxito.
- Gestiona los nombres de escaneo duplicados (400) o los escaneos de origen que faltan (404).
start_post_scan:
- Propósito: Inicia un escaneo de detalles de publicaciones procesando las publicaciones de un escaneo de origen en lotes utilizando varios bots.
- Características principales:
- Verifica que el escaneo existe, no se está ejecutando y tiene las API necesarias (traducción e IAB) y los bots activos.
- Divide las publicaciones en lotes y las asigna a los bots para su raspado, traducción y clasificación simultáneos.
- Utiliza
scrape_post_details,translate_stringeiab_classifypara procesar las publicaciones. - Guarda los resultados en
MarketplacePostDetailsy actualiza el estado del escaneo aRUNNINGoCOMPLETED/STOPPEDsegún el éxito. - Gestiona los errores con los códigos de estado HTTP adecuados (404, 400, 500).
delete_post_scan:
- Finalidad: Elimina un escaneo de detalles de una publicación específica de la base de datos.
- Características principales:
- Comprueba que el escaneo existe antes de eliminarlo.
- Elimina el registro
PostDetailScany confirma el cambio. - Devuelve un mensaje de éxito en JSON o un error 404 si no se encuentra el escaneo.
- Gestiona los errores inesperados con un código de estado 500.
get_scan_results:
- Finalidad: Recupera los resultados detallados de un escaneo de detalles de una publicación específica.
- Características principales:
- Consulta
MarketplacePostDetailspara un ID de escaneo determinado. - Devuelve una respuesta JSON con detalles como el título, el contenido, el autor, la marca de tiempo, los datos de traducción y las puntuaciones de clasificación.
- Genera un error 500 si la consulta falla.
- Consulta
download_post_results:
- Finalidad: Descarga los detalles de una publicación específica para un ID de escaneo determinado en función de los ID de publicación proporcionados.
- Características principales:
- Verifica que el escaneo existe y que los ID de publicación solicitados son válidos.
- Devuelve una respuesta JSON con los detalles de la publicación seleccionada, incluyendo el título, la marca de tiempo, el autor, las puntuaciones de sentimiento y los datos de traducción.
- Genera un error 404 si no se encuentra el escaneo o las publicaciones, o un error 500 por otros problemas.
Utilizamos la función detect de la biblioteca langdetect dentro de scrape_post_batches. Si lo deseas, puedes modificar la función «translate_string» en «post_scraper.py» para incorporar también «langdetect». Aunque es una opción, prefiero el enfoque actual por su eficiencia.
Pruebas
Para probar esta funcionalidad, configura los siguientes componentes:
- Extrae los detalles de las publicaciones del mercado utilizando la página «/marketplace-scan».
- Configure una API de IA para eludir el CAPTCHA.
- Cree al menos dos perfiles de bot con el propósito establecido en «scrape_post» e inicie sesión para obtener las cookies de sesión.
- Configure la API de DeepL para la traducción.
- Configure la API de IAB para identificar los brokers de acceso inicial.
Para la API de IAB, necesitará el siguiente prompt:
Does this post discuss selling initial access to a company (e.g., RDP, VPN, admin access), selling unrelated items (e.g., accounts, tools), or warnings/complaints? Classify it as:
- Positive Posts: direct sale of unauthorized access to a company, this usually include the target's name.
- Neutral Posts: general offers for tools, exploits or malware without naming a specific target.
- Negative Posts: off-topic or unrelated services such as hosting, spam tools or generic VPS sales.
The content must be specifically about selling access to a company or business whose name is mentioned in the post.
Return **only** a JSON object with:
- `classification`: "Positive", "Neutral", or "Negative".
- `scores`: Probabilities for `positive`, `neutral`, `negative` (summing to 1).
Wrap the JSON in ```json
{
...
}
``` to ensure proper formatting. Do not include any reasoning or extra text.
Post:
```markdown
TARGET-POST-PLACEHOLDER
```
Puedes modificar el mensaje y probarlo por tu cuenta.