En esta sección, daremos los primeros pasos para dominar el web scraping. Lo que más me gusta de esta sección es lo interesante que puede resultar la recopilación de datos, y superar retos como la limitación de velocidad lo hace aún más emocionante.
Estoy seguro de que superarás esta sección con facilidad, especialmente con tus asistentes de IA listos para aclarar cualquier cosa que no explique con suficiente claridad.
Los temas de esta sección incluyen lo siguiente:
- Identificación de idiomas y traducción de datos
- Extracción de datos del foro Clearnet
- Extracción de datos del foro Tornet
Identificación de idiomas y traducción de datos
Nuestros sitios web simulados de ciberdelincuencia contienen publicaciones en varios idiomas, entre ellos chino, ruso y coreano. Por lo tanto, es esencial identificar el idioma de un texto y traducirlo al inglés si no está en ese idioma.
Para empezar con la traducción de datos, puede utilizar el plan API gratuito de DeepL:
https://www.deepl.com/en/pro#developer
Para ampliar y traducir conjuntos de datos más grandes, deberá adquirir uno de sus planes de suscripción.
Acceda a su clave API a través de la página de claves API: https://www.deepl.com/en/your-account/keys
A continuación se muestra un programa en Python que muestra cómo identificar el idioma de cinco publicaciones y traducir las publicaciones que no están en inglés al inglés. En primer lugar, configure un entorno de desarrollo Python:
mkdir deepl_test && cd deepl_test
python3 -m venv venv
source venv/bin/activate
pip install deepl
touch deepl_en.py
Introduzca el siguiente código en «deepl_en.py»:
import deepl, json
# Specify your API key
auth_key = "xxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxx:fx"
translator = deepl.Translator(auth_key)
# Sellers posts
sellers_posts = [
{
"title": "AspenWell 生物科技有限公司 访问",
"description": "公司:AspenWell Biotech Ltd\n地点:荷兰\n收入:$1.3 亿美元\n访问类型:VPN > 内部 RDP > DA\n额外信息:SharePoint 暴露,部署了 RMM 代理\n安全:内部无防病毒软件,未触发日志\n价格:1.05 BTC,支持 escrow 支付(需与卖家协商)\n仅向一名买家出售。",
"price": "1.05 BTC"
},
{
"title": "Silverline Capital Partners Access",
"description": "Company: Silverline Capital Partners (NY Private Equity firm)\nRevenue: approx $500M AUM\nAccess: RDP into domain w/ admin rights\nDetected: not yet, 3 days stable\nBonus: Access to doc mgmt system + Zoom internal creds\nSelling for 0.9 BTC – will include persistence script\nDM to deal",
"price": "0.9 BTC"
},
{
"title": "베리타스 헬스 파트너스 액세스",
"description": "판매 중: Veritas Health Partners, 캐나다\n매출: 약 $430M\n산업: 의료\n접근 유형: VPN + 작동 중인 RDP\n권한: 로컬 관리자 권한 및 내부 공유 액세스\n탐지되지 않음 – 6일 연속 가동 중\n가격: 0.58 BTC 고정 가격",
"price": "0.58 BTC"
},
{
"title": "Portwell Maritime Group Access",
"description": "Selling access to Portwell Maritime Group (Singapore)\nAccess: DA creds via exposed RDP svc acct\nRevenue: Est. $2B\nIndustry: Shipping / Transport\nExtras: payroll exports + crew schedules\nContact: DM if you’ve dealt before. Fast buyers only.",
"price": "DM for price"
},
{
"title": "Доступ к недвижимости Monarch",
"description": "Компания: Monarch Real Estate Investments\nМестонахождение: США, Нью-Йорк\nВыручка: 380 млн долларов\nДоступ: Citrix > внутренний RDP > DA\nНабор постоянных файлов, нетронутый более 4 дней\nДоступны резервные копии документов + договоры аренды клиентов\nЦена: 0,7 BTC, только для серьезных людей",
"price": "0.7 BTC"
}]
results = []
for post in sellers_posts:
# Detect language for title
title_detection = translator.translate_text(
post["title"], target_lang="EN-US", source_lang=None
)
title_lang = title_detection.detected_source_lang
title_translated = title_lang != "EN"
title_text = (
title_detection.text if title_translated else post["title"]
)
# Detect language for description
description_detection = translator.translate_text(
post["description"], target_lang="EN-US", source_lang=None
)
description_lang = description_detection.detected_source_lang
description_translated = description_lang != "EN"
description_text = (
description_detection.text if description_translated else post["description"]
)
# Build result dictionary for the post
result = {
"original_title": {
"text": post["title"],
"language": title_lang,
"translated": title_translated,
"translated_text": title_text if title_translated else None
},
"original_description": {
"text": post["description"],
"language": description_lang,
"translated": description_translated,
"translated_text": description_text if description_translated else None
},
"price": post["price"]
}
results.append(result)
# Print beautified JSON
print(json.dumps(results, indent=2, ensure_ascii=False))
Ejecute ese código y el resultado JSON será el siguiente:
[
{
"original_title": {
"text": "AspenWell 生物科技有限公司 访问",
"language": "ZH",
"translated": true,
"translated_text": "AspenWell Biotechnology Limited Visit"
},
"original_description": {
"text": "公司:AspenWell Biotech Ltd\n地点:荷兰\n收入:$1.3 亿美元\n访问类型:VPN > 内部 RDP > DA\n额外信息:SharePoint 暴露,部署了 RMM 代理\n安全:内部无防病毒软件,未触发日志\n价格:1.05 BTC,支持 escrow 支付(需与卖家协商)\n仅向一名买家出售。",
"language": "ZH",
"translated": true,
"translated_text": "Company: AspenWell Biotech Ltd\nLocation: Netherlands\nRevenue: $130 million\nAccess Type: VPN > Internal RDP > DA\nAdditional Information: SharePoint exposed, RMM proxy deployed\nSecurity: No internal anti-virus software, no logs triggered\nPrice: 1.05 BTC, escrow payments supported (subject to negotiation with seller)\nSold to one buyer only."
},
"price": "1.05 BTC"
},
{
"original_title": {
"text": "Silverline Capital Partners Access",
"language": "EN",
"translated": false,
"translated_text": null
},
"original_description": {
"text": "Company: Silverline Capital Partners (NY Private Equity firm)\nRevenue: approx $500M AUM\nAccess: RDP into domain w/ admin rights\nDetected: not yet, 3 days stable\nBonus: Access to doc mgmt system + Zoom internal creds\nSelling for 0.9 BTC – will include persistence script\nDM to deal",
"language": "EN",
"translated": false,
"translated_text": null
},
"price": "0.9 BTC"
},
{
"original_title": {
"text": "베리타스 헬스 파트너스 액세스",
"language": "KO",
"translated": true,
"translated_text": "Veritas Health Partners Access"
},
"original_description": {
"text": "판매 중: Veritas Health Partners, 캐나다\n매출: 약 $430M\n산업: 의료\n접근 유형: VPN + 작동 중인 RDP\n권한: 로컬 관리자 권한 및 내부 공유 액세스\n탐지되지 않음 – 6일 연속 가동 중\n가격: 0.58 BTC 고정 가격",
"language": "KO",
"translated": true,
"translated_text": "Sold by: Veritas Health Partners, Canada\nRevenue: Approximately $430M\nIndustry: Healthcare\nAccess type: VPN + working RDP\nPermissions: Local administrator privileges and access to internal shares\nUndetected - up and running for 6 days straight\nPrice: 0.58 BTC fixed price"
},
"price": "0.58 BTC"
},
{
"original_title": {
"text": "Portwell Maritime Group Access",
"language": "EN",
"translated": false,
"translated_text": null
},
"original_description": {
"text": "Selling access to Portwell Maritime Group (Singapore)\nAccess: DA creds via exposed RDP svc acct\nRevenue: Est. $2B\nIndustry: Shipping / Transport\nExtras: payroll exports + crew schedules\nContact: DM if you’ve dealt before. Fast buyers only.",
"language": "EN",
"translated": false,
"translated_text": null
},
"price": "DM for price"
},
{
"original_title": {
"text": "Доступ к недвижимости Monarch",
"language": "RU",
"translated": true,
"translated_text": "Access to Monarch real estate"
},
"original_description": {
"text": "Компания: Monarch Real Estate Investments\nМестонахождение: США, Нью-Йорк\nВыручка: 380 млн долларов\nДоступ: Citrix > внутренний RDP > DA\nНабор постоянных файлов, нетронутый более 4 дней\nДоступны резервные копии документов + договоры аренды клиентов\nЦена: 0,7 BTC, только для серьезных людей",
"language": "RU",
"translated": true,
"translated_text": "Company: Monarch Real Estate Investments\nLocation: USA, New York\nRevenue: $380 million\nAccess: Citrix > internal RDP > DA\nSet of permanent files, untouched for more than 4 days\nBackup copies of documents + client leases available\nPrice: 0.7 BTC, for serious people only"
},
"price": "0.7 BTC"
}
]
¿Te has fijado en que conservamos el texto original de las publicaciones en inglés (indicado con «translated»: false)? Esto se debe a que, cuando el idioma detectado es el inglés, no es necesario traducirlo. Las publicaciones que no están en inglés se traducen al inglés.
En módulos posteriores, aprenderás a utilizar el módulo «langdetect» de Python para identificar el idioma de un texto antes de enviarlo a DeepL. Utilizamos «langdetect» para evitar gastar créditos API en detectar o traducir texto en inglés.
Módulo traductor
El ejemplo anterior se centraba en la traducción masiva de datos codificados. Para una experiencia de desarrollo más flexible y modular, proporcionaré un fragmento de código que acepta una clave API y datos como entradas para la traducción. Este enfoque, encapsulado en un módulo «translator.py», es más fácil de integrar y utilizar en cualquier programa:
import deepl, json
def translate_to_en(auth_key: str, data_string: str) -> dict:
"""
Detects the language of a data string and translates it to English if not already in English.
Args:
auth_key (str): DeepL API authentication key.
data_string (str): The input string to process.
Returns:
dict: A dictionary containing:
- text: Original input string
- language: Detected source language
- translated: Boolean indicating if translation was performed
- translated_text: Translated text (if translated) or None (if English)
Raises:
ValueError: If auth_key or data_string is empty or invalid.
deepl.exceptions.AuthorizationException: If the API key is invalid.
deepl.exceptions.DeepLException: For other DeepL API errors.
"""
# Input validation
if not auth_key:
raise ValueError("DeepL API key cannot be empty")
if not data_string or not isinstance(data_string, str):
raise ValueError("Data string must be a non-empty string")
try:
# Initialize DeepL translator
translator = deepl.Translator(auth_key)
# Detect language and translate if necessary
detection = translator.translate_text(
data_string, target_lang="EN-US", source_lang=None
)
detected_lang = detection.detected_source_lang
translated = detected_lang != "EN"
translated_text = detection.text if translated else None
# Build result dictionary
result = {
"text": data_string,
"language": detected_lang,
"translated": translated,
"translated_text": translated_text
}
return result
except deepl.exceptions.AuthorizationException as e:
raise deepl.exceptions.AuthorizationException(
f"Authorization error: Check your DeepL API key. {str(e)}"
)
except deepl.exceptions.DeepLException as e:
raise deepl.exceptions.DeepLException(
f"DeepL API error: {str(e)}"
)
except Exception as e:
raise Exception(f"Unexpected error: {str(e)}")
def format_result(result: dict) -> str:
"""
Formats the translation result as beautified JSON.
Args:
result (dict): The result dictionary from translate_to_en.
Returns:
str: Beautified JSON string.
"""
return json.dumps(result, indent=2, ensure_ascii=False)
A continuación se muestra cómo llamar a «translator.py» desde «translate.py»:
import translator
import os
import json
# Load API key
auth_key = "XXXXXXX-XXXXXXXXXXXXXXXX:fx"
# Sellers posts
sellers_posts = [
{
"title": "AspenWell 生物科技有限公司 访问",
"description": "公司:AspenWell Biotech Ltd\n地点:荷兰\n收入:$1.3 亿美元\n访问类型:VPN > 内部 RDP > DA\n额外信息:SharePoint 暴露,部署了 RMM 代理\n安全:内部无防病毒软件,未触发日志\n价格:1.05 BTC,支持 escrow 支付(需与卖家协商)\n仅向一名买家出售。",
"price": "1.05 BTC"
},
{
"title": "Silverline Capital Partners Access",
"description": "Company: Silverline Capital Partners (NY Private Equity firm)\nRevenue: approx $500M AUM\nAccess: RDP into domain w/ admin rights\nDetected: not yet, 3 days stable\nBonus: Access to doc mgmt system + Zoom internal creds\nSelling for 0.9 BTC – will include persistence script\nDM to deal",
"price": "0.9 BTC"
},
{
"title": "베리타스 헬스 파트너스 액세스",
"description": "판매 중: Veritas Health Partners, 캐나다\n매출: 약 $430M\n산업: 의료\n접근 유형: VPN + 작동 중인 RDP\n권한: 로컬 관리자 권한 및 내부 공유 액세스\n탐지되지 않음 – 6일 연속 가동 중\n가격: 0.58 BTC 고정 가격",
"price": "0.58 BTC"
},
{
"title": "Portwell Maritime Group Access",
"description": "Selling access to Portwell Maritime Group (Singapore)\nAccess: DA creds via exposed RDP svc acct\nRevenue: Est. $2B\nIndustry: Shipping / Transport\nExtras: payroll exports + crew schedules\nContact: DM if you’ve dealt before. Fast buyers only.",
"price": "DM for price"
},
{
"title": "Доступ к недвижимости Monarch",
"description": "Компания: Monarch Real Estate Investments\nМестонахождение: США, Нью-Йорк\nВыручка: 380 млн долларов\nДоступ: Citrix > внутренний RDP > DA\nНабор постоянных файлов, нетронутый более 4 дней\nДоступны резервные копии документов + договоры аренды клиентов\nЦена: 0,7 BTC, только для серьезных людей",
"price": "0.7 BTC"
}]
results = []
for post in sellers_posts:
try:
title_result = translator.translate_to_en(auth_key, post["title"])
description_result = translator.translate_to_en(auth_key, post["description"])
result = {
"original_title": title_result,
"original_description": description_result,
"price": post["price"]
}
results.append(result)
except Exception as e:
print(f"Error processing post '{post['title']}': {e}")
continue
print(json.dumps(results, indent=2, ensure_ascii=False))
En capítulos posteriores, cuando construyas un rastreador web, utilizarás algo como «translator.py», ya que es modular y la API que utiliza se puede cambiar en cualquier momento. Esto facilita su personalización. Es lo que llamamos un diseño modular, algo que se puede modificar o eliminar más adelante.
Extracción de datos del foro Clearnet
Para empezar, voy a mostrar cómo utilizar «Playwright» para extraer el título y la descripción de una publicación y traducirlos si es necesario.
En primer lugar, asegúrate de que el foro Clearnet está en funcionamiento. Inicia sesión y navega hasta una publicación específica, como la número 17 del mercado de vendedores:
http://127.0.0.1:5000/post/marketplace/17
Cuando accede a esta página, el título de la publicación, la descripción, el nombre de usuario y la marca de tiempo son claramente visibles. Sin embargo, si inspecciona el código fuente de la página:
view-source:http://127.0.0.1:5000/post/marketplace/17
No encontrarás estos datos. Esto se debe a que la página importa y utiliza «main.js». Puedes ver el código fuente de este archivo aquí:
[ver fuente:http://127.0.0.1:5000/static/js/main.js](ver fuente:http://127.0.0.1:5000/static/js/main.js)
Dentro de main.js, la función loadPostDetails se encarga de recuperar los datos de las API del backend:
function loadPostDetails() {
if ($('#post-title').length) {
console.log('Loading post details for:', postType, postId);
$.get(`/api/post/${postType}/${postId}`, function(data) {
console.log('API response:', data);
const post = data.post;
const comments = data.comments;
const user = data.user;
// Update post title
$('#post-title').text(post.title || 'Untitled');
// Update post content
let contentHtml = '';
if (postType === 'announcements') {
contentHtml = post.content || '';
} else {
contentHtml = `${post.description || ''}<br><strong>Price:</strong> ${post.price || 'N/A'}`;
}
contentHtml += `<br><br><strong>Posted by:</strong> <a href="/profile/${post.username}" class="text-light">${post.username}</a>`;
contentHtml += `<br><strong>Date:</strong> ${post.date}`;
$('#post-content').html(contentHtml);
// Update category
$('#post-category').text(post.category || 'N/A');
// Update comments
$('#comments-section').empty();
if (comments && comments.length > 0) {
comments.forEach(function(comment) {
$('#comments-section').append(
`<div class="mb-3">
<p class="text-light"><strong>${comment.username}</strong> (${comment.date}): ${comment.content}</p>
</div>`
);
});
} else {
$('#comments-section').html('<p class="text-light">No comments yet.</p>');
}
// Update back link
$('#back-link').attr('href', `/category/${postType}/${post.category}`).text(`Back to ${post.category}`);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error('Failed to load post details:', textStatus, errorThrown);
$('#post-title').text('Error');
$('#post-content').html('<p class="text-light">Failed to load post. Please try again later.</p>');
$('#comments-section').html('<p class="text-light">Failed to load comments.</p>');
});
}
}
Haga clic con el botón derecho del ratón en la descripción de la publicación y seleccione «Inspeccionar elemento» en las herramientas de desarrollo de su navegador para ver claramente el contenido de la publicación. Esto demuestra que es necesario un navegador para extraer los datos. Utilizamos «Playwright» porque ofrece potentes herramientas para automatizar la extracción de datos de páginas web dentro de un entorno de navegador.
¿Cómo funciona la página web?
Antes de sumergirnos en el código, es fundamental comprender cómo funciona la página web y, por extensión, el sitio. Cada dato está contenido en un elemento, como <h1 id="header">. Para extraer datos de este encabezado, podemos seleccionar su ID, «#header».
Para explorar esto, abre cualquier publicación en el sitio clearnet -> haz clic con el botón derecho del ratón en el título -> selecciona «Inspeccionar elemento» -> luego haz clic con el botón derecho del ratón en el código resaltado en el inspector y elige «Editar como HTML».
Copia y pega el código en un editor de notas:
<h2 class="text-light" id="post-title">AspenWell 生物科技有限公司 访问</h2>
Haga clic con el botón derecho del ratón en la descripción -> «inspeccionar elemento» -> haga clic con el botón derecho del ratón en el código resaltado en el inspector -> haga clic en «editar como HTML».
Copie y pegue el código en su editor:
<p class="card-text text-light" id="post-content">公司:AspenWell Biotech Ltd
地点:荷兰
收入:$1.3 亿美元
访问类型:VPN > 内部 RDP > DA
额外信息:SharePoint 暴露,部署了 RMM 代理
安全:内部无防病毒软件,未触发日志
价格:1.05 BTC,支持 escrow 支付(需与卖家协商)
仅向一名买家出售。<br><strong>Price:</strong> 1.05 BTC<br><br><strong>Posted by:</strong> <a href="/profile/AnonX" class="text-light">AnonX</a><br><strong>Date:</strong> 2025-06-30 14:53:51</p>
El nombre de usuario y la fecha también se encuentran aquí. Nos centraremos en estos elementos con «playwright».
Uso de cookies con playwright
Como ya hemos iniciado sesión en el sitio web, podemos ver las publicaciones. Aunque es posible automatizar el inicio de sesión y omitir los CAPTCHA mediante IA, por ahora, un método más sencillo es iniciar sesión manualmente y extraer las cookies de sesión de las herramientas para desarrolladores. Más adelante en el curso aprenderás a automatizar el paso de los CAPTCHA con IA.
Para recuperar las cookies de sesión:
- Pulsa
F12para abrir las Herramientas de desarrollador. - Ve a la pestaña
Almacenamiento. - En el panel izquierdo, selecciona
Cookies. - Haz clic en la dirección del sitio web y copia todas las cookies.
Para inspeccionar cómo se envían las solicitudes al sitio web:
- Pulsa «F12» para abrir las Herramientas de desarrollador.
- Ve a la pestaña «Red».
- Localiza el archivo de la publicación «17» con tipo «html» y haz clic en él.
- En el panel derecho, desplázate hasta la sección «Encabezados de solicitud».
- Haz clic en «Sin formato» y copia todo el contenido.
A continuación se muestra un ejemplo de cómo se estructuran las solicitudes, incluidas las cookies:
GET /post/marketplace/17 HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br, zstd
Referer: http://127.0.0.1:5000/category/marketplace/Sellers?page=1
Connection: keep-alive
Cookie: csrftoken=RxeTY7ifP4ooXCk95Q5ry9ZaFGMOMuZI; _ga_HSTF3XTLDW=GS2.1.s1750188904$o17$g1$t1750189624$j47$l0$h0; _ga_Y0Z4N92WQM=GS2.1.s1751918138$o16$g1$t1751919098$j60$l0$h0; sessionid=qqri9200q5ve9sehy7gp3yeax28bnrhm; session=.eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Priority: u=0, i
Esto puede ser un poco diferente para ti, pero este es un ejemplo. La única cookie que necesitamos es «session».
Extracción de datos con Playwright
Para empezar, configura un entorno e instala las dependencias:
mkdir play && cd play
touch play.py
python3 -m venv venv
source venv/bin/activate
sudo apt install libavif16
pip3 install playwright
playwright install
Abre «play.py» y pega el siguiente código:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
# Essential cookies
cookies = [
{
"name": "session",
"value": ".eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8",
"domain": "127.0.0.1",
"path": "/",
"expires": -1,
"httpOnly": True,
"secure": False,
"sameSite": "Lax"
}
]
# Add cookies to the context
context.add_cookies(cookies)
# Open a page and navigate to the target URL
page = context.new_page()
page.goto("http://127.0.0.1:5000/post/marketplace/17")
print(page.title())
from time import sleep
sleep(400000)
browser.close()
Este código utiliza la cookie «session» para acceder a una página web. Una cookie no válida impedirá el acceso y requerirá un inicio de sesión. La función «sleep(400000)» proporciona tiempo suficiente para interactuar con la sesión del navegador de Playwright.
Ahora que sabemos dónde se almacenan los datos, estamos listos para extraerlos de la página web. A continuación se muestra cómo se pueden extraer los datos:
from playwright.sync_api import sync_playwright
import json
from urllib.parse import urljoin
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
# Essential cookies
cookies = [
{
"name": "session",
"value": ".eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8",
"domain": "127.0.0.1",
"path": "/",
"expires": -1,
"httpOnly": True,
"secure": False,
"sameSite": "Lax"
}
]
# Add cookies to the context
context.add_cookies(cookies)
# Open a page and navigate to the target URL
page = context.new_page()
page.goto("http://127.0.0.1:5000/post/marketplace/17")
# Extract data
title = page.locator('h2#post-title').inner_text()
content = page.locator('p#post-content').inner_text()
username = page.locator('p#post-content a').inner_text()
author_link = page.locator('p#post-content a').get_attribute('href')
# Extract timestamp
timestamp = page.locator('p#post-content').evaluate(
"""el => el.innerHTML.split('<strong>Date:</strong> ')[1].trim()"""
)
post_url = page.url
full_author_link = urljoin(post_url, author_link)
# Construct JSON output
post_data = {
"title": title,
"content": content,
"username": username,
"timestamp": timestamp,
"post_link": post_url,
"author_link": full_author_link
}
print(json.dumps(post_data, indent=2, ensure_ascii=False))
browser.close()
Con ligeros ajustes, podemos importar la función translate_to_en del módulo translator.py y utilizarla. Para que esto funcione, coloque tanto play.py como translator.py en el mismo directorio, cree un entorno virtual Python e instale todas las dependencias requeridas por ambos archivos. A continuación, modifique play.py para integrarlo con translator.py:
from playwright.sync_api import sync_playwright
import json
from urllib.parse import urljoin
from translator import translate_to_en
DEEPL_API_KEY = "XXXXXXXXXXXX-XXXXXXXXXXXXXX:fx"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
# Essential cookies
cookies = [
{
"name": "session",
"value": ".eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8",
"domain": "127.0.0.1",
"path": "/",
"expires": -1,
"httpOnly": True,
"secure": False,
"sameSite": "Lax"
}
]
# Add cookies to the context
context.add_cookies(cookies)
page = context.new_page()
page.goto("http://127.0.0.1:5000/post/marketplace/17")
# Extract data
title = page.locator('h2#post-title').inner_text()
content = page.locator('p#post-content').inner_text()
username = page.locator('p#post-content a').inner_text()
author_link = page.locator('p#post-content a').get_attribute('href')
# Extract timestamp
timestamp = page.locator('p#post-content').evaluate(
"""el => el.innerHTML.split('<strong>Date:</strong> ')[1].trim()"""
)
# Get the current page URL dynamically
post_url = page.url
full_author_link = urljoin(post_url, author_link)
# Translate title and content using DeepL
try:
title_translation = translate_to_en(DEEPL_API_KEY, title)
content_translation = translate_to_en(DEEPL_API_KEY, content)
except Exception as e:
print(f"Translation error: {str(e)}")
title_translation = {
"text": title,
"language": "UNKNOWN",
"translated": False,
"translated_text": None
}
content_translation = {
"text": content,
"language": "UNKNOWN",
"translated": False,
"translated_text": None
}
# Construct JSON output with translation details
post_data = {
"title": {
"original": title_translation["text"],
"language": title_translation["language"],
"translated": title_translation["translated"],
"translated_text": title_translation["translated_text"]
},
"content": {
"original": content_translation["text"],
"language": content_translation["language"],
"translated": content_translation["translated"],
"translated_text": content_translation["translated_text"]
},
"username": username,
"timestamp": timestamp,
"post_link": post_url,
"author_link": full_author_link
}
print(json.dumps(post_data, indent=2, ensure_ascii=False))
browser.close()
Así es como funciona el código:
- Importaciones: Utiliza
playwrightpara la automatización del navegador,jsonpara la salida,urljoinpara el manejo de URL ytranslate_to_enpara las traducciones de DeepL. Define la clave API de DeepL. - Configuración del navegador: Inicia el navegador Chromium sin interfaz gráfica con
sync_playwright, crea el contexto y añade una cookie de sesión para la autenticación. - Navegación por la página: Abre la página en
http://127.0.0.1:5000/post/marketplace/17. - Extracción de datos: Utiliza los localizadores de Playwright para extraer:
- El título de
<h2>con el IDpost-title. - El contenido de
<p>con el IDpost-content. - El nombre de usuario de
<a>enpost-content. - Enlace del autor de
<a>’shref. - Marca de tiempo mediante el análisis del texto después de
<strong>Date:</strong>a través de JavaScript. - URL de la publicación de la página actual.
- Enlace completo del autor utilizando
urljoin.
- El título de
- Traducción: Traduce el título y el contenido al inglés a través de DeepL. En caso de error, devuelve el texto original con el idioma «DESCONOCIDO».
- Salida: Crea un JSON con el título, el contenido (original, idioma, traducido), el nombre de usuario, la marca de tiempo, el enlace de la publicación y el enlace del autor. Imprime con sangría.
- Limpieza: Cierra el navegador.
Aquí está la salida JSON en la terminal:
{
"title": {
"original": "AspenWell 生物科技有限公司 访问",
"language": "ZH",
"translated": true,
"translated_text": "AspenWell Biotechnology Limited Visit"
},
"content": {
"original": "公司:AspenWell Biotech Ltd 地点:荷兰 收入:$1.3 亿美元 访问类型:VPN > 内部 RDP > DA 额外信息:SharePoint 暴露,部署了 RMM 代理 安全:内部无防病毒软件,未触发日志 价格:1.05 BTC,支持 escrow 支付(需与卖家协商) 仅向一名买家出售。\nPrice: 1.05 BTC\n\nPosted by: AnonX\nDate: 2025-06-30 14:53:51",
"language": "ZH",
"translated": true,
"translated_text": "Company: AspenWell Biotech Ltd Location: Netherlands Revenue: $130M Access Type: VPN > Internal RDP > DA Additional Information: SharePoint exposed, RMM proxy deployed Security: No antivirus software on-premise, no logs triggered Price: 1.05 BTC, escrow payment supported (subject to seller's negotiation) Sold to a single Sold to one buyer only.\nPrice: 1.05 BTC\n\nPosted by: AnonX\nDate: 2025-06-30 14:53:51"
},
"username": "AnonX",
"timestamp": "2025-06-30 14:53:51",
"post_link": "http://127.0.0.1:5000/post/marketplace/17",
"author_link": "http://127.0.0.1:5000/profile/AnonX"
}
Como se muestra, tanto el título como la descripción se han traducido. Esto sirve como un ejemplo sencillo de cómo extraer datos utilizando Playwright y realizar traducciones.
Extracción de datos del foro Tornet
En esta sección, mostraré cómo extraer todos los títulos de las publicaciones, los nombres de usuario, las marcas de tiempo y los enlaces de la sección «Buyers» (Compradores) del foro Tornet. Dado que el foro Tor no utiliza JavaScript, la extracción de datos es sencilla.
Para esta tarea, utilizaremos las bibliotecas requests y BeautifulSoup de Python.
Empieza accediendo a la sección Buyers:
http://127.0.0.1:5000/category/marketplace/Buyers
En mi caso, esta página contiene 13 publicaciones, con paginación para navegar al siguiente conjunto de 10 publicaciones o menos. Al hacer clic en «Siguiente», la URL cambia a:
http://127.0.0.1:5000/category/marketplace/Buyers?page=2
Este sistema de paginación es muy práctico: basta con ajustar el número de página para avanzar o retroceder. Sin embargo, si introduces un número de página no válido, como 56, la respuesta mostrará «No se han encontrado entradas». Esto sirve como una señal clara para que tu script se detenga.
Toma nota de esto en tu editor de notas.
Al ver el código fuente de la página 1, puedes ver todas las entradas directamente en el HTML:
<!-- templates/category.html -->
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Buyers - Cyber Forum</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-dark text-light">
<nav class="navbar navbar-dark bg-secondary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Cyber Forum</a>
<ul class="navbar-nav ms-auto d-flex flex-row">
<li class="nav-item me-3"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item me-3"><a class="nav-link" href="/marketplace">Marketplace</a></li>
<li class="nav-item me-3"><a class="nav-link" href="/services">Services</a></li>
<li class="nav-item me-3"><a class="nav-link" href="/search">Search</a></li>
<li class="nav-item me-3"><a class="nav-link" href="/profile/DarkHacker">Profile</a></li>
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
</ul>
</div>
</nav>
<div class="container my-4">
<h2 class="text-light">Buyers</h2>
<div class="card bg-dark border-secondary">
<div class="card-body">
<table class="table table-dark table-hover">
<thead class="table-dark">
<tr>
<th scope="col">Title</th>
<th scope="col">Posted By</th>
<th scope="col">Date</th>
<th scope="col">Comments</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody class="text-light">
<tr>
<td>Seeking CC dumps ASAP</td>
<td><a href="/profile/GhostRider" class="text-light">GhostRider</a></td>
<td>2025-07-06 17:32:21</td>
<td>2</td>
<td><a href="/post/marketplace/7" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Seeking data leaks ASAP</td>
<td><a href="/profile/HackSavvy" class="text-light">HackSavvy</a></td>
<td>2025-07-06 15:05:00</td>
<td>2</td>
<td><a href="/post/marketplace/2" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Buying Fresh PayPal accounts</td>
<td><a href="/profile/N3tRunn3r" class="text-light">N3tRunn3r</a></td>
<td>2025-07-06 03:29:42</td>
<td>2</td>
<td><a href="/post/marketplace/12" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Need gift card codes, High Budget</td>
<td><a href="/profile/ShadowV" class="text-light">ShadowV</a></td>
<td>2025-07-04 00:58:36</td>
<td>2</td>
<td><a href="/post/marketplace/4" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Looking for CC dumps</td>
<td><a href="/profile/Crypt0King" class="text-light">Crypt0King</a></td>
<td>2025-07-03 07:51:05</td>
<td>2</td>
<td><a href="/post/marketplace/11" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Looking for PayPal accounts</td>
<td><a href="/profile/DarkHacker" class="text-light">DarkHacker</a></td>
<td>2025-07-02 03:44:01</td>
<td>2</td>
<td><a href="/post/marketplace/8" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Need CC dumps, High Budget</td>
<td><a href="/profile/GhostRider" class="text-light">GhostRider</a></td>
<td>2025-06-30 16:23:41</td>
<td>2</td>
<td><a href="/post/marketplace/1" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Looking for data leaks</td>
<td><a href="/profile/CyberGhost" class="text-light">CyberGhost</a></td>
<td>2025-06-26 23:42:20</td>
<td>2</td>
<td><a href="/post/marketplace/5" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Seeking RDP credentials ASAP</td>
<td><a href="/profile/N3tRunn3r" class="text-light">N3tRunn3r</a></td>
<td>2025-06-26 18:50:33</td>
<td>2</td>
<td><a href="/post/marketplace/13" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
<tr>
<td>Seeking VPN logins ASAP</td>
<td><a href="/profile/GhostRider" class="text-light">GhostRider</a></td>
<td>2025-06-21 22:55:49</td>
<td>2</td>
<td><a href="/post/marketplace/3" class="btn btn-outline-secondary btn-sm">View</a></td>
</tr>
</tbody>
</table>
<nav aria-label="Category pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#">Previous</a>
</li>
<li class="page-item"><span class="page-link">Page 1 of 2</span></li>
<li class="page-item ">
<a class="page-link" href="/category/marketplace/Buyers?page=2">Next</a>
</li>
</ul>
</nav>
</div>
</div>
<a href="/" class="btn btn-outline-secondary mt-3">Back to Home</a>
</div>
</body>
</html>
El objetivo principal es comprender la estructura de la tabla. Debes analizar programáticamente el código fuente HTML utilizando BeautifulSoup y extraer los datos de la tabla.
Codificación del rastreador
Para empezar, configura un entorno virtual Python e instala las dependencias.
mkdir clearnet_scraper && cd clearnet_scraper
python3 -m venv venv
source venv/bin/activate
pip install requests beautifulsoup4
touch tornet_scrape.py
Abre «tornet_scrape.py» y pega el siguiente código:
import requests
from bs4 import BeautifulSoup
import json
url = "http://127.0.0.1:5000/category/marketplace/Buyers?page=1"
cookies = {"session": ".eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8"}
response = requests.get(url, cookies=cookies)
if response.status_code != 200:
print(f"Failed to retrieve page: {response.status_code}")
exit()
soup = BeautifulSoup(response.text, "html.parser")
posts = []
tbody = soup.find("tbody", class_="text-light")
base_url = "http://127.0.0.1:5000"
for row in tbody.find_all("tr"):
title = row.find_all("td")[0].text.strip()
author_cell = row.find_all("td")[1]
author = author_cell.text.strip()
author_link = base_url + author_cell.find("a")["href"]
timestamp = row.find_all("td")[2].text.strip()
post_link = base_url + row.find_all("td")[4].find("a")["href"]
posts.append({
"title": title,
"post_link": post_link,
"post_author": author,
"author_link": author_link,
"timestamp": timestamp
})
json_output = json.dumps(posts, indent=4)
print(json_output)
El script obtiene una página web de un servidor local utilizando requests con una cookie de sesión para la autenticación. Analiza el HTML con BeautifulSoup, buscando una tabla (<tbody> con la clase text-light). Para cada fila de la tabla, extrae el título de la publicación, el autor, el enlace del autor, la marca de tiempo y el enlace de la publicación, y los almacena en una lista de diccionarios. Por último, convierte los datos a formato JSON con json.dumps y los imprime.
Extraer todos los datos
Has aprendido a extraer datos de una sola página, pero ahora vas a aprender a pasar a la página siguiente aumentando el número de página en 1.
Estos son los cambios que debes realizar en tu código:
import requests
from bs4 import BeautifulSoup
import json
url_base = "http://127.0.0.1:5000/category/marketplace/Buyers?page={}"
cookies = {"session": ".eJwlzjkOwjAQAMC_uKbwru098hmUvQRtQirE30FiXjDvdq8jz0fbXseVt3Z_RtsaLq_dS6iPEovRuyIyhhGoxMppFG5CJEAEy3cuZ9LivWN0nWwyLCZS6tI5h66oWdA9hBADWN14xyrJ6LkmFFj5yjKsMVjbL3Kdefw30D5fvlgvjw.aG6mEQ.B_zqhhmM1qXJrt8glWcY3eIzNQ8"}
posts = []
page = 1
base_url = "http://127.0.0.1:5000"
while True:
url = url_base.format(page)
response = requests.get(url, cookies=cookies)
if response.status_code != 200:
print(f"Failed to retrieve page {page}: {response.status_code}")
break
soup = BeautifulSoup(response.text, "html.parser")
if "No posts found." in soup.text:
break
tbody = soup.find("tbody", class_="text-light")
if not tbody:
break
for row in tbody.find_all("tr"):
title = row.find_all("td")[0].text.strip()
author_cell = row.find_all("td")[1]
author = author_cell.text.strip()
author_link = base_url + author_cell.find("a")["href"]
timestamp = row.find_all("td")[2].text.strip()
post_link = base_url + row.find_all("td")[4].find("a")["href"]
posts.append({
"title": title,
"post_link": post_link,
"post_author": author,
"author_link": author_link,
"timestamp": timestamp
})
page += 1
json_output = json.dumps(posts, indent=4)
print(json_output)
Ahora debería aparecer un total de 13 entradas (el número puede variar en tu caso):
[
{
"title": "Seeking CC dumps ASAP",
"post_link": "http://127.0.0.1:5000/post/marketplace/7",
"post_author": "GhostRider",
"author_link": "http://127.0.0.1:5000/profile/GhostRider",
"timestamp": "2025-07-06 17:32:21"
},
{
"title": "Seeking data leaks ASAP",
"post_link": "http://127.0.0.1:5000/post/marketplace/2",
"post_author": "HackSavvy",
"author_link": "http://127.0.0.1:5000/profile/HackSavvy",
"timestamp": "2025-07-06 15:05:00"
},
{
"title": "Buying Fresh PayPal accounts",
"post_link": "http://127.0.0.1:5000/post/marketplace/12",
"post_author": "N3tRunn3r",
"author_link": "http://127.0.0.1:5000/profile/N3tRunn3r",
"timestamp": "2025-07-06 03:29:42"
},
{
"title": "Need gift card codes, High Budget",
"post_link": "http://127.0.0.1:5000/post/marketplace/4",
"post_author": "ShadowV",
"author_link": "http://127.0.0.1:5000/profile/ShadowV",
"timestamp": "2025-07-04 00:58:36"
},
{
"title": "Looking for CC dumps",
"post_link": "http://127.0.0.1:5000/post/marketplace/11",
"post_author": "Crypt0King",
"author_link": "http://127.0.0.1:5000/profile/Crypt0King",
"timestamp": "2025-07-03 07:51:05"
},
{
"title": "Looking for PayPal accounts",
"post_link": "http://127.0.0.1:5000/post/marketplace/8",
"post_author": "DarkHacker",
"author_link": "http://127.0.0.1:5000/profile/DarkHacker",
"timestamp": "2025-07-02 03:44:01"
},
{
"title": "Need CC dumps, High Budget",
"post_link": "http://127.0.0.1:5000/post/marketplace/1",
"post_author": "GhostRider",
"author_link": "http://127.0.0.1:5000/profile/GhostRider",
"timestamp": "2025-06-30 16:23:41"
},
{
"title": "Looking for data leaks",
"post_link": "http://127.0.0.1:5000/post/marketplace/5",
"post_author": "CyberGhost",
"author_link": "http://127.0.0.1:5000/profile/CyberGhost",
"timestamp": "2025-06-26 23:42:20"
},
{
"title": "Seeking RDP credentials ASAP",
"post_link": "http://127.0.0.1:5000/post/marketplace/13",
"post_author": "N3tRunn3r",
"author_link": "http://127.0.0.1:5000/profile/N3tRunn3r",
"timestamp": "2025-06-26 18:50:33"
},
{
"title": "Seeking VPN logins ASAP",
"post_link": "http://127.0.0.1:5000/post/marketplace/3",
"post_author": "GhostRider",
"author_link": "http://127.0.0.1:5000/profile/GhostRider",
"timestamp": "2025-06-21 22:55:49"
},
{
"title": "Seeking RDP credentials ASAP",
"post_link": "http://127.0.0.1:5000/post/marketplace/10",
"post_author": "AnonX",
"author_link": "http://127.0.0.1:5000/profile/AnonX",
"timestamp": "2025-06-20 11:57:41"
},
{
"title": "Buying Fresh RDP credentials",
"post_link": "http://127.0.0.1:5000/post/marketplace/9",
"post_author": "DarkHacker",
"author_link": "http://127.0.0.1:5000/profile/DarkHacker",
"timestamp": "2025-06-20 06:34:27"
},
{
"title": "Need VPN logins, High Budget",
"post_link": "http://127.0.0.1:5000/post/marketplace/6",
"post_author": "Crypt0King",
"author_link": "http://127.0.0.1:5000/profile/Crypt0King",
"timestamp": "2025-06-13 07:52:15"
}
]
Prueba esto en cualquier otra página y comprueba qué resultado obtienes. ¿Funciona o es necesario modificarlo para que se dirija correctamente a los datos?