In diesem Abschnitt machen wir die ersten Schritte zum Erlernen des Web-Scrapings. Was mir an diesem Abschnitt am besten gefällt, ist, wie spannend die Datenerfassung sein kann, und dass die Bewältigung von Herausforderungen wie Ratenbegrenzungen das Ganze noch spannender macht.
Ich bin zuversichtlich, dass Sie diesen Abschnitt mit Leichtigkeit meistern werden, zumal Ihnen meine KI-Assistenten zur Seite stehen, um alles zu erklären, was ich vielleicht nicht klar genug formuliert habe.
Die Themen dieses Abschnitts umfassen Folgendes:
- Spracherkennung und Datenübersetzung
- Clearnet-Forum-Datenextraktion
- Tornet-Forum-Datenextraktion
Spracherkennung und Datenübersetzung
Unsere simulierten Cyberkriminalitäts-Websites enthalten Beiträge in verschiedenen Sprachen, darunter Chinesisch, Russisch und Koreanisch. Daher ist es wichtig, die Sprache eines Textes zu identifizieren und ihn ins Englische zu übersetzen, wenn er nicht bereits auf Englisch verfasst ist.
Um mit der Datenübersetzung zu beginnen, können Sie den kostenlosen API-Tarif von DeepL nutzen:
https://www.deepl.com/en/pro#developer
Für die Skalierung und Übersetzung größerer Datensätze müssen Sie eines der Abonnementmodelle erwerben.
Sie können Ihren API-Schlüssel über die Seite „API-Schlüssel” abrufen:
https://www.deepl.com/en/your-account/keys
Im Folgenden finden Sie ein Python-Programm, das zeigt, wie Sie die Sprache von fünf Beiträgen identifizieren und nicht-englische Beiträge ins Englische übersetzen können. Richten Sie zunächst eine Python-Entwicklungsumgebung ein:
mkdir deepl_test && cd deepl_test
python3 -m venv venv
source venv/bin/activate
pip install deepl
touch deepl_en.py
Geben Sie den folgenden Code in deepl_en.py ein:
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))
Führen Sie diesen Code aus, und die JSON-Ausgabe sieht wie folgt aus:
[
{
"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"
}
]
Beachten Sie, dass wir den Originaltext für englische Beiträge beibehalten (gekennzeichnet durch „translated”: false). Der Grund dafür ist, dass eine Übersetzung nicht erforderlich ist, wenn die erkannte Sprache Englisch ist. Nicht-englische Beiträge werden ins Englische übersetzt.
In späteren Modulen erfahren Sie, wie Sie mit dem Python-Modul „langdetect“ die Sprache eines Textes identifizieren können, bevor Sie ihn an DeepL senden. Wir verwenden „langdetect“, um API-Credits für die Erkennung oder Übersetzung von englischem Text zu sparen.
Übersetzungsmodul
Das vorherige Beispiel konzentrierte sich auf die Massenübersetzung von fest codierten Daten. Für eine flexiblere und modularere Entwicklung stelle ich einen Codeausschnitt zur Verfügung, der einen API-Schlüssel und Daten als Eingaben für die Übersetzung akzeptiert. Dieser Ansatz, der in einem Modul „translator.py“ zusammengefasst ist, lässt sich leichter in jedes Programm integrieren und verwenden:
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)
So rufen Sie „translator.py” aus „translate.py” auf:
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))
In späteren Kapiteln, wenn Sie einen Web-Scraper erstellen, würden Sie etwas wie translator.py verwenden, da es modular aufgebaut ist und die verwendete API jederzeit geändert werden kann. Dies erleichtert die Anpassung. Dies bezeichnen wir als modulares Design, das später geändert oder gelöscht werden kann.
Clearnet-Forum-Datenextraktion
Zunächst zeige ich Ihnen, wie Sie mit Playwright den Titel und die Beschreibung eines Beitrags extrahieren und bei Bedarf übersetzen können.
Stellen Sie zunächst sicher, dass das Clearnet-Forum läuft. Melden Sie sich an und navigieren Sie zu einem bestimmten Beitrag, z. B. Beitrag Nummer 17 im Verkäufermarktplatz:
http://127.0.0.1:5000/post/marketplace/17
Wenn Sie diese Seite aufrufen, sind der Titel des Beitrags, die Beschreibung, der Benutzername und der Zeitstempel deutlich sichtbar. Wenn Sie jedoch den Quellcode der Seite überprüfen:
view-source:http://127.0.0.1:5000/post/marketplace/17
finden Sie diese Daten nicht. Das liegt daran, dass die Seite „main.js“ importiert und verwendet. Den Quellcode dieser Datei können Sie hier einsehen:
view-source:http://127.0.0.1:5000/static/js/main.js
Innerhalb von „main.js“ ist die Funktion „loadPostDetails“ für das Abrufen von Daten aus Backend-APIs zuständig:
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>');
});
}
}
Klicken Sie mit der rechten Maustaste auf die Post-Beschreibung und wählen Sie in den Entwicklertools Ihres Browsers „Element untersuchen“, um den Inhalt des Posts klar anzuzeigen. Dies zeigt, dass ein Browser erforderlich ist, um die Daten zu extrahieren. Wir verwenden „Playwright“, da es leistungsstarke Tools zur Automatisierung der Datenextraktion aus Webseiten in einer Browserumgebung bietet.
Wie funktioniert die Webseite?
Bevor wir uns mit dem Code befassen, ist es wichtig zu verstehen, wie die Webseite und damit auch die Website funktioniert. Jede Datenangabe ist in einem Element enthalten, z. B. „
“. Um Daten aus dieser Kopfzeile zu extrahieren, können wir ihre ID „#header“ als Ziel verwenden.
Um dies zu untersuchen, öffnen Sie einen beliebigen Beitrag auf der Clearnet-Website -> klicken Sie mit der rechten Maustaste auf den Titel -> wählen Sie „Element untersuchen“ -> klicken Sie dann mit der rechten Maustaste auf den markierten Code im Inspektor und wählen Sie „Als HTML bearbeiten“.
Kopieren Sie den Code und fügen Sie ihn in einen Notizeditor ein:
<h2 class="text-light" id="post-title">AspenWell 生物科技有限公司 访问</h2>
Klicken Sie mit der rechten Maustaste auf die Beschreibung -> „Element untersuchen“ -> klicken Sie mit der rechten Maustaste auf den markierten Code im Inspektor -> klicken Sie auf „Als HTML bearbeiten“
Kopieren Sie den Code und fügen Sie ihn in Ihren Editor ein:
<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>
Der Benutzername und das Datum sind ebenfalls hier zu finden. Wir werden diese Elemente mit „playwright“ anvisieren.
Verwendung von Cookies mit Playwright
Da wir bereits auf der Website angemeldet sind, können wir Beiträge anzeigen. Es ist zwar möglich, die Anmeldung und das Umgehen von CAPTCHAs mithilfe von KI zu automatisieren, aber vorerst ist es einfacher, sich manuell anzumelden und Ihre Sitzungscookies aus den Entwicklertools zu extrahieren. Wie Sie das Umgehen von CAPTCHAs mit KI automatisieren können, erfahren Sie später in diesem Kurs.
So rufen Sie Ihre Sitzungscookies ab:
- Drücken Sie „F12“, um die Entwicklertools zu öffnen.
- Navigieren Sie zur Registerkarte „Speicher“.
- Wählen Sie im linken Bereich „Cookies“ aus.
- Klicken Sie auf die Website-Adresse und kopieren Sie alle Cookies.
So überprüfen Sie, wie Anfragen an die Website gesendet werden:
- Drücken Sie „F12“, um die Entwicklertools zu öffnen.
- Wechseln Sie zur Registerkarte „Netzwerk“.
- Suchen Sie die Datei für den Beitrag „17“ mit dem Typ „html“ und klicken Sie darauf.
- Scrollen Sie im rechten Bereich zum Abschnitt „Anforderungsheader“.
- Klicken Sie auf „Roh“ und kopieren Sie den gesamten Inhalt.
Nachfolgend finden Sie ein Beispiel für die Struktur von Anfragen einschließlich 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
Dies kann bei Ihnen etwas anders aussehen, dies ist nur ein Beispiel. Der einzige Cookie, den wir benötigen, ist „session“.
Datenextraktion mit Playwright
Richten Sie zunächst eine Umgebung ein und installieren Sie die Abhängigkeiten:
mkdir play && cd play
touch play.py
python3 -m venv venv
source venv/bin/activate
sudo apt install libavif16
pip3 install playwright
playwright install
Öffnen Sie play.py und fügen Sie den folgenden Code ein:
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()
Dieser Code verwendet das session-Cookie, um auf eine Webseite zuzugreifen. Ein ungültiges Cookie verhindert den Zugriff und erfordert eine Anmeldung. Die Funktion sleep(400000) bietet ausreichend Zeit für die Interaktion mit der Browser-Sitzung von Playwright.
Nachdem wir nun wissen, wo die Daten gespeichert sind, können wir sie aus der Webseite extrahieren. Im Folgenden wird gezeigt, wie Daten extrahiert werden können:
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()
Mit geringfügigen Anpassungen können wir die Funktion „translate_to_en” aus dem Modul „translator.py” importieren und verwenden. Damit dies funktioniert, müssen Sie sowohl „play.py” als auch „translator.py” im selben Verzeichnis ablegen, eine virtuelle Python-Umgebung erstellen und alle von beiden Dateien benötigten Abhängigkeiten installieren. Anschließend müssen Sie „play.py” ändern, um „translator.py” zu integrieren:
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()
So funktioniert der Code:
- Importe: Verwendet
playwrightfür die Browser-Automatisierung,jsonfür die Ausgabe,urljoinfür die URL-Verarbeitung undtranslate_to_enfür DeepL-Übersetzungen. Definiert den DeepL-API-Schlüssel. - Browser-Einrichtung: Startet den headless Chromium-Browser mit
sync_playwright, erstellt einen Kontext und fügt ein Sitzungscookie für die Authentifizierung hinzu. - Seitennavigation: Öffnet die Seite unter
http://127.0.0.1:5000/post/marketplace/17. - Datenextraktion: Verwendet Playwright-Lokalisierer, um Folgendes zu extrahieren:
- Titel aus
<h2>mit der IDpost-title. - Inhalt aus
<p>mit der IDpost-content. - Benutzername aus
<a>inpost-content. - Autorenlink aus
<a>shref. - Zeitstempel durch Parsen des Textes nach
<strong>Date:</strong>über JavaScript. - Post-URL der aktuellen Seite.
- Vollständiger Autorenlink mit
urljoin.
- Titel aus
- Übersetzung: Übersetzt Titel und Inhalt über DeepL ins Englische. Bei einem Fehler wird der Originaltext mit der Sprache „UNKNOWN” zurückgegeben.
- Ausgabe: Erstellt JSON mit Titel, Inhalt (Original, Sprache, Übersetzung), Benutzername, Zeitstempel, Post-Link und Autoren-Link. Druckt mit Einrückung.
- Bereinigung: Schließt den Browser.
Hier ist die JSON-Ausgabe auf dem 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"
}
Wie zu sehen ist, wurden sowohl der Titel als auch die Beschreibung übersetzt. Dies ist ein einfaches Beispiel dafür, wie man mit Playwright Daten extrahiert und Übersetzungen durchführt.
Datenextraktion aus dem Tornet-Forum
In diesem Abschnitt zeige ich, wie man alle Beitragstitel, Benutzernamen, Zeitstempel und Links aus dem Bereich „Buyers“ des Tornet-Forums extrahiert. Da das Tor-Forum kein JavaScript verwendet, ist die Datenextraktion unkompliziert.
Für diese Aufgabe verwenden wir die Python-Bibliotheken „requests“ und „BeautifulSoup“.
Rufen Sie zunächst den Bereich „Buyers“ auf: http://127.0.0.1:5000/category/marketplace/Buyers
Bei mir enthält diese Seite 13 Beiträge mit einer Paginierung, um zu den nächsten 10 oder weniger Beiträgen zu navigieren. Clicking „Next“ changes the URL to:
http://127.0.0.1:5000/category/marketplace/Buyers?page=2
Dieses Paginierungssystem ist praktisch – passen Sie einfach die Seitenzahl an, um vorwärts oder rückwärts zu navigieren. Wenn Sie jedoch eine ungültige Seitenzahl eingeben, z. B. 56, wird die Antwort „Keine Beiträge gefunden“ angezeigt. Dies ist ein klares Signal für Ihr Skript, anzuhalten.
Notieren Sie sich dies in Ihrem Notizeditor.
Wenn Sie den Quellcode von Seite 1 anzeigen, können Sie alle Beiträge direkt im HTML-Code sehen:
<!-- 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>
Der Schwerpunkt liegt auf dem Verständnis der Tabellenstruktur. Sie müssen den HTML-Quellcode mit BeautifulSoup programmgesteuert analysieren und die Daten aus der Tabelle extrahieren.
Codierung des Scrapers
Richten Sie zunächst eine virtuelle Python-Umgebung ein und installieren Sie die erforderlichen Abhängigkeiten.
mkdir clearnet_scraper && cd clearnet_scraper
python3 -m venv venv
source venv/bin/activate
pip install requests beautifulsoup4
touch tornet_scrape.py
Öffnen Sie tornet_scrape.py und fügen Sie den folgenden Code ein:
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)
Das Skript ruft eine Webseite von einem lokalen Server mit requests und einem Sitzungscookie zur Authentifizierung ab. Es analysiert den HTML-Code mit BeautifulSoup und sucht dabei nach einer Tabelle (<tbody> mit der Klasse text-light). Für jede Tabellenzeile extrahiert es den Titel des Beitrags, den Autor, den Autorenlink, den Zeitstempel und den Link zum Beitrag und speichert diese in einer Liste von Wörterbüchern. Schließlich konvertiert es die Daten mit json.dumps in das JSON-Format und gibt sie aus.
Alle Daten scrapen
Sie haben gelernt, wie Sie Daten von einer einzigen Seite scrapen können. Nun lernen Sie, wie Sie zur nächsten Seite wechseln können, indem Sie die Seitenzahl um 1 erhöhen.
Hier sind die Änderungen, die Sie in Ihrem Code vornehmen müssen:
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)
Die Ausgabe sollte nun insgesamt 13 Beiträge anzeigen (die Anzahl kann bei Ihnen abweichen):
[
{
"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"
}
]
Probieren Sie dies auf einer anderen Seite aus und sehen Sie, was Sie erhalten. Funktioniert es oder müssen Sie es ändern, um die Daten korrekt anzusprechen?