Nesta secção, daremos os primeiros passos para dominar a extração de dados da web. O que mais gosto nesta secção é como a recolha de dados pode ser envolvente, e superar desafios como a limitação de taxa torna tudo ainda mais emocionante.
Estou confiante de que irá passar por esta secção com facilidade, especialmente com os seus assistentes de IA prontos para esclarecer qualquer coisa que eu possa não explicar claramente.
Os tópicos desta secção incluem o seguinte:
- Identificação de idioma e tradução de dados
- Extração de dados do fórum Clearnet
- Extração de dados do fórum Tornet
Identificação de idioma e tradução de dados
Os nossos sites simulados de crimes cibernéticos apresentam publicações em vários idiomas, incluindo chinês, russo e coreano. Portanto, é essencial identificar o idioma de um texto e traduzi-lo para o inglês, caso ainda não esteja nesse idioma.
Para começar a traduzir os dados, pode usar o plano API gratuito do DeepL:
https://www.deepl.com/en/pro#developer
Para ampliar e traduzir conjuntos de dados maiores, será necessário adquirir um dos planos de assinatura.
Aceda à sua chave API através da página de chaves API:
https://www.deepl.com/en/your-account/keys
Abaixo está um programa Python que demonstra como identificar o idioma de cinco publicações e traduzir as publicações que não estão em inglês para inglês. Primeiro, configure um ambiente de desenvolvimento Python:
mkdir deepl_test && cd deepl_test
python3 -m venv venv
source venv/bin/activate
pip install deepl
touch deepl_en.py
Insira o seguinte código em 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))
Execute esse código e a saída JSON será a seguinte:
[
{
"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"
}
]
Repare que preservamos o texto original das publicações em inglês (indicado por «translated»: false). Isso ocorre porque, quando o idioma detectado é o inglês, a tradução é desnecessária. As publicações que não estão em inglês são traduzidas para o inglês.
Nos módulos posteriores, aprenderá a utilizar o módulo langdetect do Python para identificar o idioma de um texto antes de enviá-lo para o DeepL. Utilizamos o langdetect para evitar desperdiçar créditos da API na deteção ou tradução de texto em inglês.
Módulo tradutor
O exemplo anterior focou na tradução em massa de dados codificados. Para uma experiência de desenvolvimento mais flexível e modular, fornecerei um trecho de código que aceita uma chave API e dados como entradas para tradução. Essa abordagem, encapsulada em um módulo translator.py, é mais fácil de integrar e usar em qualquer 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)
Veja como chamar «translator.py» a partir de «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))
Nos capítulos posteriores, ao criar um web scraper, utilizará algo como «translator.py», pois é modular e a API que utiliza pode ser alterada a qualquer momento. Isso facilita a personalização. É o que chamamos de design modular, algo que pode ser modificado ou excluído posteriormente.
Extração de dados do fórum Clearnet
Para começar, demonstrarei como usar o Playwright para extrair o título e a descrição de uma publicação e traduzi-los, se necessário.
Primeiro, certifique-se de que o fórum Clearnet está a funcionar. Inicie sessão e navegue até uma publicação específica, como a publicação número 17 no mercado de vendedores:
http://127.0.0.1:5000/post/marketplace/17
Ao aceder a esta página, o título da publicação, a descrição, o nome de utilizador e o carimbo de data/hora ficam claramente visíveis. No entanto, se inspecionar o código-fonte da página:
view-source:http://127.0.0.1:5000/post/marketplace/17
Não encontrará esses dados. Isso ocorre porque a página importa e usa main.js. Pode visualizar o código-fonte deste ficheiro aqui:
view-source:http://127.0.0.1:5000/static/js/main.js
Dentro de main.js, a função loadPostDetails é responsável por buscar dados das APIs de back-end:
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>');
});
}
}
Clique com o botão direito do rato na descrição da publicação e selecione «Inspecionar elemento» nas Ferramentas de programador do seu navegador para visualizar o conteúdo da publicação com clareza. Isto mostra que é necessário um navegador para extrair os dados. Utilizamos o «Playwright» porque oferece ferramentas poderosas para automatizar a extração de dados de páginas web num ambiente de navegador.
Como funciona a página da web?
Antes de mergulhar no código, é fundamental entender como funciona a página da web e, por extensão, o site. Cada dado está contido em um elemento, como <h1 id="header">. Para extrair dados desse cabeçalho, podemos direcionar seu ID, #header.
Para explorar isso, abra qualquer postagem no site clearnet -> clique com o botão direito do mouse no título -> selecione Inspect Element -> clique com o botão direito do mouse no código destacado no inspetor e escolha Edit as HTML.
Copie e cole o código em um editor de notas:
<h2 class="text-light" id="post-title">AspenWell 生物科技有限公司 访问</h2>
Clique com o botão direito do rato na descrição -> «inspecionar elemento» -> clique com o botão direito do rato no código destacado no inspetor -> clique em «Editar como HTML»
Copie e cole o código no seu 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>
O nome de utilizador e a data também estão aqui. Vamos direcionar esses elementos com playwright.
Usando cookies com playwright
Como já estamos conectados ao site, podemos ver as publicações. Embora seja possível automatizar o login e ignorar CAPTCHAs usando IA, por enquanto, uma abordagem mais simples é fazer o login manualmente e extrair os cookies da sessão das Ferramentas do Programador. Você aprenderá como automatizar a ignorar CAPTCHAs com IA mais adiante no curso.
Para recuperar os cookies da sua sessão:
- Pressione
F12para abrir as Ferramentas do Desenvolvedor. - Navegue até a guia
Armazenamento. - No painel esquerdo, selecione
Cookies. - Clique no endereço do site e copie todos os cookies.
Para inspecionar como as solicitações são enviadas ao site:
- Pressione
F12para abrir as Ferramentas do Programador. - Vá para o separador
Rede. - Localize o ficheiro para a publicação
17com o tipohtmle clique nele. - No painel direito, role até à secção
Cabeçalhos da solicitação. - Clique em
Rawe copie todo o conteúdo.
Abaixo está um exemplo de como as solicitações, incluindo cookies, são estruturadas:
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
Isso pode ser um pouco diferente para si e este é um exemplo. O único cookie de que precisamos é session.
Extração de dados com o Playwright
Para começar, configure um ambiente e instale as dependências:
mkdir play && cd play
touch play.py
python3 -m venv venv
source venv/bin/activate
sudo apt install libavif16
pip3 install playwright
playwright install
Abra play.py e cole o seguinte 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 usa o cookie session para aceder a uma página web. Um cookie inválido impedirá o acesso, exigindo um login. A função sleep(400000) fornece tempo suficiente para interagir com a sessão do navegador do Playwright.
Com uma compreensão de onde os dados são armazenados, estamos agora prontos para extraí-los da página web. Abaixo está como os dados podem ser extraídos:
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()
Com pequenos ajustes, podemos importar a função translate_to_en do módulo translator.py e utilizá-la. Para que isso funcione, coloque os ficheiros play.py e translator.py no mesmo diretório, crie um ambiente virtual Python e instale todas as dependências exigidas pelos dois ficheiros. Em seguida, modifique o ficheiro play.py para integrar com o ficheiro 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()
Veja como o código funciona:
- Importações: usa
playwrightpara automação do navegador,jsonpara saída,urljoinpara tratamento de URLs etranslate_to_enpara traduções do DeepL. Define a chave API do DeepL. - Configuração do navegador: Inicia o navegador Chromium sem interface gráfica com
sync_playwright, cria o contexto e adiciona um cookie de sessão para autenticação. - Navegação na página: Abre a página em
http://127.0.0.1:5000/post/marketplace/17. - Extração de dados: Utiliza localizadores Playwright para extrair:
- Título de
<h2>com IDpost-title. - Conteúdo de
<p>com IDpost-content. - Nome de utilizador de
<a>empost-content. - Link do autor de
<a>’shref. - Carimbo de data/hora analisando o texto após
<strong>Date:</strong>via JavaScript. - URL da publicação da página atual.
- Link completo do autor usando
urljoin.
- Título de
- Tradução: Traduz o título e o conteúdo para inglês via DeepL. Em caso de erro, retorna o texto original com o idioma “DESCONHECIDO”.
- Saída: Cria JSON com título, conteúdo (original, idioma, traduzido), nome de utilizador, carimbo de data/hora, link da publicação e link do autor. Imprime com indentação.
- Limpeza: Fecha o navegador.
Aqui está a saída JSON no 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"
}
Conforme mostrado, tanto o título quanto a descrição foram traduzidos. Este é um exemplo simples de como extrair dados usando o Playwright e realizar traduções.
Extração de dados do fórum Tornet
Nesta seção, demonstrarei como extrair todos os títulos das publicações, nomes de utilizadores, carimbos de data/hora e links da seção Buyers (Compradores) do fórum Tornet. Como o fórum Tor não usa JavaScript, a extração de dados é simples.
Para esta tarefa, utilizaremos as bibliotecas requests e BeautifulSoup do Python.
Comece por aceder à secção Buyers:
http://127.0.0.1:5000/category/marketplace/Buyers
Para mim, esta página contém 13 publicações, com paginação para navegar para o próximo conjunto de 10 ou menos publicações. Clicar em «Next» altera o URL para:
http://127.0.0.1:5000/category/marketplace/Buyers?page=2
Este sistema de paginação é conveniente — basta ajustar o número da página para navegar para a frente ou para trás. No entanto, se introduzir um número de página inválido, como 56, a resposta exibirá «Nenhuma publicação encontrada». Isto serve como um sinal claro para o seu script parar.
Anote isto no seu editor de notas.
Ao visualizar o código-fonte da página 1, pode ver todas as publicações diretamente no 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>
O foco principal é compreender a estrutura da tabela. É necessário analisar programaticamente o código-fonte HTML usando BeautifulSoup e extrair os dados da tabela.
Codificação do scraper
Para começar, configure um ambiente virtual Python e instale as dependências.
mkdir clearnet_scraper && cd clearnet_scraper
python3 -m venv venv
source venv/bin/activate
pip install requests beautifulsoup4
touch tornet_scrape.py
Abra tornet_scrape.py e cole o seguinte 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)
O script obtém uma página da Web de um servidor local usando requests com um cookie de sessão para autenticação. Ele analisa o HTML com BeautifulSoup, visando uma tabela (<tbody> com classe text-light). Para cada linha da tabela, ele extrai o título da publicação, o autor, o link do autor, o carimbo de data/hora e o link da publicação, armazenando-os em uma lista de dicionários. Por fim, converte os dados para o formato JSON com json.dumps e imprime-os.
Recolher todos os dados
Aprendeu a recolher dados de apenas uma página, mas agora vai aprender como passar para a página seguinte aumentando o número da página em 1.
Aqui estão as alterações que precisa de fazer no seu 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)
A saída deve agora mostrar 13 publicações no total (o número pode ser diferente para si):
[
{
"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"
}
]
Experimente isto em qualquer outra página e veja o que acontece. Funciona ou precisa de modificar para direcionar corretamente os dados?