Extraire les données d’un site en 3 minutes avec Javascript

Rédigé par Gabin Desserprit


Le but de cet article est de vous montrer qu'il est très simple, avec quelques lignes de javascript, d'extraire des données d'une page internet (html) en les structurant.

A l'ère du Big Data, l'intérêt business du webscraping est de faire face à une demande toujours croissante de l'analyse des tendances de consommation. Les intérêts sont multiples et sont applicables à de nombreux domaines hautement concurrentiels.

En voici une liste non-exhaustive qui pourra vous faire vous rendre compte du réel enjeu de la récupération de données scrapées :

  • Listing des offres du marché immobilier
  • Collecte des données de site de type "retail" et analyse des tendances journalières
  • Récupération des offres promotionnelles des sites marchands
  • Extraction des offres d'emploi des jobs boards
  • Monitoring des prix des environnements à fort taux de compétitivité par les prix
  • Récupération des "leads" des annuaires et des répertoires business
  • Analyse des tendances de mots clés dans un marché donné


Prérequis : Connaître un minimum le javascript et la composition d'une page html ainsi que du CSS.


Une liste de sociétés



Pour l'exemple, je vous propose une page sur laquelle sont répertoriées 2 entreprises que nous allons extraire.

Intéressons-nous à son code HTML :

<!DOCTYPE html>  
<html lang="en">  
<head>  
</head>

<body>  
<!--     Data we want to scrape starts here -->  
  <div class="list items">
    <div class="item">
      <div class="header">
        <h1 itemprop="name">  <a href="/comp/tessera">Tessera  </a>        </h1>
        <p rel="description"> Proud of our wide range of product
          we developped many project in the past 4 years. <br><br> You can find the company 
          in 14 different countries <p></p> in the world. <br>
          Blablabla. <br>
        </p>
        <span rel="updatedAt">     Updated - 05/01/2017  </span>
      </div>
      <div class="contact">
        <span itemprop="employeeName">  Mike Layn        </span> <br>
        <span itemprop="employeeJobTitle">      Marketing Assistant</span> <br>
        <span itemprop="telephone">       Phone: (841) 467-168  </span> <br>
        <span itemprop="email">      Email: mike.layn@tessera.io</span> <br>
      </div>
    </div>
    <div class="item">
      <div class="header">
        <h1 itemprop="name">  <a href="/comp/marcox">  Marcox   </a>     </h1>
        <p rel="description"> Lorem ipsum dolor  <p></p> sit amet, consectetur adipisicing elit. Cupid 
          in any actions we  <br> take in the world. <br> <br>
        </p>
        <span rel="updatedAt">  Updated - 01/02/2017  </span>
      </div>
      <div class="contact">
        <span itemprop="employeeName">  Jake Kannegan      </span> <br>
        <span itemprop="employeeJobTitle">   Owner </span> <br>
        <span itemprop="telephone">       Phone:    +1497 467168  </span> <br>
        <span itemprop="email">      Email:     jakek@marcox.com</span> <br>
      </div>
    </div>
  </div>
    <!--    Data we want to scrape ends here -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="./js/script.js"></script>
</body>

</html>  

Comme vous l'avez vu le code est simple mais les textes qui le composent sont un peu en vrac.

Pour l'exercice, j'ai gardé une arborescence simple pour ne pas vous perdre dès la première ligne. Mais vous verrez que dans la vie, les pages deviennent très vite complexes. D'où l'intérêt de vous armez des bons outils !

La structure des données



Donnons un sens au données que nous souhaitons récupérer. Lorsque vous scrapez des données, vous voulez être capable de les enregistrer quelque part, comme dans une base de données.

Il nous faut alors organiser la structure de chaque société.

company  
|_  name
|_  description
|_  updatedAt
|_  url
|_  contact
    |_  telephone
    |_  employee
        |_  name
        |_  jobTitle
        |_  email

Une feuille blanche et un stylo font aussi bien l'affaire !


Vous souhaitez collaborer avec les meilleurs freelance en Growth Hacking ?

Rendez vous sur Crème de la Crème !



Des selecteurs uniques



Nous souhaitons maintenant identifier quels sélecteurs CSS vont nous permettrent d'identifier chacune des informations que nous venons d'organiser.

Des outils existent pour nous aider. Je vous invite à installer le SelectorGadget ou le CSS Selector Tester sur Chrome. Associés à l'utilisation du panneau développeur de votre navigateur, vous devriez pouvoir vous en sortir !

Ici, la structure est plutôt simple. Nous pouvons donc déterminez les sélecteurs suivants :

company : .list.items .item  
|_  name : .header [itemprop=name]
|_  description : .header [rel=description]
|_  updatedAt : .header [rel=updatedAt]
|_  url : .header [itemprop=name] a
|_  contact : .contact
    |_  telephone : [itemprop=telephone]
    |_  employee
        |_  name : [itemprop=employeeName]
        |_  jobTitle : [itemprop=employeeJobTitle]
        |_  email : [itemprop=email]

Comme vous pouvez le voir, le noeud employé n'a pas de sélecteur.

En effet, nous ne cherchons pas à copier la structure de la page mais bien à donner un sens aux données que nous récupérons. Il m'a semblé intéressant de séparer l'employé des informations générales de l'entreprise.

Récupérer les données



Nous pourrions soit le faire avec du javascript simple ou alors nous pourrions utiliser la force d'un super outil : cheerio.js. Cheerio se base sur la syntaxe de jQuery ce qui le rend très agréable à l'utilisation.

Faisons-le d'abord avec Cheerio seul pour voir comment l'on devrait s'y prendre (on utilisera un plugin pour Cheerio sympa juste ensuite) :

let cheerio = require('cheerio')  
let $ = cheerio.load('our html page url here')

var companiesList = [];

// For each .item, we add all the structure of a company to the companiesList array
// Don't try to understand what follows because we will do it differently.
$('.list.items .item').each(function(index, element){
    companiesList[index] = {};
    var header = $(element).find('.header');
    companiesList[index]['name'] = $(header).find('[itemprop=name]').text();
    companiesList[index]['description'] = $(header).find('[rel=description]').text();
    companiesList[index]['updatedAt'] = $(header).find('[rel=updatedAt]').text();
    companiesList[index]['url'] = $(header).find('.header [itemprop=name] a').getAttribute('href');
    var contact = $(element).find('.contact');
    companiesList[index]['contact'] = {};
    companiesList[index]['contact']['telephone'] = $(contact).find('[itemprop=telephone]').text();
    companiesList[index]['contact']['employee'] = {};
    companiesList[index]['contact']['employee']['name'] = $(contact).find('[itemprop=employeeName]').text();
    companiesList[index]['contact']['employee']['jobTitle'] = $(contact).find('[itemprop=employeeJobTitle]').text();
    companiesList[index]['contact']['employee']['email'] = $(contact).find('[itemprop=email]').text();
});

console.log(companiesList); // Output the data in the terminal  

Ce qui donne le résultat suivant (au format json / javascript object) :

// Here is the output data:
// [
//     {
//         "name": "  Tessera       ",
//         "description": " Proud of our wide range of product\n\t\t\t\twe developped many project in the past 4 years.  You can find the company \n\t\t\t\tin 14 different countries ",
//         "updatedAt": "     Updated - 05/01/2017  ",
//         "contact": {
//             "telephone": "       Phone: (841) 467-168  ",
//             "employee": {
//                 "name": "  Mike Layn        ",
//                 "jobTitle": "      Marketing Assistant",
//                 "email": "      Email: mike.layn@tessera.io"
//             }
//         }
//     },
//     {
//         "name": "  Marcox      ",
//         "description": " Lorem ipsum dolor  ",
//         "updatedAt": "  Updated - 01/02/2017  ",
//         "contact": {
//             "telephone": "       Phone:    +1497 467168  ",
//             "employee": {
//                 "name": "  Jake Kannegan      ",
//                 "jobTitle": "   Owner ",
//                 "email": "      Email:     jakek@marcox.com"
//             }
//         }
//     }
// ]

Nous récupérons bien les 2 entreprises dans une liste (array) ! Par contre, ce n'est pas fameux. Il y a des espaces partout, des dates qui ne sont pas que des dates, des emails avec du texte à coté et des numéro de téléphones pas formatés.


Alors oui, on aurait pu prendre la peine d'utiliser des fonctions pour nettoyer tout ça mais ça nous aurait encore rajouté du code et il aurait fallu copier/coller ces fonctions pour chacune des extractions.

Maintenant que l'on a vu la façon "bourrin", découvrons le plugin qui va vous faciliter la vie :

jsonframe-cheerio



json frame est un plugin qui étend les possibilités de cheerio avec une simple fonction : .scrape(frame, {options}).

Visitez le repo github pour plus d'exemples et d'information sur le plugin ou directement sur le package npm.

Mais à quoi sert-il ?



json frame vous permet de spécifier la structure de vos données ainsi que les sélecteurs associés sous un format simple, le JSON. Le même format d'entrée vous est retourné en sortie, prêt à être sauvegardé en base de données !

Voyons tout de suite comme ça fonctionne. Nous commençons d'abord par définir la structure des données que nous souhaitons extraire puis nous lançons le scraper.

let cheerio = require('cheerio');  
let jsonframe = require('jsonframe-cheerio');

let $ = cheerio.load(/* the html here */');  
jsonframe($); // initializes the plugin

var frame = {  
  "companies": { // setting the parent item as "companies"
    _s: ".item", // defines the elements to search for
    _d: [{ // _d: [{}] defines a list of items
      "name": ".header [itemprop=name]", // inline selector defining "name" so "company"."name"
      "description": ".header [rel=description]", // inline selector defining "description" as "company"."description"
      "updatedAt": {
          _s: ".header [rel=updatedAt]",
        _p: /\d{1,2}\/\d{1,2\/\d{2,4}/
      },
      "url": { // defining "url" by an attribute with "attr" and "selector" in an object
        _s: ".header [itemprop=name]", // is actually the same as the inline selector
        _a: "href" // the attribute name to retrieve
      },
      "contact": { // set up a parent "contact" element as "company"."contact"
        _s: ".contact", // defines the element to search for
        _d: { // defines the data which "contact" will contain
          "telephone": { // using "type" to use "telephone" parser to extract only the telephone
            _s: "[itemprop=telephone]", // simple selector for "telephone"                
            _t: "telephone" // using "telephone" plugin parser
          },
          "employee": { // setting a parent node "employee" as "company"."contact"."employee"
            "name": "[itemprop=employeeName]", // inline selector defining "name"
            "jobTitle": "[itemprop=employeeJobTitle]", // inline selector defining "jobtitle"
            "email": { // using "type" to use "email" parser to extract only the email
              _s: "[itemprop=email]", // simple selector for "email"
              _t: "email" // using "email" plugin parser
            }
          }
        }
      }
    }]
  }

};

var companiesList = $('.list.items').scrape(frame);  
console.log(companiesList); // Output the data in the terminal  

Et voici le beau résultat qu'il nous retourne :

/*
{
  "companies": [{
      "name": "Tessera",
      "description": "Proud of our wide range of product we developped many project in the past 4 years. You can find the company in 14 different countries in the world. Blablabla.",
            "updatedAt": "05/01/2017",
      "url": "/comp/tessera",
      "contact": {
        "telephone": "841467168",
        "employee": {
          "name": "Mike Layn",
          "jobTitle": "Marketing Assistant",
          "email": "mike.layn@tessera.io"
        }
      }
    },
    {
      "name": "Marcox",
      "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Cupid in any actions we take in the world.",
            "updatedAt": "01/02/2017",
      "url": "/comp/marcox",
      "contact": {
        "telephone": "1497467168",
        "employee": {
          "name": "Jake Kannegan",
          "jobTitle": "Owner",
          "email": "jakek@marcox.com"
        }
      }
    }
  ]
}
*/

Comme vous pouvez le constater, nous récupérons un jeu de données propre. La structure de sortie a été définie correctement dès le départ, je pourrais donc directement sauvegarder ces données.

Maintenant que nous avons définie cette frame, nous pouvez très facilement l'appliquer à l'ensemble des pages similaires à celle-ci afin d'extraire l'ensemble des entreprises répertoriées sur un site par exemple.

Gardez à l'esprit que cet article vous propose un exemple simple. Quand il est question de récupérer des jeu de données importants (plusieurs dizaines de miliers, centaines de miliers voir millions), garder une structure claire et simple sous la forme d'une arborescence aide beaucoup! (comme avec la frame json)

N'hésitez pas d'ailleurs à imbriquer des frames les unes dans les autres pour rendre votre code plus lisible.

Encore une fois, pour en apprendre d'avantage sur l'utilisation du plugin jsonframe, la documentation ce trouve ici.


code-digital



J'espère que cette article vous aura donné l'envie d'en apprendre un peu plus sur l'extraction de données. Et n'oubliez pas qu'à chaque problème, l'outil adapté n'est pas forcément le même. Pour des extractions simples on peut par exemple utiliser l'extension chrome WebScraper.

Sachez également que je suis le créateur du plugin jsonframe-cheerio. En tant que Data Consultant, je dois être capable de faire face à n'importe quelle demande et cet outil est un des secrets qui me permet de traiter autant données en si peu de temps avec très peu d'essais - erreurs.





Rédigé par Gabin Desserprit
Follow me on Medium & LinkedIn
My blog