Extraction de données web et persistance

Dans ce tutoriel, nous allons sauvegarder dans une base de données NoSQL, Elasticsearch, les informations extraites automatiquement de pages web.

Dans les précédents articles (ici et ), nous avons vu comment extraire des données web grâce au framework Scrapy, puis comment les sauvegarder dans des fichiers au format CSV ou JSON.

Si ces données sont destinées à être exploitées par d’autres processus logiciels, il peut être intéressant de mettre en place une base de données afin d’obtenir une persistance de ces données.

Elasticsearch

Elasticsearch est un serveur d’indexation et de recherche de données basé sur la librairie Lucene. La particularité de cette base NoSQL est que les documents, ainsi que le requêtage, sont au format JSON.

La documentation officielle indique plusieurs solutions d’installation, soit via les paquets de votre distribution, soit par compilation depuis une archive, ou encore via le système de containers Docker. Personnellement, j’utilise cette dernière solution qui est la plus moderne et la moins dépendante de votre distribution.

Si tout s’est bien passé à l’installation, vous devriez pouvoir accéder à la page http://localhost:9200/ :

Structuration des données

Avant de pouvoir sauvegarder nos données, il est nécessaire de définir un schéma structurel d’un document (la page web). Pour rappel, le scraper et le crawler récupèrent trois informations sur chaque page : l’url, le titre et la description.

Le schéma JSON Elasticsearch minimal correspondant à ce type de document est le suivant :

{
 "properties": {
  "description": {
    "type": "text"
  },
  "title": {
    "type": "text"
  },
  "url": {
    "type": "keyword"
  }
 }
}

Il faut ensuite créer un index nommé web, puis associer cet index à un type nommé page correspondant au schéma structurel précédent. Pour cela, nous utiliserons le logiciel cURL :

$ curl -X PUT http://localhost:9200/web
$ curl -X PUT -H "Content-Type: application/json" -d '{
"properties": {
  "description": {
    "type": "text",
    "analyzer": "french"
  },
  "title": {
    "type": "text",
    "analyzer": "french"
  },
  "url": {
    "type": "keyword"
  }
}
}
' "http://localhost:9200/web/_mapping/page"

Vous aurez peut être besoin d’ajouter des informations d’authentification à cette commande.

Persistance des données

Maintenant que notre base est en place et prête à recevoir nos documents, il est temps de modifier le scraper et le crawler.

Tout d’abord, installez le client Python pour Elasticsearch :

$ pip install elasticsearch

Puis, modifiez le scraper :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import scrapy
import elasticsearch

# open elasticsearch connection
from elasticsearch import Elasticsearch
es = Elasticsearch(hosts=["localhost"], http_auth=("elastic", "changeme"), port=9200)

class PersistenceSpider(scrapy.Spider):
    name = 'persistencespider'
    start_urls = []

    def __init__(self, *args, **kwargs):
        super(BasicSpider, self).__init__(*args, **kwargs)
        # read input file that contains pages to crawl
        with open(kwargs.get('file')) as f :
            self.start_urls = f.read().splitlines()

    def parse(self, response):
        doc = {
            "url": response.url,
            "title": response.css('title::text').extract_first(),
            "description": response.css("meta[name=description]::attr(content)").extract_first()
        }
        # insert document in elasticsearch
        res = es.index(index="web", doc_type='page', body=doc)
        print(res['created'])
        yield doc

Et enfin, lancez le :

$ scrapy runspider persistencespider.py -a file=list_pages.txt

Si tout se passe bien, allez sur la page http://localhost:9200/web/page/_count. Vous devriez trouver le même nombre de documents en base que le nombre de pages de votre fichier :

{"count":56,"_shards":{"total":5,"successful":5,"failed":0}}

Faisons de même avec le crawler :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from urllib.parse import urlparse

# open elasticsearch connection
from elasticsearch import Elasticsearch
es = Elasticsearch(hosts=["localhost"], http_auth=("elastic", "changeme"), port=9200)

class PersistenceCrawler(CrawlSpider):
    name = 'persistencecrawler'
    start_urls = []
    allowed_domains = []
    rules = (
        # Extract all inner domain links with state "follow"
        Rule(LinkExtractor(), callback='parse_items', follow=True, process_links='links_processor'),
    )

    def __init__(self, *args, **kwargs):
        super(CrawlSpider, self).__init__(*args, **kwargs)
        # read input file that contains domains to crawl
        with open(kwargs.get('file')) as f :
            self.start_urls = f.read().splitlines()
            self.allowed_domains = [urlparse(url).netloc for url in self.start_urls]
        self._compile_rules()

    def links_processor(self,links):
        """
        A hook into the links processing from an existing page, done in order to not follow "nofollow" links
        """
        ret_links = list()
        if links:
            for link in links:
                if not link.nofollow:
                    ret_links.append(link)
        return ret_links

    def parse_items(self, response):
        doc = {
            "url": response.url,
            "title": response.css('title::text').extract_first(),
            "description": response.css("meta[name=description]::attr(content)").extract_first()
        }
        # insert document in elasticsearch
        res = es.index(index="web", doc_type='page', body=doc)
        print(res['created'])
        yield doc

Le crawler se lance avec cette commande :

$ scrapy runspider persistencecrawler.py -a file=list_pages.txt

Moteur de recherche

Les données des pages sont maintenant dans Elasticsearch ! Vous pouvez requêter la base simplement via votre navigateur, par exemple http://localhost:9200/web/page/_search?q=freelance, ou encore via une requête en POST avec cURL :

$ curl -X POST -H "Content-Type: application/json" -d '{
	"query":{
		"match":{"title":"freelance"}
	}
}' "http://localhost:9200/web/page/_search"

Avec pour résultat :

...
  },
  "hits": {
    "total": 35,
    "max_score": 0.60887885,
    "hits": [
      {
        "_index": "web",
        "_type": "page",
        "_id": "AV1jHGK8y84g6ALLsHm0",
        "_score": 0.60887885,
        "_source": {
          "url": "https://www.byprog.com/fr/blog/",
          "description": "Tutoriels et articles autour de l'informatique et des nouvelles technologies",
          "title": "Anthony Sigogne / Freelance / Le Blog"
        }
      },
      {
        "_index": "web",
        "_type": "page",
        "_id": "AV1jHHh9y84g6ALLsHnL",
        "_score": 0.60887885,
        "_source": {
          "url": "https://www.byprog.com/fr/blog/?type=RaspberryPi",
          "description": "Tutoriels et articles autour de l'informatique et des nouvelles technologies",
          "title": "Anthony Sigogne / Freelance / Le Blog"
        }
      },
      {
        "_index": "web",
        "_type": "page",
...

La documentation officielle d’Elasticsearch est très complète, je vous laisse l’explorer en détail.

Le scraper et le crawler modifiés dans ce tutoriel sont disponibles dans mon dépôt github.

Développeur Full-Stack en Freelance, je me donne pour mission d'aider les entreprises et les particuliers pour tous leurs projets et toutes leurs problématiques en informatique. N'hésitez pas à me contacter !