Que ce soit sur Facebook, Twitter, Instagram ou tout autre média social, beaucoup d’entreprises organisent des jeux concours. Partagez, aimez, tweetez, etc… sont autant de moyens d’atteindre de nouveaux clients/consommateurs en échange d’une chance d’obtenir un lot.

De nombreux sites abusent alors de la confiance des gens, profitant du fait que les tirages ne sont pas publiques (ni même en présence d’un huissier), pour conserver les lots (s’ils existent)…

Dans ce tutoriel nous allons vous présenter le code pour récupérer tous les commentaires, likes et autre d’une publication sur Facebook et Twitter, afin de pouvoir organiser votre propre concours équitable. Le code étant accessible par tout le monde, l’aspect « tirage aléatoire non biaisé » sera indubitable, et on vous encourage à le faire soit en ligne soit devant un huissier.

Ce code est celui utilisé par Pensée Artificielle lors de ses concours, qui sont diffusés en live.

Pré-requis

Notre github

L’ensemble du code utilisé dans ce cours est disponible sur le github de Pensée Artificielle.

N’hésitez pas à le télécharger (on le mettra à jour régulièrement), mais attention si vous voulez commiter par la suite : votre apikeys.py ne doit jamais être partagé, il contient des informations sensibles !

Python

Le scraper sera rédigé en Python 3, mais rien ne vous empêche de partir sur du Java si vous le préférez (ou n’importe quel autre langage). Téléchargez la dernière version de Python : 3.6.4.

Dépendances vers d’autres modules

On va utiliser plusieurs modules supplémentaires. Ouvrez une fenêtre de commande et saisissez les lignes suivantes :

pip3 install requests
pip3 install bs4
pip3 install selenium
pip3 install python-oauth2

On va maintenant détailler l’utilité de chacun.

  • Requests est le module « HTTP for Humans« , qui permet de travailler avec des appels HTTP sans que le code devienne illisible
  • BS4, de BeautifulSoup permet – entre autre – de parser efficacement du code HTML. Vous pourrez donc rechercher toutes les balises « img » facilement !
  • Selenium est un framework de test qui autorise l’ouverture d’un navigateur (fantôme ou non) et interagit à notre place. Par exemple, vous pouvez ouvrir la page de Twitter, cliquer sur des boutons, saisir du texte, puis fermer le navigateur très facilement
  • Enfin, Python-OAuth2 est le module grâce auquel nous allons nous identifier auprès des API de Facebook et Twitter

Inscription auprès de Facebook API

Dans notre TP, nous allons utiliser Facebook de deux manières

  1. On va passer par Facebook API
  2. On va utiliser le navigateur pour récupérer les informations voulues

La seconde manière pourrait remplacer totalement la 1 mais est plus lente. Quant à la 1ère, elle ne permet pas d’avoir accès à toutes les informations…

  • Allez sur Facebook Developers et connectez-vous/créez un compte.
  • Ouvrez ensuite « Outil et assistance » puis « Explorateur de l’API Graph ».
facebook developers api key
Génération d’un token d’accès utilisateur Facebook (crédit : Lambert Rosique, screenshot du site de Facebook)

Vous arrivez alors sur une interface de l’API de Facebook. On ne s’en servira pas, mais vous pouvez y faire des tests assez simplement et surtout générer des tokens d’accès sans davantage de configuration :

  • Cliquez sur « Obtenir le token » puis « Obtenir un token d’accès utilisateur »
  • Dans la fenêtre qui s’ouvre, cochez a minima user_posts
  • En cliquant sur « Obtenir un token d’accès », le champ « Jeton d’accès » se remplit : copiez cette clé et mettez-la dans le fichier apikeys.py, dans la variable TOKEN_USER_FB_SECRET.

A noter que cette clé expire après un certain temps ! Si vous souhaitez générer un token d’accès immortel, il faut tout d’abord créer une application (en haut à droite, via Mes apps) puis :

  • Revenez sur « Outil et assistance » et cliquez sur « Outil Token d’accès« 
  • Votre application devrait apparaître, indiquant qu’il faut donner des autorisations (pour le user token)
  • Faites-le et votre token apparaîtra : cliquez à droite sur Débuguer
  • Dans la fenêtre qui s’ouvre, cliquez sur Etendre le token d’accès

Ce token aura alors une durée de vie illimitée, mais cela reste plutôt déconseillé (pour des raisons de sécurité).

Au passage, remplissez aussi dans le fichier apikeys.py votre nom d’utilisateur et mot de passe pour Facebook. Ceux-ci seront utilisés par Selenium lorsqu’il se connectera avec votre navigateur en mode « automatisé ».

Inscription auprès de Twitter API

Pour les mêmes raisons que Facebook, renseignez dans apikeys.py votre nom d’utilisateur et mot de passe pour Twitter. Attention à ne jamais partager ce fichier !

Pour utiliser l’API Twitter, la manipulation est similaire à celle que l’on vient d’effectuer :

  • Allez sur Twitter Developer (même si on n’a rien à y faire, c’était pour vous donner le lien)
  • Allez sur Twitter Apps, le gestionnaire d’application et cliquez sur « Create New App »
  • Remplissez les différents champs (vous n’avez pas besoin de mettre de vraies informations)
  • Vous arrivez alors sur le détail de votre nouvelle application
  • Cliquez sur Manage keys and access token vers le milieu de la page
  • Vous obtenez ainsi votre Consumer Key et Consumer Secret (à copier dans apikeys.py)
  • Cliquez ensuite sur « Create my access token »
  • Vous obtenez votre Access Token et Access Token Secret (à renseigner aussi)

Et voilà, tout est prêt pour commencer à développer !

0. Fichier apikeys.py (pour ceux qui ne l’ont pas)

Ce fichier contient toutes les informations de sécurité, renseignées dans des variables. Il doit être placé au même niveau que le fichier principal de code que nous allons écrire.

A noter également que le « geckodriver.exe » est le programme qui permet de lancer Selenium (navigateur internet), il vous le faudra également (via notre github).

# Fichier apikeys.py
# Renseignez les différents champs et ne partagez jamais ce fichier...

# -*- coding: utf-8 -*-
# FACEBOOK 
TOKEN_USER_FB_SECRET = "Your user token"
FB_USERNAME = "Your username from FB"
FB_PASSWORD = "Your password from FB"
# TWITTER
TW_CONSUMER_KEY = "Twitter consumer key"
TW_CONSUMER_SECRET = "Twitter consumer secret"
TW_ACCESS_KEY = "Twitter access key"
TW_ACCESS_SECRET = "Twitter access secret"
TW_USERNAME = "Your username from Twitter"
TW_PASSWORD = "Your password from Twitter"

I. Récupérer les informations de Facebook

Commençons par les imports, que nous avons détaillé précédemment :

import random as rd
import requests, json
from apikeys import *
import oauth2 as oauth
from selenium import webdriver
import time

Puis définissons un certain nombre de variables qui rendront notre code un peu plus lisible et réutilisable :

TOKEN_FB = "?summary=true&access_token="
FB_RACINE = "https://graph.facebook.com/v2.12/"
FB_ID_PENSEE_ARTIFICIELLE = "113483579338897"
FB_ID_CONCOURS = "158412888179299"

FB_ACCOUNTS = "/me/accounts"
FB_COMMENTS = "/comments"
FB_REACTIONS = "/reactions"
FB_SHARES = "&fields=shares"
FB_CONCOURS = FB_RACINE + FB_ID_PENSEE_ARTIFICIELLE + "_" + FB_ID_CONCOURS

Pour aller récupérer les informations d’un post qui vous appartient (ou celui d’une page que vous gérez, ce qui est notre cas ici), il vous faut deux informations :

  • L’identifiant de votre page (variable FB_ID_PENSEE_ARTIFICIELLE)
  • L’identifiant de votre post (variable FB_ID_CONCOURS)
facebook id page
(crédit : Lambert Rosique)

Le premier est obtenu via l’API Graph de Facebook (celui dont nous avons parlé au début). Allez à cette URL et cliquez à droite sur « Obtenir le token » puis choisissez votre page (dans Jeton d’accès à la page). Cliquez sur Envoyer et vous obtiendrez l’ID.

Pour le second, il suffit d’aller sur le post dans Facebook, et vous aurez l’ID dans l’URL (à la fin : par exemple https://www.facebook.com/penseeartificielle/photos/a.140514526635802.1073741828.113483579338897/158411998179388/?type=3 ).

Génération d’un token d’accès

Ce jeton vous permettra de réaliser tous les appels à Facebook API en tant que votre page ! En effet, contrairement aux jetons utilisateurs, les jetons pages ne peuvent pas être étendus, donc cette méthode évite d’avoir à aller générer manuellement de nouveaux jetons.

def generate_fb_token():
    r = requests.get(FB_RACINE + FB_ACCOUNTS + "?access_token="+TOKEN_USER_FB_SECRET)
    token = json.loads(r.text)['data'][0]['access_token']
    return token

Un simple appel à une adresse, la lecture au format json de la réponse et la récupération d’une donnée précise.

Récupération des commentaires, partages et réactions (like, grr, etc…)

Pour avoir accès à certaines informations globales sur le post, on peut le récupérer via

def get_wanted_post_fb():
    r = requests.get(FB_CONCOURS + TOKEN_FB)
    post_fb = json.loads(r.text)
    return post_fb

Néanmoins, ce qui nous intéresse, c’est la récupération des commentaires pour en élire un.

def get_comments_fb():
    r = requests.get(FB_CONCOURS + FB_COMMENTS + TOKEN_FB)
    comments_fb = json.loads(r.text)
    nb_comments = comments_fb['summary']['total_count']
    return comments_fb, nb_comments

Vous aurez ainsi le nombre total de commentaire avec la liste complète.

Si vous voulez les partages, ça se complique : il n’existe aucun point d’entrée qui permette de les avoir (à cause de la visibilité des partages qui sont « restreints »). La seule solution sera de passer par Selenium (à voir plus loin), mais en attendant on va récupérer le nombre de partages pour notre tirage aléatoire.

def get_shares_fb():
    r = requests.get(FB_CONCOURS + TOKEN_FB + FB_SHARES)
    shares_fb = json.loads(r.text)
    nb_shares = shares_fb['shares']['count']
    return shares_fb, nb_shares

Enfin, pour les réactions c’est aussi simple que pour le reste :

def get_reactions_fb():
    r = requests.get(FB_CONCOURS + FB_REACTIONS + TOKEN_FB)
    reactions_fb = json.loads(r.text)
    nb_reactions = reactions_fb['summary']['total_count']
    return reactions_fb, nb_reactions

Selenium pour les partages

Pour finir, on va récupérer la liste des partages de la publication :

  • On va lancer un robot via le navigateur (Firefox) sur la page de connexion de Facebook
  • Il va saisir votre nom d’utilisateur et mot de passe
  • Puis va accéder à votre page et à la publication demandée
  • A partir de là, il va cliquer sur le bouton « Partages » et lire (via le code HTML) tous les noms des personnes qui ont partagé !
  • Le tout sans que l’on ait besoin de cliquer sur quoi que ce soit !

Voici le code pas à pas.

Lancement de Firefox :

def recuperer_partages_fb():
    url = "https://www.facebook.com/login"
    driver = webdriver.Firefox()
    driver.get(url)
    time.sleep(1)

Saisie du mot de passe : on a accès à deux méthodes pratiques, qui sont « find by… » et « send keys… ».

Find by permet de trouver un ou des éléments correspondant au critère demandé (ici une balise class) tandis que send keys remplit le champ demandé.

A noter qu’il faut être très vigilants lorsqu’on choisit sa méthode pour détecter un champs. En effet, beaucoup de balises sont générées à la volée par Facebook et changent d’une session à l’autre ! De manière générale, évitez les chemins absolus et toute balise comportant des caractères mélangés (par exemple class= »j_UfeZF »).

    USERNAME = "#email"
    PASSWORD = "#pass"
    LOGIN = "#loginbutton"
    username = driver.find_element_by_css_selector(USERNAME)
    password = driver.find_element_by_css_selector(PASSWORD)
    username.send_keys(FB_USERNAME)
    password.send_keys(FB_PASSWORD)
    time.sleep(1)
    submit = driver.find_element_by_css_selector(LOGIN)
    submit.click()
    time.sleep(1)

Ouverture des partages : on a besoin de scroller sur la page car toute la liste n’est pas chargée (c’est souvent le cas avec Facebook qui utilise un scroll infini). On demande alors à notre driver d’exécuter un script inclus dans Selenium qui réalise cette action, et on le fait plusieurs fois car Facebook charge par « paquets de scroll » et non par « longueur du scroll ».

    url = "https://www.facebook.com/penseeartificielle/posts/158412888179299"
    driver.get(url)
    time.sleep(1)
    SHARES = 'UFIShareLink'
    shares = driver.find_elements_by_class_name(SHARES)
    shares[0].click()
    time.sleep(1)
    for __ in range(10):
        # multiple scrolls needed to show all 400 images
        driver.execute_script("window.scrollBy(0, 1000000)")
        time.sleep(0.2)
    time.sleep(1)

Liste de tous les partages : lorsque vous utilisez un « find by » il est important de garder à l’esprit qu’il s’applique aux sous-éléments de l’objet auquel vous l’appliquez. Dans le code suivant, le find_element_by_xpath ne cherchera les span de span que dans les balises h5 !

    liste = driver.find_elements_by_tag_name("h5")
    liste = liste[1:] #Remove 1st element which is original publication
    
    liste_partages = []
    for w in liste:
        w = w.find_element_by_xpath(".//span/span").text
        liste_partages.append(w)
    return liste_partages

II. Travailler avec Twitter

Cette partie va fortement ressembler à notre travail sur Facebook. On commence par définir nos variables :

TW_RACINE = "https://api.twitter.com/1.1/statuses"
TW_URL_PENSEEARTIF = "https://twitter.com/PenseeArtif/status"
TW_ID_CONCOURS = "/975328231208509440"
TW_ID_CONCOURS_JSON = TW_ID_CONCOURS+".json"
TW_RETWEETS = "/retweets"
TW_SHOW = "/show"

où TW_ID_CONCOURS est récupéré dans l’URL à partir du tweet qui nous intéresse.

Génération du token d’accès

Contrairement à ce qu’on a vu avant, ici on ne peut pas utiliser de token infini. En revanche, on peut en générer un assez facilement grâce à toutes nos informations :

def generate_tw_token():
    consumer = oauth.Consumer(key=TW_CONSUMER_KEY, secret=TW_CONSUMER_SECRET)
    access_token = oauth.Token(key=TW_ACCESS_KEY, secret=TW_ACCESS_SECRET)
    client = oauth.Client(consumer, access_token)
    return client

Récupération des favoris (likes) et retweets

A l’image de Facebook avec les partages, les favoris d’un tweet ne sont pas accessibles. La principale raison avancée par Twitter est la problématique de performance (par rapport à leur manière de stocker les données).

On peut néanmoins en récupérer le nombre exact grâce au tweet principal :

def get_wanted_tweet_tw(client):
    response, data = client.request(TW_RACINE + TW_SHOW + TW_ID_CONCOURS_JSON)
    tweet_tw = json.loads(data)
    nb_favorites = tweet_tw['favorite_count']
    nb_retweets = tweet_tw['retweet_count']
    return tweet_tw, nb_favorites, nb_retweets

Les retweets en revanche sont lisibles directement :

def get_retweets_tw(client):
    response, data = client.request(TW_RACINE + TW_RETWEETS + TW_ID_CONCOURS_JSON)
    retweets_tw = json.loads(data)
    return retweets_tw

Selenium pour les favoris

Utilisons Selenium cette fois-ci pour récupérer la liste de tous les favoris. La démarche va être identique à la partie sur Facebook :

  • Ouverture du navigateur
  • Connexion
  • Ouverture du tweet
  • Clic sur le bouton des favoris
  • Lecture de la liste

Et le code ne présente aucune surprise…

def recuperer_favorites_tw(url):
    liste_favorites = []
    driver = webdriver.Firefox()
    driver.get(url)
    
    FAVORITES = ".request-favorited-popup"
    CONNEXION = ".SignupDialog-signinLink"
    USERNAME = ".js-username-field"
    PASSWORD = ".js-password-field"
    LISTE = '.activity-popup-users'
    
    time.sleep(1)
    lien_favoris = driver.find_element_by_css_selector(FAVORITES)
    lien_favoris.click()
    time.sleep(1)
    lien_connecter = driver.find_element_by_css_selector(CONNEXION)
    lien_connecter.click()
    time.sleep(1)
    username = driver.find_element_by_css_selector(USERNAME)
    password = driver.find_element_by_css_selector(PASSWORD)
    username.send_keys(TW_USERNAME)
    password.send_keys(TW_PASSWORD)
    time.sleep(1)
    
    lien_favoris = driver.find_element_by_css_selector(FAVORITES)
    lien_favoris.click()
    time.sleep(3)
    ol = driver.find_element_by_css_selector(LISTE)
    for li in ol.find_elements_by_tag_name("li"):
        div = li.find_elements_by_tag_name("div")
        if div is not None and len(div) > 0:
            div = div[0]
            name = div.get_attribute('data-screen-name')
            if name is not None:
                liste_favorites.append(name)
    driver.quit()
    return liste_favorites

III. Aggrégation des données

Une partie extrêmement courte pour résumer les appels à faire une fois toutes nos méthodes/variables déclarées.

Facebook :

TOKEN_FB += generate_fb_token()
post_fb = get_wanted_post_fb()
comments_fb, nb_comments = get_comments_fb()
reactions_fb, nb_reactions = get_reactions_fb()
shares_fb, nb_shares = get_shares_fb()

Twitter :

client = generate_tw_token()
tweet_tw, nb_favorites, nb_retweets = get_wanted_tweet_tw(client)
retweets_tw = get_retweets_tw(client)

IV. Tirage aléatoire

Dans cette partie on va voir (même si c’est subjectif et que chacun est invité à traiter ses données comme il l’entend) comment tirer aléatoirement un grand gagnant parmi tous nos participants.

Pour Pensée Artificielle, on a choisit de suivre le protocole suivant :

  • Récupération de la liste de tous les participants par plateforme (Facebook, Twitter) et type de participation (réaction, commentaire, partage…)
  • Elimination des participations invalides (en fonction des règles imposées) (ici il n’y en avait pas, même si on aurait pu enlever la « participation » de la page ou de l’administrateur)
  • On va alors tirer aléatoirement une plateforme et un type de participation (proportionnellement au nombre de participation).

Par exemple, s’il y a 5 likes et 1 commentaire, on aura 5 chances sur 6 que le gagnant soit dans les likes et 1 chance sur 6 qu’il soit dans les commentaires. Bien évidemment, on pourrait dire que le numéro tiré est directement le grand gagnant, mais on voulait pouvoir découper en deux parties et faire durer le suspens…

  • Parmi ce type de participation, tirage aléatoire du grand gagnant.

Tirage de la plateforme et du type de participation

On affiche les statistiques pour commencer :

def print_statistiques():
    print('Statistiques :')
    print('*** Facebook')
    print('      Likes =',nb_reactions)
    print('      Commentaires =',nb_comments)
    print('      Partages =',nb_shares)
    print('*** Twitter')
    print('      Likes =',nb_favorites)
    print('      Retweets =',nb_retweets)

Puis on génère la liste des résultats, proportionnelle au nombre de participations :

def generate_liste_resultats():
    total = nb_reactions + nb_comments + nb_shares + nb_favorites + nb_retweets
    resultats = ['Facebook like']*nb_reactions + ['Facebook commentaire']*nb_comments + ['Facebook partage']*nb_shares 
    resultats += ['Twitter likes']*nb_favorites + ['Twitter retweet']*nb_retweets
    return total, resultats

Le type gagnant sera donc un élément de cette liste. Pour notre tirage on utilise la fonction random de Python.

def tirage_media_gagnant(resultats):
    tirage = rd.randint(0, total-1)
    print("Tirage aléatoire :",tirage)
    print("Le gagnant fait partie de :",resultats[tirage])
    return tirage

Tirage du grand gagnant

On définit une méthode générique qui redirigera sur le bon tirage :

def grand_gagnant(resultats, tirage, reactions_fb, comments_fb, nb_shares, nb_favorites, retweets_tw):
    if resultats[tirage] == 'Facebook like':
        tirer_reaction_fb(reactions_fb)
    elif resultats[tirage] == 'Facebook commentaire':
        tirer_commentaire_fb(comments_fb)
    elif resultats[tirage] == 'Facebook partage':
        tirer_partage_fb(nb_shares)
    elif resultats[tirage] == 'Twitter likes':
        tirer_favorite_tw()
    elif resultats[tirage] == 'Twitter retweet':
        tirer_retweet_tw(retweets_tw)

Il va nous falloir écrire, maintenant, la méthode pour chaque type de participation (ce qui est simple). On a notre liste de participants, donc on tire un nombre au hasard dans cette liste et on regarde à qui ça correspond.

A noter que Selenium étant assez lent, la récupération des partages Facebook et des favoris Twitter ne se fait que si nécessaire !

def tirer_reaction_fb(reactions_fb):
    tirage = rd.randint(0, len(reactions_fb['data'])-1)
    print("... Le gagnant du concours est :",
          reactions_fb['data'][tirage]['name'],
          '(id :',
          reactions_fb['data'][tirage]['id'],
          ', type :',
          reactions_fb['data'][tirage]['type']+")")

def tirer_commentaire_fb(comments_fb):
    tirage = rd.randint(0, len(comments_fb['data'])-1)
    print("... Le gagnant du concours est :",
          comments_fb['data'][tirage]['from']['name'],
          '(id :',
          comments_fb['data'][tirage]['from']['id'],
          ', le :',
          comments_fb['data'][tirage]['created_time']+")")

def tirer_partage_fb():
    liste_partages = recuperer_partages_fb()
    tirage = rd.randint(1, len(liste_partages))
    print("... Le gagnant du concours est : le partage n°"+str(tirage)+" intitulé "+liste_partages[tirage-1])

Et pour Twitter :

def tirer_favorite_tw():
    liste_favorites = recuperer_favorites_tw(TW_URL_PENSEEARTIF+TW_ID_CONCOURS)
    tirage = rd.randint(1, len(liste_favorites))
    print("... Le gagnant du concours est : le like n°"+str(tirage)+" de @"+liste_favorites[tirage-1])

def tirer_retweet_tw(retweets_tw):
    tirage = rd.randint(0, len(retweets_tw)-1)
    print("... Le gagnant du concours est : @"+retweets_tw[tirage]['user']['screen_name'])

Ainsi, le code final d’appel est :

def print_merci():
    print("Merci à tous pour votre participation ! N'hésitez pas à vous abonner à notre page Facebook / Twitter pour les prochaines fois :)")
            
print_statistiques()
total, resultats = generate_liste_resultats()
tirage = tirage_media_gagnant(resultats)
grand_gagnant(resultats, tirage, reactions_fb, comments_fb, nb_shares, nb_favorites, retweets_tw)
print_merci()

V. Conclusion

podium
(crédit : Good Free PhotosCC0 1.0)

Dans ce tutoriel on a pu voir comment récupérer toutes les informations Facebook associées à une publication et toutes celles de Twitter liées à un tweet spécifique.

Grâce à cela, on a pu facilement tirer au hasard un grand gagnant à notre concours.

Le tirage était totalement aléatoire et équitable, le code est accessible, aucun doute n’est permis sur la véracité et le bien fondé du concours !

N’hésitez pas à nous faire part de vos retours en commentaire, le code n’est pas parfait et évoluera certainement sur Github…

Crédit de l’image de couverture : geraltPixabay License