Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modifier masterspider.py risque de casser des spiders existantes #61

Open
DavidBruant opened this issue Feb 3, 2019 · 2 comments
Open

Comments

@DavidBruant
Copy link

Aujourd'hui, une spider est constituée de :

  • sa configuration (dont les XPath)
  • le code de masterspider.py qui tourne avec la configuration en argument

Quand on modifie la configuration ou masterspider.py (ou une dépendance de masterspider.py), on prend le risque de casser l'araignée en question

A priori, une fois que la configuration d'une spider est faite, il n'y a pas de raison de la modifier sauf si le site visé a changé

Par contre, masterspider.py est changé à chaque fois qu'un nouveau cas de site web est trouvé ou à chaque fois qu'un bug lié à ce fichier est trouvé. Quand un tel changement a lieu, il n'est actuellement pas possible facilement de savoir si l'on a cassé une spider existante. On peut seulement l'espérer.
Actuellement, la manière la plus efficace de vérifier que l'on n'a rien cassé est de tester à la main toutes les configurations de spider existantes avec le nouveau code. La détection d'un bug suite à cette vérification peut amener à un nouveau changement sur masterspider.py...

masterspider.py a changé et va encore changer et encore dans le futur, de manières que l'on n'a pas encore prévu

@JulienParis
Copy link
Collaborator

JulienParis commented Feb 7, 2019

Les modifications sur masterspider.py sont effectivement à faire avec précaution toutefois il y a quelques pare-feux en place dans la structure même de cette fonction pour éviter de provoquer des dégâts.

architecture générale du fichier masterspider.py

masterspider.py contient plusieurs objects, les principaux étant la fonction run_generic_spider et une classe GenericSpider.

  • la fonction run_generic_spider a pour but de charger les settings scrappy propres au spider que l'on veut lancer (process = CrawlerRunner( settings = settings )) puis de lancer le spider (une instance de la classe GenericSpider) en tant que fonction parrallèlisée (multithreading).

  • la classe GenericSpider(Spider) hérite des propriétés de la classe Spider de scrappy et construit le spider sur cette base en y adjoignant toutes les propriétés du spider spécifique que l'on veut lancer :

    • lors de l'initialisation ( __init__(...) ) la classe Spider de scrappy est chargée de cette manière : super(GenericSpider, self).__init__(*args, **kwargs)
    • lors de l'initialisation ( __init__(...) ) la configuration du spider vient de ce qui gardé en mémoire dans MongoDB () et est "aplati/sérialisé" dans la variable interne de la classe en tant que self.spider_config_flat.
    • d'autres variables sont chargées à l'initialisation : datamodel, user_id en particulier...
    • une fois l'instance de la classe GenericSpider initialisée certaines fonctions internes sont nécessaires afin de pouvoir bénéficier des fonctions propres à la classe Spider de scrappy :
      • start_request (le load initial de la variable start_url)
      • parse
      • et d'autres que l'on peut customiser : fill_item_from_results_page, parse_detailed_page, get_next_page, clean_data_list, clean_link...

Se baser sur une instanciation générique (GenericSpider) de la classe elle-même générique Spiderde Scrappy permet à la fois de bénéficier des fonctions de scrappy 'out the box' (parsing, requests, itérations, configuration des robots, etc...) et de sa logique de 'pipelines' pour bien différencier ce qui est de l'ordre du crawling et ce qui est de l'ordre de la sauvegarde dans la BDD, les pipelines.

les 'pare-feux' actuels pour éviter toute catastrophe lors de modifications de masterspider.py

Pour éviter qu'un changement dans masterspider.py rende une configuration pré-existante incompatible et caduque les précautions que j'ai prises jusqu'à maintenant sont les suivantes :

1. la structure générale cohérente avec la logique de Scrappy

Les nouvelles fonctions sont écrites à part : soit en 'dehors' de la classe GenericSpider (comme flattenSpiderConfig, clean_xpath_for_reactive, etc...), soit à l'intérieur de la classe (comme get_next_page). Les fonctions 'en dehors' pourront plus tard être mises dans un autre fichier (on cleanerait 150 lignes de codes)

Une fois l'url_start chargée on peut à volonté et en fonction de la configuration du spider :

  • charger les items ( selon le item_xpath configuré),
  • et/ou aller vers la page détaillée de chaque item avec une fonction de callback :

yield scrapy.Request(url, callback=self.parse_detailed_page, meta={ 'item': item, 'start_url' : start_url, 'item_n' : self.item_count , 'parse_api' : follow_is_api })

  • et/ou aller à la page suivante avec une fonction de callback :

yield response.follow(next_page, callback=self.parse, meta={'start_url': start_url} )

2. le contenu de la fonction "interne" parse

La fonction "interne" de la classe GenericSpider jouant un rôle centrale est la fonction parse, dont a besoin l'instance de Spider, c'est dans cette fonction que jouent toutes les conditions... Pour bien séparer les besoins parse possède trois types de comportements bien distingués qui s'activent en fonction de la configuration du scraper :

  1. parser une API (if self.spider_config_flat["parse_api"] == True) ;
  2. parser un site non réactif (if self.spider_config_flat["parse_reactive"] == False)
  3. parser un site réactif (else... )

Les principaux fonctionnements de GenericSpider sont basés sur des cliquets (true|false), et ce "triage" initial (parser une API | un site non réactif | un site réactif) est assez central : en effet ces 3 possibilités de base actionnent chacune des outils spécifiques : pures requêtes et parsing de JSON dans le 1er cas, bibliothèque de requête/parsing de scrappy (urllib) dans le 2eme cas, Selenium dans le 3eme...
Les cas particuliers de sites (réactifs ou pas, infinite scroll ou pas, pages détaillées ou pas, API ou pas... ) sont au final autant de cliquets à activer ou pas à l'intérieur de la fonction parse ou ailleurs dans GenericSpider. De fait ces conditions sont bien isolées les unes des autres et on choisit de les activer dans la configuration du spider côté client.

3. les commentaires

J'ai été peu avare sur les commentaires et les logs de debug dans le fichier masterspider.py, certains logs étant commentés dans le but de pouvoir re-débugger plus simplement en phase de développement. (logs et commentaires expliquant aussi que ce fichier compte autant de lignes de code)

4. le modèle de données des spiders

Les spiders ont un modèle de données relativement stable, bien séparés dans chaque document et à l'image de ce qu'on voir sur l'interface graphique figurent :

  • la partie de la configuration "globale" : start_url, item_xpath...
  • la partie "custom" dépendant du datamodel
  • la partie "advanced settings" pour jouer avec les settings de l'instance Spider

Les valeurs par défaut de tous les spiders de la BDD sont initialisées et vérifiées à chaque fois qu'on relance l'appli, au niveau de la fonction de run main.py
Donc si on veut ajouter de nouvelles conditions paramétrables par l'utilisateur via l'interface, et que ces nouvelles conditions n'existent pas auparavant pour d'autres spiders de la BDD on peut injecter des valeurs par défaut qu'on peut choisir d'activer ou pas ensuite dans parse de GenericSpider


Conclusion temporaire

Sans que cela ne réduise à zéro les risques ces précautions m'ont servi à plusieurs reprises à éviter de mélanger ce qui était de l'ordre de la configuration de spider (très flexible), des variables à disposition pour jouer avec la classe Spider de Scrappy, et enfin ce qui est de l'ordre du plus délicat, cad le contenu de la fonction parse de GenericSpider

On peut pour le moment changer les configurations de spider à sa guise (voire même utiliser la partie "notes" de la config pour garder en mémoire des éléments de config). Le problème n'est pas tant de "casser des spiders" (OpenScraper est même fait pour que ce soit simple de casser, refaire), mais bien lorsqu'il s'agit d'ajouter de nouvelles fonctionnalités (type infinite scroll qui n'est activable que lorsque qu'on utilise Selenium) et que ces nouvelles fonctionnalités demandent d'ajouter de nouvelles variables de configuration "dans le dur"

Un travail de factorisation et d'externalisation des fonctions critiques est certainement nécessaire afin d'épurer GenericSpider, toutefois le but de cette classe (nodale dans OpenScraper) est bien de pouvoir s'adapter à la configuration de spider proposée par le client, et de gérer les conditions|erreurs|variables sans faire bugger l'appli dans son ensemble.

@JulienParis
Copy link
Collaborator

JulienParis commented Feb 7, 2019

modif de masterspider.py (+main.py, +settings_corefields.py) pour gérer le nouveau cas d'infinite scroll sans casser la logique générale de masterspider ni le bon fonctionnement des spiders précédents : fe582d0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants