Python : MVC et statefull, best Practice

Salut à tous,

Je recopie ici un message que j’ai posté sur openclassrooms : openclassrooms.com/forum/sujet/m … t-practice
J’ai voulu le poster ici cet après-midi, mais le forum me déconnectait sans cesse, me pourrissant ainsi trois messages -_-

Je développe actuellement un script Python qui a pour but de récupérer des informations sur le web, de les traiter, et de les afficher via une interface conviviale. Pour info (mais je ne pense pas que ce soit très utile ici), je code en Python 2.7.9, utilise selenium pour le scraping et wxPython pour l’interface utilisateur.

Je respecte une architecture proche du MVC. Le modèle est en fait remplacé par mon outil de scraping.

J’ai donc trois packages :
[ul][li]gui pour l’interface graphique, divisé en modules pour chaque onglet ;[/li]
[li]controller qui gère le cœur de métier, divisé aussi en modules pour chaque onglet (puisque le cœur de métier et l’interface graphique sont divisés de la même manière ici) ;[/li]
[li]scraper qui sert “d’interface avec le web”, divisé en modules pour chaque site web.[/li][/ul]

Pour la suite, je prends l’exemple d’un projet qui devrait proposer à l’utilisateur une interface simplifiée pour faire des recherches sur le web. Il y aurait trois onglets :
[ul][li]un onglet général qui affiche le premier résultat de DuckDuckGo et le premier résultat de Google, ainsi que la somme du nombre de résultats de chaque moteur de recherche ;[/li]
[li]un onglet DuckDuckGo qui propose un champ pour soumettre les mots clefs de la recherche et affiche le nombre de résultats ainsi que le premier lien ;[/li]
[li]un onglet Google qui fait la même chose pour Google.[/li][/ul]
Je sais, c’est stupide, mais c’est un exemple.

L’architecture du projet serait la suivante :

|- gui
|  |- __init__.py
|  |- general.py
|  |- duckduckgo.py
|  |- google.py
|
|- controller
|  |- __init__.py
|  |- general.py
|  |- duckduckgo.py
|  |- google.py
|
|- scraper
|  |- __init__.py
|  |- duckduckgo.py
|  |- google.py</pre>

Avec les explications précédentes, je pense que cette décomposition paraît logique.

Dans les modules du package “scraper”, chaque méthode renvoit une seule information récupérée sur le web (ex : le titre de la page, la couleur du background, le contenu du premier lien hypertexte, etc.). Lorsque l’utilisateur affiche l’onglet DuckDuckGo, deux méthodes du package “controller” sont appelées (controller.duckduckgo.nb_results(mots_clefs) et controller.duckduckgo.first_result(mots_clefs)). Chacune de ces méthodes va faire appel à une méthode du package “scraper” (respectivement scraper.duckduckgo.nb_results(mots_clefs) et scraper.duckduckgo_first_results(mots_clefs)).

Un petit diagramme de séquence pour que ce soit clair :

_____ _______ _______ | | | | | | | GUI | | Ctrl | | Scrp | |_____| |_______| |_______| | | | | nb_results() | | |----------------->| | | | nb_results() | | |---------------->| | | 120563 | <-- R1 | |<----------------| | 120563 | | |<-----------------| | | | | | | | | first_result() | | |----------------->| | | | first_results() | | |---------------->| | | OCR.com | <-- R2 | |<----------------| | OCR.com | | |<-----------------| | | | |

Le design pattern me semble parfait. Pour ceux qui se poserait la question de l’utilité du contrôleur, il n’est pas évident dans cet exemple. Mais dans l’onglet “Général”, il faut faire la somme du nombre de résultats de DuckDuckGo et de Google. C’est au contrôleur qu’incombe cette tâche.

Dans le diagramme de séquence, j’ai noté deux emplacements ‘R1’ et ‘R2’. Ce sont les opérations qu’effectue le scraper. Tel que je les ai codées, les méthodes du package scraper effectuent les opérations dans cet ordre :
[ul][li]ouverture d’un navigateur ;[/li]
[li]requête GET vers la page qui m’intéresse (il y a aussi du POST dans mon projet, mais on ne s’y intéresse pas ici) ;[/li]
[li]recherche de l’information (nombre de résultats, par exemple) ;[/li]
[li]fermeture du navigateur ;[/li]
[li]renvoie de la valeur trouvée.[/li][/ul]
Ce qui pose donc un problème de performance. ‘R1’ et ‘R2’ ouvrent tous les deux le même navigateur, effectuent une requête GET vers la même page, récupère une information différente et referme le navigateur. Évidemment, effectuer une requête GET demande énormément de temps, et je souhaite les réduire au strict minimum. Encore une fois, pour être clair, voici un diagramme de séquence plus complet :

_____ _______ _______ __________ | | | | | | | | | GUI | | Ctrl | | Scrp | | Browser | |_____| |_______| |_______| |__________| | | | | | nb_results() | | | |----------------->| | | | | nb_results() | | | |---------------->| | | | | get(ddg.com/mot) | | | |--------------------->| | | | <page object> | | | |<---------------------| | | 120563 | | | |<----------------| | | 120563 | | | |<-----------------| | | | | | | | | | | | first_result() | | | |----------------->| | | | | first_results() | | | |---------------->| | | | | get(ddg.com/mot) | | | |--------------------->| | | | <page object> | | | |<---------------------| | | OCR.com | | | |<----------------| | | OCR.com | | | |<-----------------| | | | | | |

J’ai donc pensé à créer une classe dans le module scraper.duckduckgo et y ajouter le navigateur en attribut. Les méthodes de cette classe serait celles du module actuel. La méthode scraper.duckduckgo.classe.nb_results() pourrait donc vérifier si le navigateur est ouvert, et à la bonne page pour essayer de récupérer le nombre de résultats. Mais je me retrouve ici avec deux problèmes supplémentaires :
[ul][li]mon programme devient statefull et je n’aime pas ça, sans compter qu’il y a des Thread qui traînent dans mon projet (fausse excuse me direz-vous, il suffit de coder correctement) ;[/li]
[li]j’aurais une instance de scraper.duckduckgo.classe dans mon contrôleur de DuckDuckGo (controller.duckduckgo), mais les méthodes de controller.general n’ont pas accès à cet objet, et impliquent donc à nouveau de faire une requête GET “inutile” pour obtenir le nombre de résultats de DuckDuckGo (point R3 et R4 dans le diagramme suivant).[/li][/ul]

_____ _____________ ____________ ___________ | | | | | | | | | GUI | | Ctrl (DDG) | | Scrp (DDG) | | Browser | |_____| |_____________| |____________| |___________| | | | | | nb_results() | | | |----------------->| | | | | nb_results() | | | |---------------->| | | | | get(ddg.com/mot) | | | |--------------------->| | | | >page object> | <-- R3 | | |<---------------------| | | 120563 | | | |<----------------| | | 120563 | | | |<-----------------| | | | | | | | | | | | first_result() | | | |----------------->| | | | | first_results() | | | |---------------->| | | | | # Pas d'appel ici | | | | # la page est | | | | # en "cache" | | | | | | | |--| | | | | | lit self.page | | | OCR.com |<-- | | |<----------------| | | OCR.com | | | |<-----------------| | | | | | | | | | Ces trois objets continuent à exister | | | dans le namespace de Ctrl (DDG) | | |________________________________________| | | | _____________ ____________ ____________ ___________ | | | | *| | *| | *| | | Ctrl (gnrl) | | Scrp (Ggle)| | Scrp (DDG) | | Browser | *Nouvelles instances | |_____________| |____________| |____________| |___________| | | | | | | | | | | | nb_results() | | | | |----------------->| | | | | | nb_results() | | | | |---------------->| | | | | 153502 |** | | ** Je passe, on connaît | |<----------------| | | | | | | | | | | | | | | | | | nb_results() | | | |----------------------------------->| | | | | get(ddg.com/mot)| | | |---------------->| | | | 120563 | <-- R4 | | |<----------------| | | 120563 | | | |<-----------------------------------| | | 274065 | | | | |<-----------------| | | | | | | | |

Pour pallier à ce second point, je peux utiliser mon navigateur en tant que variable globale. Tous les modules du package controller appelleraient ainsi de méthodes ayant accès à une seule et même instance de Browser. C’est sûr que mes problèmes actuels sont ainsi résolus. Mais je ne suis pas sûr que ce soit très “Best Practice”, et vive les effets de bord.

Bref, que feriez-vous à ma place ? (“Aller sur 9gag en attendant un miracle” n’est pas une option.)

Merci d’avance pour votre aide, ou toute idée, qui pourrait me faire avancer.

Désolé pour la question un peu stupide, mais j’en suis encore à mes débuts en programmation, et encore plus en orienté objet.
Et désolé aussi pour les fautes de frappe qui se sont fatalement glissées, j’utilise un clavier qwerty…
Et enfin, désolé pour le long post. Voici une instance de pomme de terre.

A+
Duna

PS : En me relisant, je me suis aperçu que je ne fais pas bien la différence entre “navigateur” et “page web”. Pour être plus clair, le module selenium propose une classe “webdriver”. C’est plus ou moins l’équivalent d’un navigateur. D’ailleurs, on peut utiliser Firefox comme webdriver, et voir les actions qu’il effectue (aller sur une page, changer d’onglet, fermer le navigateur, etc.). Du coup, quand je parle de mettre une page en attribut d’une classe, il s’agit en fait du navigateur complet, dans un certain état. Pour récupérer des informations, j’utilise les méthodes de selenium qui permettent de traiter les pages comme du XML. Du coup, dans les diagrammes de séquence, il faudrait modifier les échanges entre le scraper et le navigateur :

____________ ___________ | | | | | Scrp (DDG) | | Browser | |____________| |___________| | | | | | get(ddg.com/mot) | |--------------------->| | Done | |<---------------------| | | | | | elem_by_xpath(//h5) | |--------------------->| | "120563" | |<---------------------| | |
Mais c’est du chipotage, et je ne suis pas sûr que ça apporte grand chose.

Pour info, j’ai résolu mon problème en ajoutant une couche sur la fonction get utilisant un cache. Je ne pense pas que ce soit très “Best practice”, mais le résultat est fonctionnel, joli, performant, et aisément compréhensible à la lecture du code.

Concrètement, j’ai ajouté une couche à la foncion get de selenium. Ce n’est plus le module Scraper qui appelle Browser.get, mais Scraper appelle cached_browser.get. Cette méthode utilise un cache. Si le cache ne peut pas répondre à la demande, la méthode get de selenium est appelée (Browser.get). Le cache est stocké sous forme de dictionnaire (la clef est l’URL de la requête initiale), et l’objet associé est un navigateur (objet Browser). Je vérifie aussi l’URL courante du navigateur qui pourrait avoir changé.

Par contre, attention aux effets de bord. Un navigateur en cache peut avoir la bonne URL, être envoyé en résultat à deux fonctions différentes, modifié par l’une, puis accédée par l’autre…