Open DavidBruant opened 5 years ago
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.
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 :
__init__(...)
) la classe Spider de scrappy est chargée de cette manière : super(GenericSpider, self).__init__(*args, **kwargs)
__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
.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
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 Spider
de 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.
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 :
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 :
item_xpath
configuré),
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 })
yield response.follow(next_page, callback=self.parse, meta={'start_url': start_url} )
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 :
if self.spider_config_flat["parse_api"] == True
) ;if self.spider_config_flat["parse_reactive"] == False
)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.
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)
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 :
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
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.
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 : https://github.com/entrepreneur-interet-general/OpenScraper/commit/fe582d0dfe9c8f7c8f1c9091365cb36f8e18e060
Aujourd'hui, une spider est constituée de :
masterspider.py
qui tourne avec la configuration en argumentQuand on modifie la configuration ou
masterspider.py
(ou une dépendance demasterspider.py
), on prend le risque de casser l'araignée en questionA 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 surmasterspider.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