• BLOG

  • Suivre une commande d'achat comme un hacker avec PhantomJS, Selenium, requests et BeautifulSoup (FR)

    mar. 25 avril 2017 Dan Lousqui

    Share on: Twitter - Facebook - Google+

    Pour la petite histoire, j'ai récemment (18/03/2017) commandé une console "Nintendo Switch" sur la boutique Internet d'une grande enseigne de vente (celle au logo doré).

    Pour ceux qui ne savent pas, l'article "Nintendo Switch" a été en rupture de stock, dès son jour de lancement, et l'est encore au moment de la rédaction de cet article (26/04/2017). Ainsi... ça fait plus d'un mois que je patiente la réception de ma commande.

    Edit: finalement, la commande a été préparée à la date de publication de l'article et le script a bien marché :-)

    Du coup, pendant ce temps, j'ai gagné un nouveau passe-temps. Je m'amuse à appuyer sur F5 sur mon navigateur sur deux sites:

    • Un site de jeux vidéo possédant l'un des plus grands forums Internet, avec un fil de discussion dédié au problème de stock de la console sur le site en question ;
    • Le site du revendeur, pour voir si le statut de ma commande avance.

    Etant flemmard, mon sport préféré est l'automatisation de ces tâches :-)

    C'est parti !

    Suivre un thread d'un forum

    Screenshot of script 1

    Le concept

    Nous avons donc:

    • Un fil d'actualité sur un forum ;
    • Plusieurs pages, pour pouvoir afficher les messages;
    • Parmi les informations interessantes par message, nous avons besoin de :
      • La date du message ;
      • Le pseudo de l'auteur d'un message ;
      • Le message !

    Le fonctionnement est le suivant:

    • On initie une liste de message déjà affiché;
    • On initie le numéro de page courante;
    • Toutes les 30 minutes :
      • On Récupère le contenu de la page en cours (avec requests) ;
      • On parse le contenu HTML de la page (avec BeautifulSoup) ;
      • On récupère tous les messages (on sait qu'il s'agit des div avec la class inner-head-content) ;
      • Pour chaque message :
      • On récupère la date ;
      • On récupère l'auteur ;
      • On récupère le contenu ;
      • Si le message ne fait pas parti des messages déjà affichés:
        • On l'affiche ;
        • On l'ajoute à la liste des messages déjà affichés ;
      • Si il y a une page suivante (on sait qu'il s'agit des div avec la class pagi-after):
        • On incrémente le numéro de page actuelle.

    Le script

    Faisons tout ça en python :-)

    from bs4 import BeautifulSoup
    import requests
    import time
    
    viewed = []
    page=218
    
    while True:
      url = "http://www.SUPER_SITE_DE_JEUX_VIDEO.com/forums/42-3007199-50107898-{}-0-1-0-precommandes-livraison.htm"
      html = requests.get(url.format(page)).content
      bs = BeautifulSoup(html, "html.parser")
      messages = bs.findAll("div", {'class':"inner-head-content"})
      for message in messages:
        date = message.find("div", {"class":"bloc-date-msg"}).text.replace("\n","")
        nick = message.find("span").text.replace("\n","").replace(" ","")
        content = message.find("div", {"class":"bloc-contenu"}).text.replace("\n","")
        if date not in viewed:
          viewed.append(date)
          print("{} - {}\n{}\n--------------\n".format(date,nick,content))
      if "Page suivante" in bs.find("div", {'class':"pagi-after"}).text:
        page += 1
      else:
        time.sleep(1800)
    

    Suivre la commande sur un site de revendeur

    Screenshot of script 2

    Le concept

    Cette fois, c'est légèrement plus compliqué, on doit aller sur le site du revendeur pour voir le statut de notre commande. Pour cela, deux solutions sont possibles :

    • On effectue une requête légitime avec un browser, on récupère la requête et on la rejoue avec requests en python ;
    • On crée un browser "virtuel" avec PhantomJSet on effectue tout le scénario (authentification + affichage des commandes).

    Le site du revendeur possède beaucoup de JavaScript, et j'ai peur que l'état de la commande soit lié à la session. Je préfère donc utiliser la deuxième méthode afin d'être sûr d'avoir une information "fraiche" et non celle en cache. De plus, ça fera manipuler PhantomJS :-)

    Le fonctionnement est le suivant:

    • On récupère login / mot de passe de l'utilisateur ;
    • Toutes les 30 minutes :
      • On crée une instance de PhantomJS;
      • On redirige cette instance vers la partie authentifiée du site du revendeur ;
      • L'instance de PhantomJS est automatiquement redirigée vers la mire d'authentification ;
      • On remplit le login (on sait qu'il s'agit d'un input avec l'id Login_Email) ;
      • On remplit le password (on sait qu'il s'agit d'un input avec l'id Login_Password) ;
      • On soumet le formulaire ;
      • On parse le contenu HTML de la page (avec BeautifulSoup) ;
      • On récupère la div avec l'avancement de la dernière commande (on sait qu'il s'agit des div avec la class ma-GaugeWrap) ;
      • On récupère et affiche les étapes passées (on sait qu'il s'agit des div avec la class ma-GaugeStep--passed) ;
      • On récupère et affiche les étapes en cours (on sait qu'il s'agit des div avec la class ma-GaugeStep--current) ;

    Le script

    Faisons tout ça en python :-)

    from selenium.webdriver.common.keys import Keys
    from selenium import webdriver
    from bs4 import BeautifulSoup
    import datetime
    import getpass
    import time
    
    login = "mon@email.com"
    password = getpass.getpass(prompt='Password: ')
    
    while True:
      driver = webdriver.PhantomJS()
      driver.set_window_size(1024, 768)
      driver.get('https://secure.SUPER_BOUTIQUE_EN_LIGNE.com/MyAccount/Order')
      inp = driver.find_element_by_id("Login_Email")
      inp.clear()
      inp.send_keys(login)
      inp = driver.find_element_by_id("Login_Password")
      inp.clear()
      inp.send_keys(password)
      inp.send_keys(Keys.RETURN)
      html = driver.page_source
      soup = BeautifulSoup(html)
      last_command = soup.find("div", {"class":"ma-GaugeWrap"})
      print(str(datetime.datetime.now()))
      for data in last_command.findAll("div", {"class": "ma-GaugeStep--passed"}):
        print(data.text.replace("\n\n\n","").replace("\n\n","-"))
      for data in last_command.findAll("div", {"class": "ma-GaugeStep--current"}):
        print(data.text.replace("\n\n\n","").replace("\n\n","#"))
      time.sleep(1800)
    
  • Comments