Comment explorer une page Web avec Scrapy et Python 3

introduction

Le nettoyage Web, souvent appelé exploration Web ou exploration Web, ou «analyse programmée d'une collection de pages Web et de l'extraction de données» est un outil puissant pour travailler avec des données sur le Web.

Avec un scraper Web, vous pouvez exploiter des données sur un ensemble de produits, obtenir un grand corpus de texte ou des données quantitatives, obtenir des données d'un site sans API officielle ou simplement satisfaire votre curiosité personnelle.

Dans ce didacticiel, vous découvrirez les principes de base du processus de grattage et de nettoyage en explorant un jeu de données ludique. Nous utiliseronsBrickSet, un site géré par la communauté qui contient des informations sur les ensembles LEGO. À la fin de ce didacticiel, vous disposerez d’un grattoir Web Python entièrement fonctionnel qui parcourt une série de pages sur Brickset et extrait des données sur les ensembles LEGO à partir de chaque page, en affichant les données sur votre écran.

Le grattoir sera facilement extensible pour que vous puissiez le bricoler et l'utiliser comme base pour vos propres projets en extrayant des données du Web.

Conditions préalables

Pour compléter ce didacticiel, vous aurez besoin d’un environnement de développement local pour Python 3. Vous pouvez suivreHow To Install and Set Up a Local Programming Environment for Python 3 pour configurer tout ce dont vous avez besoin.

[[step-1 -—- creation-a-basic-scraper]] == Étape 1 - Création d'un Basic Scraper

Le grattage est un processus en deux étapes:

  1. Vous trouvez et téléchargez systématiquement des pages Web.

  2. Vous prenez ces pages Web et en extrayez des informations.

Ces deux étapes peuvent être mises en œuvre de différentes manières dans de nombreuses langues.

Vous pouvez créer un grattoir à partir de zéro en utilisantmodules ou des bibliothèques fournies par votre langage de programmation, mais vous devez ensuite faire face à des maux de tête potentiels à mesure que votre grattoir devient plus complexe. Par exemple, vous devrez gérer la simultanéité pour pouvoir analyser plusieurs pages à la fois. Vous voudrez probablement trouver comment transformer vos données récupérées en différents formats tels que CSV, XML ou JSON. Et vous devrez parfois traiter avec des sites qui nécessitent des paramètres et des modèles d’accès spécifiques.

Vous aurez plus de chance si vous construisez votre grattoir au-dessus d’une bibliothèque existante qui gère ces problèmes pour vous. Pour ce tutoriel, nous allons utiliser Python etScrapy pour créer notre scraper.

Scrapy est l’une des bibliothèques de grattage Python les plus populaires et les plus puissantes; Le raclage nécessite une approche «piles incluses», ce qui signifie qu’il gère une grande partie des fonctionnalités communes dont tous les racleurs ont besoin, de sorte que les développeurs n’ont pas à réinventer la roue à chaque fois. Cela permet de gratter un processus rapide et amusant!

Scrapy, comme la plupart des packages Python, est sur PyPI (également connu sous le nom depip). PyPI, le Python Package Index, est un référentiel appartenant à la communauté pour tous les logiciels Python publiés.

Si vous avez une installation Python comme celle décrite dans les prérequis de ce didacticiel, vous avez déjà installépip sur votre machine, vous pouvez donc installer Scrapy avec la commande suivante:

pip install scrapy

Si vous rencontrez des problèmes avec l'installation, ou si vous souhaitez installer Scrapy sans utiliserpip, consultez lesofficial installation docs.

Scrapy étant installé, créons un nouveau dossier pour notre projet. Vous pouvez le faire dans le terminal en lançant:

mkdir brickset-scraper

Maintenant, naviguez dans le nouveau répertoire que vous venez de créer:

cd brickset-scraper

Créez ensuite un nouveau fichier Python pour notre scraper appeléscraper.py. Nous placerons tout notre code dans ce fichier pour ce tutoriel. Vous pouvez créer ce fichier dans le terminal avec la commandetouch, comme ceci:

touch scraper.py

Ou vous pouvez créer le fichier en utilisant votre éditeur de texte ou votre gestionnaire de fichiers graphique.

Nous allons commencer par fabriquer un grattoir très basique reposant sur Scrapy. Pour ce faire, nous allons créer unPython class qui sous-classescrapy.Spider, une classe d'araignée de base fournie par Scrapy. Cette classe aura deux attributs obligatoires:

  • name - juste un nom pour l'araignée.

  • start_urls - unlist d'URL à partir desquelles vous commencez à explorer. Nous allons commencer avec une URL.

Ouvrez le fichierscrapy.py dans votre éditeur de texte et ajoutez ce code pour créer l'araignée de base:

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

Décrivons cela ligne par ligne:

Tout d'abord, nousimportscrapy afin de pouvoir utiliser les classes fournies par le package.

Ensuite, nous prenons la classeSpider fournie par Scrapy et en faisons unsubclass appeléBrickSetSpider. Pensez à une sous-classe en tant que forme plus spécialisée de sa classe parente. La sous-classeSpider a des méthodes et des comportements qui définissent comment suivre les URL et extraire les données des pages qu'elle trouve, mais elle ne sait pas où chercher ni quelles données rechercher. En le classant, nous pouvons lui donner cette information.

Ensuite, nous donnons à l'araignée le nombrickset_spider.

Enfin, nous donnons à notre scraper une seule URL à partir de laquelle commencer:http://brickset.com/sets/year-2016. Si vous ouvrez cette URL dans votre navigateur, vous serez dirigé vers une page de résultats de recherche, affichant la première des nombreuses pages contenant des ensembles LEGO.

Maintenant, testons le grattoir. Vous exécutez généralement des fichiers Python en exécutant une commande telle quepython path/to/file.py. Cependant, Scrapy est fourni avecits own command line interface pour rationaliser le processus de démarrage d'un grattoir. Démarrez votre grattoir avec la commande suivante:

scrapy runspider scraper.py

Vous verrez quelque chose comme ceci:

Output2016-09-22 23:37:45 [scrapy] INFO: Scrapy 1.1.2 started (bot: scrapybot)
2016-09-22 23:37:45 [scrapy] INFO: Overridden settings: {}
2016-09-22 23:37:45 [scrapy] INFO: Enabled extensions:
['scrapy.extensions.logstats.LogStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.corestats.CoreStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 ...
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2016-09-22 23:37:45 [scrapy] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 ...
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2016-09-22 23:37:45 [scrapy] INFO: Enabled item pipelines:
[]
2016-09-22 23:37:45 [scrapy] INFO: Spider opened
2016-09-22 23:37:45 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-22 23:37:45 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-22 23:37:47 [scrapy] DEBUG: Crawled (200)  (referer: None)
2016-09-22 23:37:47 [scrapy] INFO: Closing spider (finished)
2016-09-22 23:37:47 [scrapy] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 224,
 'downloader/request_count': 1,
 ...
 'scheduler/enqueued/memory': 1,
 'start_time': datetime.datetime(2016, 9, 23, 6, 37, 45, 995167)}
2016-09-22 23:37:47 [scrapy] INFO: Spider closed (finished)

C’est beaucoup de rendement, alors décomposons-le.

  • Le racleur a initialisé et chargé des composants et des extensions supplémentaires nécessaires pour gérer la lecture de données à partir d'URL.

  • Il a utilisé l'URL que nous avons fournie dans la listestart_urls et a saisi le HTML, tout comme le ferait votre navigateur Web.

  • Il a transmis ce code HTML à la méthodeparse, qui ne fait rien par défaut. Puisque nous n'avons jamais écrit notre propre méthodeparse, l'araignée se termine juste sans faire aucun travail.

Tirons maintenant quelques données de la page.

[[step-2 -—- extracting-data-from-a-page]] == Étape 2 - Extraction de données à partir d'une page

Nous avons créé un programme très basique qui déroule une page, mais il ne fait pas encore de grattage ou araignée. Donnons-lui quelques données à extraire.

Si vous regardezthe page we want to scrape, vous verrez qu'il a la structure suivante:

  • Un en-tête est présent sur chaque page.

  • Certaines données de recherche de niveau supérieur, notamment le nombre de correspondances, ce que nous recherchons et le fil d'Ariane pour le site.

  • Ensuite, il y a les ensembles eux-mêmes, affichés dans ce qui ressemble à une table ou une liste ordonnée. Chaque ensemble a un format similaire.

Lors de l'écriture d'un grattoir, c'est une bonne idée de regarder la source du fichier HTML et de vous familiariser avec la structure. Voici donc quelques éléments supprimés pour des raisons de lisibilité:

brickset.com/sets/year-2016
  

Gratter cette page est un processus en deux étapes:

  1. Commencez par saisir chaque ensemble LEGO en recherchant les parties de la page contenant les données souhaitées.

  2. Ensuite, pour chaque ensemble, récupérez les données souhaitées en les extrayant des balises HTML.

scrapy récupère les données en fonction desselectors que vous fournissez. Les sélecteurs sont des modèles que nous pouvons utiliser pour trouver un ou plusieurs éléments sur une page afin de pouvoir ensuite utiliser les données contenues dans l'élément. scrapy prend en charge les sélecteurs CSS ou les sélecteursXPath.

Nous allons utiliser les sélecteurs CSS pour le moment car CSS est l’option la plus simple et la solution idéale pour rechercher tous les ensembles de la page. Si vous regardez le code HTML de la page, vous verrez que chaque ensemble est spécifié avec la classeset. Puisque nous recherchons une classe, nous utiliserons.set pour notre sélecteur CSS. Tout ce que nous avons à faire est de passer ce sélecteur dans l'objetresponse, comme ceci:

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):
            pass

Ce code récupère tous les ensembles de la page et les parcourt pour extraire les données. Maintenant, extrayons les données de ces ensembles afin de pouvoir les afficher.

Un autre regard sur lessource de la page que nous analysons nous indique que le nom de chaque ensemble est stocké dans une baliseh1 pour chaque ensemble:

brickset.com/sets/year-2016

Brick Bank

L'objetbrickset sur lequel nous bouclons a sa propre méthodecss, nous pouvons donc passer un sélecteur pour localiser les éléments enfants. Modifiez votre code comme suit pour localiser le nom de l'ensemble et l'afficher:

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = "brickset_spider"
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
            }

[.note] #Note: la virgule de fin aprèsextract_first() n'est pas une faute de frappe. Nous allons bientôt en ajouter d'autres à cette section. Nous avons donc laissé la virgule pour faciliter l'ajout à cette section plus tard.
#

Vous remarquerez que deux choses se passent dans ce code:

  • Nous ajoutons::text à notre sélecteur pour le nom. C'est un CSSpseudo-selector qui récupère le texteinside de la balisea plutôt que la balise elle-même.

  • Nous appelonsextract_first() sur l'objet retourné parbrickset.css(NAME_SELECTOR) car nous voulons juste le premier élément qui correspond au sélecteur. Cela nous donne unstring, plutôt qu'une liste d'éléments.

Enregistrez le fichier et exécutez à nouveau le grattoir:

scrapy runspider scraper.py

Cette fois-ci, les noms des ensembles apparaissent dans la sortie:

Output...
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Brick Bank'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Volkswagen Beetle'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Big Ben'}
[scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'name': 'Winter Holiday Train'}
...

Continuons à développer cela en ajoutant de nouveaux sélecteurs pour les images, les pièces et les figurines miniatures, ou lesminifigs fournis avec un ensemble.

Jetez un autre regard sur le code HTML d'un ensemble spécifique:

brickset.com/sets/year-2016

Nous pouvons voir quelques choses en examinant ce code:

  • L'image de l'ensemble est stockée dans l'attributsrc d'une baliseimg à l'intérieur d'une balisea au début de l'ensemble. Nous pouvons utiliser un autre sélecteur CSS pour récupérer cette valeur, tout comme nous l'avons fait lorsque nous avons saisi le nom de chaque ensemble.

  • Obtenir le nombre de pièces est un peu plus compliqué. Il existe une balisedt qui contient le textePieces, puis une balisedd qui le suit et qui contient le nombre réel de pièces. Nous utiliseronsXPath, un langage de requête pour parcourir XML, pour saisir cela, car il est trop complexe pour être représenté à l'aide de sélecteurs CSS.

  • Obtenir le nombre de mini-figurines dans un ensemble revient à obtenir le nombre de pièces. Il y a une balisedt qui contient le texteMinifigs, suivi d'une balisedd juste après avec le numéro.

Modifions donc le grattoir pour obtenir cette nouvelle information:

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

Enregistrez vos modifications et exécutez à nouveau le grattoir:

scrapy runspider scraper.py

Vous verrez maintenant ces nouvelles données dans la sortie du programme:

Output2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': '5', 'pieces': '2380', 'name': 'Brick Bank', 'image': 'http://images.brickset.com/sets/small/10251-1.jpg?201510121127'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '1167', 'name': 'Volkswagen Beetle', 'image': 'http://images.brickset.com/sets/small/10252-1.jpg?201606140214'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '4163', 'name': 'Big Ben', 'image': 'http://images.brickset.com/sets/small/10253-1.jpg?201605190256'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'Winter Holiday Train', 'image': 'http://images.brickset.com/sets/small/10254-1.jpg?201608110306'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': None, 'name': 'XL Creative Brick Box', 'image': '/assets/images/misc/blankbox.gif'}
2016-09-22 23:52:37 [scrapy] DEBUG: Scraped from <200 http://brickset.com/sets/year-2016>
{'minifigs': None, 'pieces': '583', 'name': 'Creative Building Set', 'image': 'http://images.brickset.com/sets/small/10702-1.jpg?201511230710'}

Maintenant, transformons ce racloir en une araignée qui suit des liens.

[[step-3 -—- crawling-multiple-pages]] == Étape 3 - Exploration de plusieurs pages

Nous avons réussi à extraire les données de cette page initiale, mais nous n’avons pas progressé pour voir le reste des résultats. L'intérêt principal d'une araignée est de détecter et de parcourir des liens vers d'autres pages et de récupérer des données à partir de ces pages également.

Vous remarquerez que le haut et le bas de chaque page ont un petit carat droit (>) qui renvoie à la page de résultats suivante. Voici le code HTML pour cela:

brickset.com/sets/year-2016

Comme vous pouvez le voir, il y a une baliseli avec la classe denext, et à l'intérieur de cette balise, il y a une balisea avec un lien vers la page suivante. Tout ce que nous avons à faire, c'est de dire au racleur de suivre ce lien s'il existe.

Modifiez votre code comme suit:

scraper.py

class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next a ::attr(href)'
        next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse
            )

Tout d'abord, nous définissons un sélecteur pour le lien «page suivante», extrayons la première correspondance et vérifions si elle existe. Lescrapy.Request est une valeur que nous renvoyons en disant "Hé, explorez cette page", etcallback=self.parse dit "une fois que vous avez obtenu le code HTML de cette page, renvoyez-le à cette méthode afin que nous puissions analyser , extrayez les données et trouvez la page suivante. "

Cela signifie qu’une fois la page suivante passée, nous rechercherons un lien vers la page suivante, et sur cette page, nous rechercherons un lien vers la page suivante, et ainsi de suite, jusqu’à ce que nous ne trouvions pas un lien pour la page suivante. C’est l’essentiel du raclage Web: trouver et suivre des liens. Dans cet exemple, c’est très linéaire; une page contient un lien vers la page suivante jusqu'à ce que nous atteignions la dernière page. Vous pouvez toutefois suivre les liens vers les balises, les autres résultats de recherche ou toute autre URL de votre choix.

Maintenant, si vous enregistrez votre code et lancez l’araignée à nouveau, vous constaterez qu’il ne s’arrête pas une fois qu’il parcourt la première page de jeux. Il continue à parcourir tous les 779 matchs sur 23 pages! Dans l’ensemble, ce n’est pas une énorme quantité de données, mais vous connaissez maintenant le processus par lequel vous trouvez automatiquement de nouvelles pages à gratter.

Voici notre code complet pour ce tutoriel, en utilisant la surbrillance spécifique à Python:

scraper.py

import scrapy


class BrickSetSpider(scrapy.Spider):
    name = 'brick_spider'
    start_urls = ['http://brickset.com/sets/year-2016']

    def parse(self, response):
        SET_SELECTOR = '.set'
        for brickset in response.css(SET_SELECTOR):

            NAME_SELECTOR = 'h1 ::text'
            PIECES_SELECTOR = './/dl[dt/text() = "Pieces"]/dd/a/text()'
            MINIFIGS_SELECTOR = './/dl[dt/text() = "Minifigs"]/dd[2]/a/text()'
            IMAGE_SELECTOR = 'img ::attr(src)'
            yield {
                'name': brickset.css(NAME_SELECTOR).extract_first(),
                'pieces': brickset.xpath(PIECES_SELECTOR).extract_first(),
                'minifigs': brickset.xpath(MINIFIGS_SELECTOR).extract_first(),
                'image': brickset.css(IMAGE_SELECTOR).extract_first(),
            }

        NEXT_PAGE_SELECTOR = '.next a ::attr(href)'
        next_page = response.css(NEXT_PAGE_SELECTOR).extract_first()
        if next_page:
            yield scrapy.Request(
                response.urljoin(next_page),
                callback=self.parse
            )

Conclusion

Dans ce didacticiel, vous avez construit une araignée entièrement fonctionnelle qui extrait des données de pages Web en moins de trente lignes de code. C’est un bon début, mais vous pouvez faire beaucoup de choses amusantes avec cette araignée. Voici quelques manières d’élargir le code que vous avez écrit. Ils vous donneront une expérience pratique de la récupération des données.

  1. Pour le moment, nous analysons uniquement les résultats de 2016, comme vous l'avez peut-être deviné à partir de la partie2016 dehttp://brickset.com/sets/year-2016 - comment analyseriez-vous les résultats d'autres années?

  2. Un prix de vente au détail est inclus pour la plupart des ensembles. Comment extrayez-vous les données de cette cellule? Comment obtiendriez-vous un chiffre brut? Hint: vous trouverez les données dans undt tout comme le nombre de pièces et de minifigs.

  3. La plupart des résultats ont des balises qui spécifient des données sémantiques sur les ensembles ou leur contexte. Comment pouvons-nous les explorer, étant donné qu'il existe plusieurs balises pour un seul ensemble?

Cela devrait suffire à vous faire réfléchir et à expérimenter. Si vous avez besoin de plus d'informations sur Scrapy, consultezScrapy’s official docs. Pour plus d'informations sur l'utilisation des données du Web, consultez notre didacticiel sur"How To Scrape Web Pages with Beautiful Soup and Python 3”.