Scraper un site web protégé par du bannissement d'IP

Blog article image cover
Dans le cadre d’une POC (proof of concept), d’un projet personnel ou d’une étude statistique, vous pouvez être amené à devoir scraper un site web. Le scraping est l’automatisation, via un script, de la récupération de certaines données mises à disposition par un site. Dans cet article nous considérons que vous avez des notions de base en web scraping. 
La législation étant floue et la jurisprudence hésitante en termes de scraping, nous ne pouvons que vous déconseiller de le pratiquer à des fins commerciales mais nous allons néanmoins vous montrer comment mettre en place un système de scraping avec rotation d’IP grâce à Python, Scrapy et Scrapoxy.  

Cas du Client Side Rendering : Exploiter l’API

Pour les sites fonctionnant en client side rendering, vous pouvez essayer d’exploiter directement l’API qui renvoie les données brutes (souvent au format JSON ou XML) à votre navigateur qui, lui, se chargera de les injecter dans le HTML. Pour cela, vous pouvez regarder (dans l'outil de développement de votre navigateur dans la section “network”) les requêtes XHR qui partent lorsque vous rafraîchissez la page web à scraper. Une fois que vous avez trouvé la ou les requêtes qui chargent les données que vous souhaitez récupérer, il ne reste plus qu’à essayer de forger cette même requête au sein d’un script Python (par exemple en utilisant la bibliothèque request). Ainsi, pas besoin de passer par du scraping à proprement parler (pas de headless webdriver, parser, xpath, etc).
Ici par exemple sur le site de IMDB on pourrait essayer d'exploiter leur API GraphQL.
Ce n’est que dans le cas où l’API se révèle trop difficile à exploiter que je sors l’artillerie lourde pour scraper des données affichées grâce à Javascript : Selenium.

Cas du Server Side Rendering

Pour les sites en server side rendering (et fonctionnant sans appel API), vous n’avez pas d’autre choix que de parser le HTML de la page via une bibliothèque de parsing comme Beautiful Soup et d’extraire les données via leur xpath
Si votre projet de scraping est conséquent, je vous conseille de vous tourner vers Scrapy, un framework Python de crawling et de scraping assez complet qui permet notamment de tester vos xpath via un terminal ou d'intégrer divers plugins comme Splash, ce qui permet d’activer le Javascript sur les pages web.

Notre problème : le bannissement d’IP

Parfois les sites utilisent des méthodes de prévention envers les scrapers. L’une des méthodes les plus redoutables qui m'a longtemps donné du fil à retordre est le bannissement d’IP. Le site scrapé analyse en direct les requêtes qu’il reçoit. S’il constate qu’une IP fait trop de requêtes pour être un utilisateur humain lambda, il peut bannir cette IP (pour une durée plus ou moins longue). Ainsi, à la prochaine requête, votre script lèvera une erreur due à une réponse 429 du serveur.

Première solution - mettre des sleep

Pour contrer cette méthode la première solution simple à laquelle on pourrait penser serait de mettre des “sleep” entre nos différentes requêtes pour ralentir l'exécution de notre script et ne pas se faire repérer par le site scrapé. Mais supposons qu’on ait 30 000 pages à scraper avec 30 secondes de sleep entre deux requêtes, il faudrait que le script tourne plus de 10 jours pour arriver au bout de la liste de pages. Cette solution fonctionnera certainement avec une valeur de “sleep” assez grande mais deviendra donc interminable à l'exécution.

Seconde solution - rotation d’IP avec Scrapoxy

Une autre solution beaucoup plus robuste et rapide à l'exécution (car ne contient pas de sleep) est d’utiliser le service open source Scrapoxy. Scrapoxy permet de faire du scraping en redirigeant les requêtes par un ensemble de proxys. Ces proxys sont des instances de serveurs hébergés par le service EC2 d’AWS.

Récapitulatif de la solution étudiée dans cet article

  • Une spider (instance de scraping de Scrapy) se promène de lien en lien (“crawl” un ensemble de pages)
  • Les requêtes de Scrapy vers le serveur du site ciblé ne sont pas effectuées par votre machine directement mais sont redirigées vers des containers EC2 grâce au service Scrapoxy.
  • Dès qu’un container voit son IP se faire bloquer, Scrapoxy relance une nouvelle armée de containers pour remplacer les anciens devenus inutilisables.

Pré-requis :

Pour suivre ce tuto, il vous faut avoir Python et l’utilitaire pip installés sur votre machine.

Création et configuration du compte AWS

Création du compte

Si vous n’en avez pas déjà, créez-vous un compte AWS afin de pouvoir accéder aux différents services disponibles sur la console AWS. En particulier EC2, le service qui fournit des serveurs à la demande et qui sera utilisé par Scrapoxy pour créer les proxys qui redirigeront nos requêtes.  Pour vous créer un compte ça se passe ici.
Remarque : Vous allez devoir mettre vos informations de carte bleue, alors essayez d'être vigilant quant à l'utilisation du service EC2 qui possède un bon Free Tier (750h d’utilisation par mois en cumulé sur toutes vos instances) mais qui n’est pas gratuit indéfiniment. Ainsi si vous avez 10 instances EC2 qui tournent en parallèle, vous commencerez à être facturé au bout de 750 / 10 = 75h. 

Création de credentials IAM

Pour que Scrapoxy ait le droit de gérer pour vous vos instances EC2, il faut que vous génériez un couple de clé access key et secret access key. Pour cela, rendez-vous dans la console AWS, dans vos identifiants de sécurité (ici)
Dépliez la section Access Key et cliquez sur ajouter une clé.  Gardez de côté la valeur de l’access key id. Cliquez ensuite sur “Show secret access key” et notez bien la valeur qu’il vous donne ou télécharger le key file.
Attention : Vous ne pourrez plus l’afficher par la suite. 

Création d’un Security Group

Vous devez aussi créer un groupe de sécurité.  Pour cela allez dans la console AWS sur le service EC2 et plus précisément sur les groupes de sécurité (sur cet URL, en remplaçant le paramètre region par la région que vous utilisez : https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#SecurityGroups)
Remarque : Nous vous conseillons de vous mettre dans la région eu-west-1 
Cliquez sur le bouton “Créer un groupe de sécurité”, puis :  
  • Mettre “forward-proxy” dans les champs nom et description. 
  • Ajoutez une Inbound rule, avec : 
    • type = TCP personnalisé
    • plage de port = 3128
    • source = n’importe où

Choix d’un AMI

Plus tard, lors de la configuration de Scrapoxy, vous allez avoir besoin d’un “AMI”.
Si vous êtes dans la région eu-west-1, vous pouvez choisir l’un de ces trois AMI en fonction de quel type d’instance vous voulez créer pour votre script : 
  • t1.micro ⇒ ami-c74d0db4
  • t2.micro ⇒ ami-485fbba5
  • t2.nano ⇒ ami-06220275
Gardez dans un coin le nom l’AMI choisi pour plus tard !
Conseil : Essayez avec le premier (ami-c74d0db4), qui génère des instances t1.micro (les plus petites possible). En effet on va demander à ces serveur de faire de simples requêtes HTTP ; pas besoin de lancer des machines de guerre.
Si par contre, vous n’êtes pas dans la région eu-west-1, vous devrez suivre ce tutoriel trouvé dans la doc de Scrapoxy pour copier l’AMI ami-c74d0db4 vers une AMI qui vous appartient situé dans votre région.

Installation et configuration de Scrapoxy

Installation de Scrapoxy

Pour installer Scrapoxy veuillez entrer les commandes suivantes dans un terminal : sudo apt-get install build-essential sudo npm install -g scrapoxy

Configuration de Scrapoxy

Pour utiliser Scrapoxy et le lier à votre compte AWS, vous devez générer et remplir un fichier de configuration. Commencez par entrer la commande suivante :  scrapoxy init conf.json
Ensuite dans le fichier conf.json qui vient d’être généré : 
  • changez le password dans la section commander (mettez ce que vous voulez, ce mot de passe servira pour accéder à l’interface graphique de Scrapoxy)
  • dans la section provider, ne conservez que l’objet avec comme type : “awsec2”
  • puis remplacez les accessKeyId et secretAccessKey de cet objet par les credentials récupérés juste avant sur AWS
  • remplacez également la valeur de l’AMI (champ ImageId) par la bonne valeur
Vous n’avez pas besoin de toucher aux paramètres liés au nombre d’instances EC2, vous pourrez régler ça directement via l’interface graphique après avoir lancé Scrapoxy.

Lancement de Scrapoxy en local

Pour lancer une instance de Scrapoxy sur votre machine, entrez dans un nouveau terminal la commande : scrapoxy start conf.json -d
Ce terminal doit rester ouvert durant toute la durée d’exécution du script que nous allons vous présenter juste après.  L’instance de Scrapoxy aura le rôle de dispatcher les requêtes entre les différents containers EC2, d’analyser les réponses, et de reboot les containers quand une requête échoue.
Remarque : Lorsque tourne Scrapoxy, vous pouvez accéder à une interface graphique (GUI) via l'adresse : http://localhost:8889/. De plus, le service sera accessible pour Scrapy via l'adresse : http://localhost:8888/.

Installation et utilisation de Scrapy

Installer Scrapy

Pour installer Scrapy avec l’utilitaire pip, entrez la commande : pip install scrapy scrapoxy

Créer un nouveau projet Scrapy

Pour créer un nouveau projet (une nouvelle “spider”), entrez dans un terminal la commande :  scrapy startproject myscraper
Cette commande créera un dossier myscraper qui aura cette arborescence : 
Créez un dossier data dans le sous-dossier myscraper et un fichier scraper.py dans le sous-dossier spider pour arriver à cette arborescence :

Intégrer Scrapoxy à la Spider

Pour intégrer Scrapoxy à votre spider et que les requêtes soient faites par vos instances EC2 plutôt que par votre machine, dans myscraper/myscraper/settings.py, ajouter les lignes : CONCURRENT_REQUESTS_PER_DOMAIN = 1 RETRY_TIMES = 0 # PROXY PROXY = 'http://127.0.0.1:8888/?noconnect' # SCRAPOXY API_SCRAPOXY = 'http://127.0.0.1:8889/api' API_SCRAPOXY_PASSWORD = 'CHANGE_THIS_PASSWORD' # BLACKLISTING BLACKLIST_HTTP_STATUS_CODES = [ 429 ] DOWNLOADER_MIDDLEWARES = { 'scrapoxy.downloadmiddlewares.proxy.ProxyMiddleware': 100, 'scrapoxy.downloadmiddlewares.wait.WaitMiddleware': 101, 'scrapoxy.downloadmiddlewares.scale.ScaleMiddleware': 102, 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': None, 'scrapoxy.downloadmiddlewares.blacklist.BlacklistDownloaderMiddleware': 950 }
Si le site que vous scrapez renvoie un status code différent de 429 pour indiquer que votre IP a été bloquée, vous pouvez le spécifier en ajoutant la valeur du status code renvoyé par le site dans la variable BLACKLIST_HTTP_STATUS_CODES.

Écriture de la Spider

Passons maintenant au coeur du script de scraping : la spider.
Une spider Scrapy est composée de plusieurs parties : 
  • Import des modules nécessaires au fonctionnement du script (comme pour n’importe quel fichier Python) 
  • Une classe Scraper : contient toute la logique du scraper
    • Des attributs (name, custom_settings, allowed_domains, …)
    • Une première méthode start_requests() qui renvoie un itérable de scrapy.Requests.
    • Une seconde méthode parse() qui sera appelée pour chaque réponse de scrapy.Request et qui a pour mission de parser la réponse et d’en extraire les données voulues. Vous pouvez renvoyer un itérable de dictionnaire par exemple, pour ensuite sauvegarder vos réponses dans un CSV. 
Voici un exemple de code d’une spider avec dans la méthode parse() des exemple de façon de récupérer les données sur une page parsée. Pour en savoir plus, consultez directement la documentation de Scrapy ou renseignez vous sur la notion de xpath (ici).
Vous pouvez copier ce code dans le fichier scraper.py que vous avez créé un peu plus tôt. Ensuite il vous suffit de reprendre le code pour l’adapter à votre site cible. import scrapy import csv class Scraper(scrapy.Spider): name = u'scraper' custom_settings = { 'FEED_FORMAT': "csv", 'FEED_URI': './data.csv', 'FEED_EXPORT_ENCODING': 'utf-8' } allowed_domains = ['www.site-url.com'] start_urls = [f"www.site-url.com/page?id={id}" for id in range(100)] def start_requests(self): total = len(self.start_urls) for i in range(total): print(f"############################## {i}/{total} #############################") url = self.start_urls[i] yield scrapy.Request(url, self.parse, meta={"url": url}) def parse(self, response): # Récupérer l'intégralité du HTML d'une div (N.B.: copier/coller le resultat dans un editeur HTML # pour trouver plus simplement les xpaths des données a collecter) data_0 = response.xpath("//div[@id='main-content']").text # Récupérer l'attribut src d'une balise img data_1 = response.xpath("//div[@id='main-content']/div/div/div/div[2]/div/div/div/div/img/@src").extract_first() # Récupérer les titres de categories (titre present dans l'attribut title de la balise category-div) # et les id des items presents dans la catégorie (ids présents dans l'url de l'item) data_2_raw = response.xpath("//category-div") data_2 = "" for data_2_item in data_2_raw: title = data_2_item.xpath("./@title").extract_first() data_2 += "{}:".format(title) links = data_2_item.xpath("./div/div/div[2]/a/@href").extract() for link in links: data_2 += "{}/".format(link.split("/")[-1].split(".")[0].split("-")[-1]) data_2 = data_2[:-1] data_2 = data_2[:-1] yield { "page_url": response.meta['url'], "data_0": data_0, "data_1": data_1, "data_2": data_2 }

Lancement de la Spider

Pour lancer la spider, exécuter dans un terminal à la racine du dossier myscraper la commande :   scrapy crawl scraper 

Crawling

Lorsque vous lancez la Spider avec Scrapoxy activé sur machine, vous devrez dans un premier temps attendre deux minutes que les containers EC2 soient bien en route. (Vous pouvez désactiver ce temps d’attente pour tester votre script plus facilement via le fichier myscraper/myscraper/settings.py en commentant la ligne 'scrapoxy.downloadmiddlewares.wait.WaitMiddleware': 101)
Surveillez ensuite que tout se passe comme prévu : une série de requêtes qui renvoie des réponses 200 puis, au bout d’un certain temps, une requête qui échoue suite à une réponse 429 suivit d’un sleep de 2-3 minutes pour relancer les containers EC2.
Vous pouvez changer à tout moment le nombre d’instances EC2 voulus via la GUI de Scrapoxy (accessible normalement à l'adresse http://localhost:8889/). Sur ce GUI vous pouvez aussi consulter les stats de vos requêtes depuis le lancement du service.

Conclusion

Si vous devez retenir trois choses de cet article, ce sont les suivantes :
  • Si vous devez scraper les données d'un site, ne partez pas directement sur des outils de scraping évolués ! Essayez dans un premier temps d'exploiter l'API du site. Pour cela, utilisez n'importe quelle librairie de requêtes HTTP de votre langage de prédilection.
  • Si ça ne fonctionne pas, passez par le scraping plus traditionnel (parsing de HTML, headless browser, ...).
  • Si le site scrapé bloque votre IP lorsque vous faites trop de requêtes, deux possibilités s'offrent à vous :
    • Si vous devez scraper un nombre réduit de pages et/ou que le temps d'exécution du script n'est pas un problème, essayez de mettre des "sleep" dans votre code entre les requêtes.
    • Si ça ne fonctionne pas ou que vous devez scraper trop de pages pour que cette technique soit envisageable (temps d'exécution du script trop long), alors, configurez Scrapoxy pour faire tourner l'IP utilisée par votre script pour faire ses requêtes.
Vous souhaitez être accompagné pour lancer votre projet digital ?
Déposez votre projet dès maintenant
Article presentation image
Comment changer de version de Node.js avec NVM ?
Vous voulez changer rapidement de version de `node` ? nvm est l’outil qu’il vous faut. Pourquoi nvm ? `node` est un exécutable. ...
Florian Yusuf Ali
Florian Yusuf Ali
Full-Stack Developer @ Galadrim
Article presentation image
Next.js App Router : le cache et ses dangers
“Il y a seulement 2 problèmes compliqués en informatique : nommer les choses, et l’invalidation de cache”. Phil Karlton. Avec ...
Valentin Gerest
Valentin Gerest
Full-Stack Developer @ Galadrim
Article presentation image
Qu'est-ce que le SMS pumping et comment s'en protéger ?
La fraude appelée SMS pumping survient lorsque des fraudeurs exploitent un champ de saisie de numéro de téléphone de votre ...
Arnaud Albalat
Arnaud Albalat
CTO @ Galadrim