Dans cette section, nous allons faire nos premiers pas vers la maîtrise du web scraping. Ce que j'aime le plus dans cette section, c'est à quel point la collecte de données peut être passionnante, et le fait de surmonter des défis tels que la limitation du débit rend l'expérience encore plus excitante.

Je suis convaincu que vous allez parcourir cette section avec facilité, d'autant plus que vos assistants IA sont là pour clarifier tout ce que je ne vous aurais pas expliqué clairement.

Les thèmes abordés dans cette section sont les suivants :

  1. Identification de la langue et traduction des données
  2. Extraction des données du forum Clearnet
  3. Extraction des données du forum Tornet

Identification de la langue et traduction des données

Nos sites de cybercriminalité simulés contiennent des publications dans différentes langues, notamment en chinois, en russe et en coréen. Il est donc essentiel d'identifier la langue d'un texte et de le traduire en anglais s'il n'est pas déjà rédigé dans cette langue.

Pour commencer la traduction des données, vous pouvez utiliser le plan API gratuit de DeepL :

https://www.deepl.com/en/pro#developer

Pour augmenter la capacité et traduire des ensembles de données plus volumineux, vous devrez souscrire à l'un de leurs plans d'abonnement.

Accédez à votre clé API via la page des clés API :

https://www.deepl.com/en/your-account/keys

Vous trouverez ci-dessous un programme Python qui montre comment identifier la langue de cinq publications et traduire en anglais celles qui ne sont pas en anglais. Commencez par configurer un environnement de développement Python :

mkdir deepl_test && cd deepl_test
python3 -m venv venv
source venv/bin/activate
pip install deepl
touch deepl_en.py

Entrez le code suivant dans 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))

Exécutez ce code et le résultat JSON sera le suivant :

[
  {
    "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"
  }
]

Remarquez que nous conservons le texte original des publications en anglais (indiqué par « translated » : false). En effet, lorsque la langue détectée est l'anglais, la traduction n'est pas nécessaire. Pour les publications dans d'autres langues, nous les traduisons en anglais.

Dans les modules suivants, vous apprendrez à utiliser le module « langdetect » de Python pour identifier la langue d'un texte avant de l'envoyer à DeepL. Nous utilisons « langdetect » pour éviter de gaspiller des crédits API pour la détection ou la traduction de textes en anglais.

Module traducteur

L'exemple précédent se concentrait sur la traduction en masse de données codées en dur. Pour une expérience de développement plus flexible et modulaire, je vais fournir un extrait de code qui accepte une clé API et des données comme entrées pour la traduction. Cette approche, encapsulée dans un module « translator.py », est plus facile à intégrer et à utiliser dans n'importe quel programme :

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)

Voici comment appeler « translator.py » depuis « 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))

Dans les chapitres suivants, lorsque vous créerez un scraper web, vous utiliserez quelque chose comme « translator.py », car il est modulaire et l'API qu'il utilise peut être modifiée à tout moment. Cela facilite sa personnalisation. C'est ce que nous appelons une conception modulaire, quelque chose qui peut être modifié ou supprimé ultérieurement.


Extraction des données du forum Clearnet

Pour commencer, je vais vous montrer comment utiliser Playwright pour extraire le titre et la description d'un message et les traduire si nécessaire.

Tout d'abord, assurez-vous que le forum Clearnet est bien ouvert. Connectez-vous et accédez à un message spécifique, par exemple le message n° 17 dans la section « Sellers' Marketplace » (Marché des vendeurs) :

http://127.0.0.1:5000/post/marketplace/17

Lorsque vous accédez à cette page, le titre de la publication, sa description, le nom d'utilisateur et l'horodatage sont clairement visibles. Cependant, si vous inspectez le code source de la page :

view-source:http://127.0.0.1:5000/post/marketplace/17

Vous ne trouverez pas ces données. Cela s'explique par le fait que la page importe et utilise « main.js ». Vous pouvez consulter le code source de ce fichier ici :

view-source:http://127.0.0.1:5000/static/js/main.js

Dans main.js, la fonction loadPostDetails est chargée de récupérer les données des API backend :

    function loadPostDetails() {
        if ($('#post-title').length) {
            console.log('Loading post details for:', postType, postId);
            $.get(`/api/post/${postType}/${postId}`, function(data) {
                console.log('API response:', data);
                const post = data.post;
                const comments = data.comments;
                const user = data.user;

                // Update post title
                $('#post-title').text(post.title || 'Untitled');

                // Update post content
                let contentHtml = '';
                if (postType === 'announcements') {
                    contentHtml = post.content || '';
                } else {
                    contentHtml = `${post.description || ''}<br><strong>Price:</strong> ${post.price || 'N/A'}`;
                }
                contentHtml += `<br><br><strong>Posted by:</strong> <a href="/profile/${post.username}" class="text-light">${post.username}</a>`;
                contentHtml += `<br><strong>Date:</strong> ${post.date}`;
                $('#post-content').html(contentHtml);

                // Update category
                $('#post-category').text(post.category || 'N/A');

                // Update comments
                $('#comments-section').empty();
                if (comments && comments.length > 0) {
                    comments.forEach(function(comment) {
                        $('#comments-section').append(
                            `<div class="mb-3">
                                <p class="text-light"><strong>${comment.username}</strong> (${comment.date}): ${comment.content}</p>
                            </div>`
                        );
                    });
                } else {
                    $('#comments-section').html('<p class="text-light">No comments yet.</p>');
                }

                // Update back link
                $('#back-link').attr('href', `/category/${postType}/${post.category}`).text(`Back to ${post.category}`);
            }).fail(function(jqXHR, textStatus, errorThrown) {
                console.error('Failed to load post details:', textStatus, errorThrown);
                $('#post-title').text('Error');
                $('#post-content').html('<p class="text-light">Failed to load post. Please try again later.</p>');
                $('#comments-section').html('<p class="text-light">Failed to load comments.</p>');
            });
        }
    }

Cliquez avec le bouton droit sur la description de la publication et sélectionnez « Inspecter l'élément » dans les outils de développement de votre navigateur pour afficher clairement le contenu de la publication. Cela montre qu'un navigateur est nécessaire pour extraire les données. Nous utilisons « Playwright » car il offre des outils puissants pour automatiser l'extraction de données à partir de pages web dans un environnement de navigateur.

Comment fonctionne la page web ?

Avant de vous plonger dans le code, il est essentiel de comprendre comment fonctionne la page web et, par extension, le site. Chaque donnée est contenue dans un élément, tel que «

». Pour extraire les données de cet en-tête, nous pouvons cibler son identifiant, « #header ».

Pour explorer cela, ouvrez n'importe quel article sur le site clearnet -> cliquez avec le bouton droit de la souris sur le titre -> sélectionnez « Inspecter l'élément » -> puis cliquez avec le bouton droit de la souris sur le code mis en évidence dans l'inspecteur et choisissez « Modifier en HTML ».

Copiez et collez le code dans un éditeur de prise de notes :

<h2 class="text-light" id="post-title">AspenWell 生物科技有限公司 访问</h2>

Cliquez avec le bouton droit sur la description -> « Inspecter l'élément » -> cliquez avec le bouton droit sur le code mis en surbrillance dans l'inspecteur -> cliquez sur « Modifier en HTML ».

Copiez et collez le code dans votre éditeur :

<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>

Le nom d'utilisateur et la date sont également indiqués ici. Nous allons cibler ces éléments avec « playwright ».

Utilisation des cookies avec playwright

Comme nous sommes déjà connectés au site web, nous pouvons consulter les publications. Bien qu'il soit possible d'automatiser la connexion et de contourner les CAPTCHA à l'aide de l'IA, pour l'instant, une approche plus simple consiste à se connecter manuellement et à extraire vos cookies de session à partir des outils de développement. Vous apprendrez comment automatiser le contournement des CAPTCHA à l'aide de l'IA plus tard dans le cours.

Pour récupérer vos cookies de session :

  1. Appuyez sur F12 pour ouvrir les outils de développement.
  2. Accédez à l'onglet Stockage.
  3. Dans le panneau de gauche, sélectionnez Cookies.
  4. Cliquez sur l'adresse du site web et copiez tous les cookies.

Pour inspecter la manière dont les requêtes sont envoyées au site web :

  1. Appuyez sur « F12 » pour ouvrir les outils de développement.
  2. Accédez à l'onglet « Réseau ».
  3. Recherchez le fichier correspondant à la publication « 17 » de type « html » et cliquez dessus.
  4. Dans le panneau de droite, faites défiler jusqu'à la section « En-têtes de requête ».
  5. Cliquez sur « Brut » et copiez tout le contenu.

Vous trouverez ci-dessous un exemple de structure des requêtes, y compris les 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

Cela peut être un peu différent pour vous, voici un exemple. Le seul cookie dont nous avons besoin est « session ».

Extraction de données avec Playwright

Pour commencer, configurez un environnement et installez les dépendances :

mkdir play && cd play
touch play.py
python3 -m venv venv
source venv/bin/activate
sudo apt install libavif16
pip3 install playwright
playwright install

Ouvrez « play.py » et collez le code suivant :

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()

Ce code utilise le cookie « session » pour accéder à une page Web. Un cookie invalide empêchera l'accès et nécessitera une connexion. La fonction « sleep(400000) » laisse suffisamment de temps pour interagir avec la session du navigateur Playwright.

Maintenant que nous savons où sont stockées les données, nous sommes prêts à les extraire de la page Web. Voici comment extraire les données :

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()

Avec quelques ajustements, nous pouvons importer la fonction translate_en du module translator.py et l'utiliser. Pour que cela fonctionne, placez les fichiers play.py et translator.py dans le même répertoire, créez un environnement Python virtuel et installez toutes les dépendances requises par les deux fichiers. Modifiez ensuite play.py pour l'intégrer à 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()

Voici comment fonctionne le code :

  1. Importations : utilise playwright pour l'automatisation du navigateur, json pour la sortie, urljoin pour la gestion des URL et translate_to_en pour les traductions DeepL. Définit la clé API DeepL.
  2. Configuration du navigateur : lance le navigateur Chromium sans interface graphique avec sync_playwright, crée un contexte et ajoute un cookie de session pour l'authentification.
  3. Navigation sur la page : ouvre la page à l'adresse http://127.0.0.1:5000/post/marketplace/17.
  4. Extraction des données : utilise les localisateurs Playwright pour extraire :
    • Le titre de <h2> avec l'ID post-title.
    • Le contenu de <p> avec l'ID post-content.
    • Le nom d'utilisateur de <a> dans post-content.
    • Le lien de l'auteur à partir de href de <a>.
    • L'horodatage en analysant le texte après <strong>Date:</strong> via JavaScript.
    • L'URL de la publication à partir de la page actuelle.
    • Le lien complet de l'auteur à l'aide de urljoin.
  5. Traduction : traduit le titre et le contenu en anglais via DeepL. En cas d'erreur, renvoie le texte original avec la mention « UNKNOWN » (inconnu) comme langue.
  6. Sortie : crée un fichier JSON avec le titre, le contenu (original, langue, traduit), le nom d'utilisateur, l'horodatage, le lien vers l'article et le lien vers l'auteur. Imprime avec indentation.
  7. Nettoyage : ferme le navigateur.

Voici la sortie JSON sur le 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"
}

Comme vous pouvez le voir, le titre et la description ont été traduits. Il s'agit d'un exemple simple illustrant comment extraire des données à l'aide de Playwright et effectuer des traductions.


Extraction des données du forum Tornet

Dans cette section, je vais vous montrer comment extraire tous les titres, noms d'utilisateur, horodatages et liens des messages de la section « Buyers » (Acheteurs) du forum Tornet. Le forum Tor n'utilisant pas JavaScript, l'extraction des données est très simple.

Pour cette tâche, nous utiliserons les bibliothèques Python requests et BeautifulSoup.

Commencez par accéder à la section « Buyers » :

http://127.0.0.1:5000/category/marketplace/Buyers

Pour moi, cette page contient 13 messages, avec une pagination permettant de passer à la série suivante de 10 messages ou moins. En cliquant sur « Next », l'URL change pour :

http://127.0.0.1:5000/category/marketplace/Buyers?page=2

Ce système de pagination est pratique : il suffit d'ajuster le numéro de page pour naviguer vers l'avant ou vers l'arrière. Cependant, si vous entrez un numéro de page invalide, tel que 56, la réponse affichera « Aucun article trouvé ». Cela sert de signal clair à votre script pour s'arrêter.

Notez cela dans votre éditeur de prise de notes.

En affichant le code source de la page 1, vous pouvez voir tous les articles directement dans le code 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>

L'objectif principal est de comprendre la structure du tableau. Vous devez analyser le code source HTML à l'aide de BeautifulSoup et extraire les données du tableau.

Codage du scraper

Pour commencer, configurez un environnement Python virtuel et installez les dépendances.

mkdir clearnet_scraper && cd clearnet_scraper
python3 -m venv venv
source venv/bin/activate
pip install requests beautifulsoup4
touch tornet_scrape.py

Ouvrez « tornet_scrape.py » et collez le code suivant :

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)

Le script récupère une page web à partir d'un serveur local à l'aide de requests avec un cookie de session pour l'authentification. Il analyse le code HTML avec BeautifulSoup, en ciblant un tableau (<tbody> avec la classe text-light). Pour chaque ligne du tableau, il extrait le titre de l'article, l'auteur, le lien vers l'auteur, l'horodatage et le lien vers l'article, puis les stocke dans une liste de dictionnaires. Enfin, il convertit les données au format JSON avec json.dumps et les imprime.

Récupération de toutes les données

Vous avez appris à récupérer les données d'une seule page, mais vous allez maintenant apprendre à passer à la page suivante en augmentant le numéro de page de 1.

Voici les modifications que vous devez apporter à votre code :

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)

Le résultat devrait maintenant afficher 13 publications au total (le nombre peut varier selon votre configuration) :

[
    {
        "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"
    }
]

Essayez cela sur n'importe quelle autre page et voyez ce que vous obtenez. Cela fonctionne-t-il ou devez-vous le modifier pour cibler correctement les données ?