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:

  1. Spracherkennung und Datenübersetzung
  2. Clearnet-Forum-Datenextraktion
  3. 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 &gt; 内部 RDP &gt; 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:

  1. Drücken Sie „F12“, um die Entwicklertools zu öffnen.
  2. Navigieren Sie zur Registerkarte „Speicher“.
  3. Wählen Sie im linken Bereich „Cookies“ aus.
  4. Klicken Sie auf die Website-Adresse und kopieren Sie alle Cookies.

So überprüfen Sie, wie Anfragen an die Website gesendet werden:

  1. Drücken Sie „F12“, um die Entwicklertools zu öffnen.
  2. Wechseln Sie zur Registerkarte „Netzwerk“.
  3. Suchen Sie die Datei für den Beitrag „17“ mit dem Typ „html“ und klicken Sie darauf.
  4. Scrollen Sie im rechten Bereich zum Abschnitt „Anforderungsheader“.
  5. 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:

  1. Importe: Verwendet playwright für die Browser-Automatisierung, json für die Ausgabe, urljoin für die URL-Verarbeitung und translate_to_en für DeepL-Übersetzungen. Definiert den DeepL-API-Schlüssel.
  2. Browser-Einrichtung: Startet den headless Chromium-Browser mit sync_playwright, erstellt einen Kontext und fügt ein Sitzungscookie für die Authentifizierung hinzu.
  3. Seitennavigation: Öffnet die Seite unter http://127.0.0.1:5000/post/marketplace/17.
  4. Datenextraktion: Verwendet Playwright-Lokalisierer, um Folgendes zu extrahieren:
    • Titel aus <h2> mit der ID post-title.
    • Inhalt aus <p> mit der ID post-content.
    • Benutzername aus <a> in post-content.
    • Autorenlink aus <a>s href.
    • Zeitstempel durch Parsen des Textes nach <strong>Date:</strong> über JavaScript.
    • Post-URL der aktuellen Seite.
    • Vollständiger Autorenlink mit urljoin.
  5. Übersetzung: Übersetzt Titel und Inhalt über DeepL ins Englische. Bei einem Fehler wird der Originaltext mit der Sprache „UNKNOWN” zurückgegeben.
  6. Ausgabe: Erstellt JSON mit Titel, Inhalt (Original, Sprache, Übersetzung), Benutzername, Zeitstempel, Post-Link und Autoren-Link. Druckt mit Einrückung.
  7. 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?