12 avril 2020

3 search engines VS puppetter

Par ghaliano

Objectif

l’exemple suivant consiste à utiliser puppetter pour récupérer ls résultats de recherche à partir de trois moteurs de recherche différents googleyahooduckduckgo, et de renvoyer un seule résultat sous forme d’un tableau, l’architecture proposé permettra d’ajouter facilement un provider de recherche d’une manière à ne pas toucher au fonctionnement des autres providers ou bien la logique script principale, On utilise « puppetter » pour scrapper les résultats de recherche.

puppetter scrapping tool logo
Puppeteer est une bibliothèque node permet de contrôler Chrome sans tête (headless browser)

Introduction

Imaginons que vous voulez scrapper les données de plusieurs sites ou moteurs de recherche, la contrainte technique que vous allez directement affronter est l’hétéroginité de ces sites (et c’est le cas pour les sites objet de l’article googleyahooduckduckgo), c’est à dire le traitement qui vise à récupérer les données entre ces site est différents (structures du site, différents barrière de blocage, mesure supplémentaire pour imiter un headless browser ou même l’utilisation d’autres librairie node), de plus vous allez peut être besoin d’un moyen simple d’ajouter une cible de recherche facilement.

Le pattern Chaine de responsabilité vient en aide en proposant une solution éléguante qui consiste à abstraire le traitement des différents providers

Design pattern: le pattern Chaine de responsabilité avec node JS

La chaîne de responsabilité est un modèle de conception comportementale qui permet de transmettre la demande le long de la chaîne des gestionnaires potentiels jusqu’à ce que l’un d’eux traite la demande.

Le modèle permet à plusieurs objets de gérer la demande sans coupler la classe émettrice aux classes concrètes des récepteurs. La chaîne peut être composée dynamiquement au moment de l’exécution avec n’importe quel gestionnaire qui suit une interface de gestionnaire standard.

Le script principale

Voici la promesse! pour ajouter un provider il suffit d’ajouter une ligne dans tableau providers
vous voulez afficher le navigateur en cours de l’éxecution

    const browser = await puppeteer.launch({
        headless: true,
        args: ['--start-fullscreen']
    });

run.js

const provider_factory = new (require('./providers/chain.js'))();

provider_factory.chain("yahoo");
provider_factory.chain("google");
provider_factory.chain("duckduckgo");

provider_factory.search("scrapping").then(
    (links) => {
        console.log(links);
        process.exit(22);
    }
);

Classe chain.js

class  provider_chain {
    constructor() {
        this.providers = [];
    }

    chain(name){
        this.providers.push(require('./'+name));
    }

    search(term){
        return Promise.all(this.providers.map(async (provider) => {
            let a=[];
            (await provider.search(term)).forEach((link) =>{
                a.push(link);
            });
            return a;
        }))
    }
}
module.exports = provider_chain;

La même version du script pour traiter les liens de recherche au fur et à mesure qu’elles viennent sans que les provider bloquent les un au autre

En cours ;)

provider_chain.js

class  provider_chain {
    constructor() {
        this.providers = [];
    }

    chain(name){
        this.providers.push(require('./'+name));
    }

    search(term){
        return Promise.all(this.providers.map(async (provider) => {
            let a=[];
            (await provider.search(term)).forEach((link) =>{
                a.push(link);
            });
            return a;
        }))
    }
}
module.exports = provider_chain;

google.provider.js

const puppeteer = require('puppeteer');

let obj = {};

obj.search =  (term) => {
    return (async () => {
        const browser = await puppeteer.launch();

        const page = await browser.newPage();
        await page.goto('https://www.google.com/search?q='+term);


        let links = await page.evaluate(() => { var links = [];
            document
                .querySelectorAll('h3')
                .forEach((a) => {
                    links.push({provider: 'google', url: a.parentNode.getAttribute('href')});
                })

            return links;
        });

        await browser.close();

        return links;
    })()
}

module.exports = obj;

yahoo.provider.js

Pour Yahoo, l’étape suplémentaire un peux particulière est l’ajout de cookies, cesi permet à puppetter de sauter la popup de validation GRPD du site et aller directement vers la page de résultat de recherche

Comment trouver ces Cookies

Console de développement -> Application -> Cookies

const puppeteer = require('puppeteer');

let obj = {};

obj.search =  (term) => {
    return (async () => {
        const browser = await puppeteer.launch({});
        const page = await browser.newPage();

        //Trois cookies indispensable
        let cookies = [
            {"domain": ".search.yahoo.com",name: 'A1S', value: 'd=AQABBL1Bk14CEKPG9tHnr99clL-WUqeASpYFEgABAQGGlF59X8bHb2UB_iMAAAcIvEGTXgRx9IA&S=AQAAAumEFjhYKlpofzJiosgNAqU&j=WORLD'},
            {"domain": ".search.yahoo.com",name: 'B', value: '81t3h0hf96gds&b=3&s=80'},
            {"domain": ".search.yahoo.com",name: 'GUC', value: 'AQABAQFelIZffUIa9AQk'}
        ];

        await page.setCookie(...cookies);

        await page.goto('https://fr.search.yahoo.com/search?p=' + term);

        const res = await page.waitForFunction(
            'document.querySelectorAll("h3.title > a")'
        );

        let links = await page.evaluate(() => { var links = [];
            document
                .querySelectorAll('h3.title > a')
                .forEach((a) => {
                    links.push({provider: 'yahoo', url: a.getAttribute('href')});
                })

            return links;
        });

        return links;
    })()
}

module.exports = obj;

duckduckgo.provider.js

const puppeteer = require('puppeteer');

let obj = {};

obj.search =  function(term){
    return (async () => {
        const browser = await puppeteer.launch();

        const page = await browser.newPage();
        //await page.goto('https://duckduckgo.com/?q=scrapping');
        await page.goto('https://duckduckgo.com/?q='+term);

        const res = await page.waitForFunction(
            'document.querySelectorAll("a.result__a")'
        );

        let links = await page.evaluate(() => {
            let links = [];
            document.querySelectorAll("a.result__a").forEach((a) => {
                links.push({provider: 'duckduckgo', url: a.getAttribute('href')});
            });
            return links;
        });
        await browser.close();

        return links;
    })()
}


module.exports = obj;

à prendre en considération

Le code actuelle ne gère pas les erreurs (pas de connexion ou deny de service)

l’utilisation de async await au niveau du script principale run.js bloque le traitement jusqu’a ou tout les script retourne leurs liste de liens de recherche, l’utilisation d’un système d’évennement permet de traiter les résultat au fur et mesure

Voila n’hésitez si vous avez des question ou bien des améliorations pour les scripts 😉

Bien à vous