Tutoriel : Comment créer un crawler en JS pour rechercher des noms de domaines expirés

Un crawler est un programme automatisé qui explore les pages web pour en extraire des données. C’est exactement ce que font les bots de Google pour indexer les sites dans les résultats de recherche. Comprendre comment fonctionne un robot d’exploration est essentiel en SEO, car le comportement du crawler détermine la visibilité d’un site.

Dans ce tutoriel, nous allons voir comment créer un petit crawler en JavaScript. L’objectif ? Détecter des noms de domaines expirés, une pratique courante chez les référenceurs pour récupérer des backlinks existants et renforcer leur stratégie de référencement naturel.

Pourquoi utiliser JavaScript pour ce type de crawler ?

  • Parce qu’il est facile à mettre en place, même pour un développeur débutant.
  • Parce que cela permet de mieux comprendre comment un Googlebot interagit avec du contenu généré dynamiquement.
  • Parce que l’automatisation en JS peut être adaptée à de nombreux usages (scraping, audit SEO, veille concurrentielle).

Avant de passer au code, il est important de rappeler que les crawlers ne sont pas uniquement réservés aux moteurs de recherche. Vous pouvez en créer un vous-même pour analyser vos propres sites, suivre vos concurrents, ou comme ici, dénicher des domaines expirés à fort potentiel SEO.

Dans la suite, nous allons coder ensemble un crawler en JavaScript, étape par étape, pour que vous puissiez l’adapter à vos besoins.

Pré-requis:

  • Avoir une connaissance de base en JavaScript.
  • Node.js installé sur votre machine.

Étapes du Tutoriel:

1. Mise en place des dépendances

Nous allons utiliser plusieurs modules : https, http, fs, htmlparser2, et url.

Installez les dépendances avec npm :bashCopy codenpm install htmlparser2

2. Configuration initiale

Définissez la configuration de base, notamment le nombre de requêtes simultanées, le nombre maximal de tentatives, le délai entre les tentatives, etc.

Stop

Si vous ne supportez plus qu’un patron décide de votre vie :

5432 personnes suivent déjà ces conseils pour quitter leur boss et lancer leur business. Voulez-vous savoir pourquoi ? Cliquez sur le bouton :

Montrez-moi!

Si vous souhaitez restreindre le crawler à un seul site, configurez RESTRICT_TO_SITE à true et fournissez l’URL de départ.

3. Liste noire des domaines

Pour éviter de crawler certains sites tels que Google, Facebook, etc., nous avons créé une liste noire BLACKLISTED_DOMAINS.

Vous pouvez ajouter ou supprimer des domaines de cette liste selon vos besoins.

4. Fonctions utilitaires

isValidUrl(url): Vérifie si une URL est valide.

isBlacklisted(url): Vérifie si une URL appartient à un domaine de la liste noire.

shouldCrawlUrl(url): Vérifie si une URL doit être crawlée (en fonction de la restriction du site).

5. Fetching des pages

La fonction fetchPage(url) est responsable de la récupération du contenu HTML d’une URL donnée.

Si une erreur de type « ERR_NAME_NOT_RESOLVED » se produit (ce qui signifie que le domaine est probablement expiré), l’URL est enregistrée.

6. Extraction des liens

extractLinks(html, baseUrl): extrait tous les liens d’une page web. Il convertit également les URL relatives en URL absolues.

7. Crawling

La fonction crawl() est le cœur du crawler. Elle parcourt les URL, vérifie si elles ont été visitées ou non, et ajoute les nouvelles URL à la file d’attente.

Le crawler s’exécute de manière récursive jusqu’à ce qu’il n’y ait plus d’URL à visiter ou jusqu’à atteindre la profondeur maximale définie.

8. Exécution

Lancez le crawler avec node votreFichier.js.

Une fois le crawling terminé, vous trouverez une liste des noms de domaines expirés dans le fichier expired_domains.txt.


Le code

const https = require('https');
const http = require('http');
const fs = require('fs');
const htmlparser = require('htmlparser2');
const urlModule = require('url');
const MAX_CONCURRENT_REQUESTS = 5;
const MAX_RETRIES = 1;
const RETRY_DELAY = 5000;  // Delay in milliseconds (5 seconds)
const EXPIRED_DOMAINS_FILE = 'expired_domains.txt';
const MAX_DEPTH = 10;  // Par exemple, pour 2 niveaux de profondeur
const START_URL = false;
if(START_URL) {
const INITIAL_HOSTNAME = new URL(START_URL).hostname;
}
const RESTRICT_TO_SITE = false;  // Mettez à false pour crawler le web entier
//si START_URL est FALSE, on charge le fichier urls.txt et on le parse pour l'ajouter à urlsToVisit
let urlsToVisit = [];
if (START_URL === false) {
    // Vérifier si le fichier urls.txt existe
    if (fs.existsSync('urls.txt')) {
        const urls = fs.readFileSync('urls.txt', 'utf8');
        const urlsArray = urls.split('\n').map(url => url.trim()); // Nettoyer chaque URL
        
        urlsArray.forEach(url => {
            // Ajouter seulement les URLs valides à urlsToVisit
            if (isValidUrl(url)) {
                urlsToVisit.push({ url: url, depth: 0 });
            }
        });
    } else {
        console.error("Le fichier urls.txt n'existe pas.");
    }
} else {
    urlsToVisit = [{ url: START_URL, depth: 0 }];
}
let visitedUrls = new Set();
let checkedDomains = new Set();
// Blacklist of domain patterns
const BLACKLISTED_DOMAINS = [
    /google\./,
    /apple\./,
    /adobe\./,
    /youtube\./,
    /facebook\./,
    /twitter\./,
    /linkedin\./,
    /pinterest\./,
    /bing\./,
    /yahoo\./,
    /instagram\./,
    /amazon\./,
    /tiktok\./,
    /gouv\./,
    // Add more as needed
];
function isBlacklisted(url) {
    const hostname = urlModule.parse(url).hostname || '';
    return BLACKLISTED_DOMAINS.some(pattern => pattern.test(hostname));
}
function logExpiredDomain(domain) {
    fs.appendFileSync(EXPIRED_DOMAINS_FILE, domain + '\n');
}
async function fetchPage(url, retries = MAX_RETRIES) {
    return new Promise((resolve, reject) => {
        const requester = url.startsWith('https:') ? https : http;
        requester.get(url,{
            rejectUnauthorized: false  // Ajoutez cette ligne pour ignorer les erreurs de certificat SSL
            
        },(res) => {
            let data = '';
            res.on('data', (chunk) => {
                data += chunk;
            });
            res.on('end', () => {
                resolve(data);
            });
        }).on('error', async (err) => {
            if (err.message.includes('ERR_NAME_NOT_RESOLVED')) {
                console.log(`Domain expired or inaccessible: ${url}`);
                logExpiredDomain(new URL(url).hostname);
            }else{
                //ignore all others erros
                resolve("");
            }
            reject(err);
        });
    });
}
function isValidUrl(url) {
    try {
        // Tentez de créer un nouvel objet URL
        new URL(url);
        // Assurez-vous que l'URL commence par http:// ou https://
        // et qu'elle n'a pas de protocole imbriqué.
        const pattern = /^https?:\/\/(?!https?:\/\/)/;
        return pattern.test(url);
    } catch (err) {
        // Si une erreur se produit lors de la création de l'objet URL,
        // cela signifie que l'URL est invalide.
        return false;
    }
}
function shouldCrawlUrl(url) {
    const hostname = new URL(url).hostname;
    return !RESTRICT_TO_SITE || hostname === INITIAL_HOSTNAME;
}
function addUrlToQueue(url, currentDepth) {
    if (isValidUrl(url) && shouldCrawlUrl(url) && !visitedUrls.has(url) && currentDepth <= MAX_DEPTH) {
        urlsToVisit.push({ url: url, depth: currentDepth + 1 });
    }
}
function extractLinks(html, baseUrl) {
    const links = [];
    const parser = new htmlparser.Parser({
        onopentag: (name, attribs) => {
            if (name === "a" && attribs.href) {
                // Convert relative URLs to absolute URLs
                let absoluteUrl = urlModule.resolve(baseUrl, attribs.href);
                // Check if the URL is a malformed one with nested protocols
                if (absoluteUrl.includes("https://https/") || absoluteUrl.includes("http://http/")) {
                    return;  // Skip this URL
                }
                // Check if the URL is valid and not blacklisted
                if (isValidUrl(absoluteUrl) && !isBlacklisted(absoluteUrl)) {
                    links.push(absoluteUrl);
                }
            }
        }
    }, { decodeEntities: true });
    parser.write(html);
    parser.end();
    return links;
}
async function crawl() {
    while (urlsToVisit.length > 0) {
        const currentUrlObj = urlsToVisit.shift();
        const currentUrl = currentUrlObj.url;
        const currentDepth = currentUrlObj.depth;
        //console.log(`Visiting ${currentUrl}`);
        const domain = new URL(currentUrl).hostname;
        if (!visitedUrls.has(currentUrl)) {
            if (!checkedDomains.has(domain)) {
                checkedDomains.add(domain);
                visitedUrls.add(currentUrl);
                const html = await fetchPage(currentUrl);
                const newUrls = extractLinks(html, currentUrl);
                for (const newUrl of newUrls) {
                    if (!visitedUrls.has(newUrl)) {
                        addUrlToQueue(newUrl, currentDepth);
                    }
                }
            }
        }
        const promises = [];
        for (let i = 0; i < Math.min(MAX_CONCURRENT_REQUESTS - 1, urlsToVisit.length); i++) {
            const nextUrlObj = urlsToVisit.shift();
            const nextUrl = nextUrlObj.url;
            console.log(`Visiting ${nextUrl}`);
            const nextDomain = new URL(nextUrl).hostname;
            if (!visitedUrls.has(nextUrl)) {
                if (!checkedDomains.has(nextDomain)) {
                    checkedDomains.add(nextDomain);
                    visitedUrls.add(nextUrl);
                    promises.push(fetchPage(nextUrl).then(html => extractLinks(html, nextUrl)));
                }
            }
        }
        const newUrlsArrays = await Promise.all(promises);
        for (const newUrls of newUrlsArrays) {
            for (const newUrl of newUrls) {
                if (!visitedUrls.has(newUrl)) {
                    addUrlToQueue(newUrl, currentDepth + 1);
                }
            }
        }
    }
}
crawl().then(() => {
    console.log('Crawling completed.');
}).catch((err) => {
    console.error('An error occurred:', err);
});

Visible sur Gist

Conseils pour les référenceurs web & fans de Google:

  • Avant d’acheter un domaine expiré, assurez-vous de vérifier son historique, son classement, son profil de backlink, etc.
  • Utilisez un outil comme Wayback Machine pour voir les anciennes versions du site. Cela vous donnera une idée du contenu précédent du site.
  • Faites attention aux sanctions de Google. Si un domaine a été pénalisé dans le passé, il peut ne pas être bénéfique pour le SEO.

Avec ce crawler Google, vous avez un outil puissant pour trouver des domaines expirés qui peuvent être bénéfiques pour vos efforts de référencement. Adaptez le code selon vos besoins et bonne chance dans vos recherches! Vous trouverez une version améliorée dans le guide complet sur les NDD’s expirés (bas de page)

Questions Fréquemment Posées

Vous souhaitez recevoir davantage de trucs & d'astuces ? Tous les jours j'envois un mail à mes 6000 (et quelques) abonnés. Si ça vous dit, cliquez-ici pour vous inscrire