コード例 #1
0
def annotate():
	sys.path.append(os.path.dirname(__file__))
	chrome_path = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s'
	insta_path = 'http://www.instagram.com/'

	### Ouvre le client SQL. ###
	sqlClient = SqlClient()
	sqlClient.openCursor()

	### Récupère les noms d'utilisateurs à annoter. ###
	users = sqlClient.getUsernameUrls(labeled=False)
	print('Fetched %s users.' % str(len(users)))
	sqlClient.closeCursor()

	for index, user in enumerate(users):

		### Ouvre un nouvel onglet, sur la page Instagram de l'utilisateur à annoter. ###
		webbrowser.get(chrome_path).open(user)
		label = -1

		username = user.split(insta_path)[1]

		### On recommence tant que l'utilisateur n'a pas été annoté par 0 ou par 1. -2 correspond à un utilisateur non trouvé. ###
		while label not in ACCEPTED_VALUES:
			label = input('%s. %s : ' % (str(index), user))

			### Si on a fait une erreur et que l'on veur revenir en arrière. ###
			if label == 'previous':
				if index > 0:
					### Ouvre de nouveau la page précédente. ###
					webbrowser.get(chrome_path).open(users[index - 1])

					labelprevious = -1
					while labelprevious not in ACCEPTED_VALUES:
						labelprevious = input('%s. %s : ' % (str(index - 1), users[index - 1]))
					### Set le label en base. ###
					sqlClient.openCursor()
					sqlClient.setLabel(users[index - 1].split(insta_path)[1], labelprevious)
					sqlClient.closeCursor()
				else:
					print('Cannot go previous first user.')

				### Réouvre la page courante. ###		
				webbrowser.get(chrome_path).open(user)

		### Set le label en base. ###
		sqlClient.openCursor()
		sqlClient.setLabel(username, label)
		sqlClient.closeCursor()

		sqlClient.openCursor()
		testRatio = sqlClient.getTestRatio()
		print(testRatio)
		if testRatio < 0.25:
			sqlClient.setTest(username, True)
		sqlClient.closeCursor()
コード例 #2
0
class User(object):
    """
	Classe utilisateur.
	"""
    def __init__(self):
        """
		__init__ function.
		"""

        ### L'utilisateur hérite de la classe `object`. ###
        super().__init__()

        ### On charge les variables depuis le fichier de config. ###
        self.config = configparser.ConfigParser()
        self.config.read(config_path)

        self.username = ''

        ### Instanciation du client SQL. ###

        self.n_clusters = N_CLUSTERS

        ### Initialisation des features pour l'apprentissage. ###
        self.lastpost = 0
        self.frequency = 0
        self.engagement = 0
        self.followings = 0
        self.followers = 0
        self.usermentions = 0
        self.brandpresence = 0
        self.brandtypes = 0
        self.commentscore = 0

        ### Paramètres de fonctions maths, pour ajustement de scores. ###
        self.K = 0.17
        self.K_ = 7
        self.B = 0.5

    def getUserNames(self, limit=0):
        """
		Récupère les usernames des utilisateurs annotés, sous la forme:

		[
			{"username": "******"},
			{"username": "******"},
			...
		]

		Args:
				limit (int) : La limite du nombre d'utilisateurs à retourner.

		Returns:
				(list) : la liste des noms d'utilisateur issus de la BDD.
		"""
        self.sqlClient = SqlClient()

        ### Récupération des noms d'utilisateurs annotés. ###
        self.sqlClient.openCursor()
        allUsers = self.sqlClient.getUserNames(limit, labeled=True)
        self.sqlClient.closeCursor()
        return allUsers

    def getUserInfoIG(self):
        """
		Récupération des critères de l'utilisateur via l'API d'Instagram.
		Utilisée lorsqu'on veut tester notre modèle en live, sur un utilisateur qui n'est pas forcément en base.

		Args:
				(none)

		Returns:
				(none)
		"""

        igusername = self.config['Instagram']['user']
        igpassword = self.config['Instagram']['password']

        ### Connexion à l'API. ###
        self.InstagramAPI = InstagramAPI(igusername, igpassword)
        self.InstagramAPI.login()
        ### On essaye d'extraire les features du profil Instagram. 								  					 ###
        ### Si il y a une erreur, on pass (on ne veut pas break e script en cas de re-promptage). 					 ###
        ### Les `time.sleep` préviennent des erreurs 503, dues à une sollicitation trop soudaine de l'API Instagram. ###

        self.loadModels()

        username = self.username

        ### On questionne l'API à propos du nom d'utilisateur, cela nous retourne l'utilisateur en entier. ###
        self.InstagramAPI.searchUsername(username)
        user_server = self.InstagramAPI.LastJson['user']

        ########################
        ### AUDIENCE, MEDIAS ###
        ########################

        self.followings = int(user_server['following_count'])
        self.followers = int(user_server['follower_count'])
        self.usermentions = int(user_server['usertags_count'])
        self.nmedias = int(user_server['media_count'])
        self.biography = str(user_server['biography'])
        if 'category' in user_server:
            self.category = str(user_server['category'])
        else:
            self.category = ''
        self.is_verified = str(user_server['is_verified'])

        ### On récupère le feed entier de l'utilisateur, afin d'analyser certaines métriques. ###
        self.InstagramAPI.getUserFeed(user_server['pk'])
        self.feed = self.InstagramAPI.LastJson['items']

        if len(self.feed) == 0:
            print('This user has no posts !')
            return

        ### On affiche la longueur du feed retourné par l'API. ###
        print('Feed is %s post-long' % str(len(self.feed)))

        ### On initialise les listes utiles pour l'étude. ###
        self.initLists()

        ### On boucle sur le feed afin d'en extraire les données pertinentes pour le calcul de nos features. 					       ###
        ### On prend l'intégralité de la première réponse de l'API (~ 12 - 18 posts) pour ne pas avoir un temps d'éxécution trop long. ###
        for post in tqdm(self.feed):  ### ICI CA PEUT PETER

            ###################################
            ### LIKES, COMMENTS, ENGAGEMENT ###
            ###################################

            ### Ajout du nombre de likes et de commentaires pour moyenner sur le feed. ###
            self.likeslist.append(post['like_count'])
            self.commentslist.append(post['comment_count'])

            ### On ajoute le timestamp du post pour les analyses de fréquence. ###
            self.timestamps.append(int(post['taken_at']))

            self.addEngagementRate(n_likes=post['like_count'],
                                   n_comments=post['comment_count'])

            ##############
            ### IMAGES ###
            ##############

            ### Depuis le JSON de réponse de l'API, on récupère l'adresse URL de la plus petite image. ###
            url = get_post_image_url(post)

            ### On fait une requête HTTP.GET sur l'adresse récupérée, puis on entrait les octets de l'image en réponse. ###
            response = requests.get(url)
            self.imageAnalysis(response.content)

            ##############
            ### BRANDS ###
            ##############

            ### Fetch les marques détectées dans les posts. ###
            brpsc = self.getBrandPresence(post)
            if brpsc:
                self.brpscs.extend(brpsc)

            ################
            ### COMMENTS ###
            ################

            ### On récupère le score de commentaires sur tout le feed de l'utilisateur. ###
            self.InstagramAPI.getMediaComments(str(post['id']))
            comments_server = self.InstagramAPI.LastJson

            if 'comments' in comments_server:
                comments = [
                    comment['text'] for comment in comments_server['comments']
                ]
            else:
                comments = list()

            self.addCommentScore(comments)

        ################
        ### FEATURES ###
        ################

        ### Assignation des critères et affichage des résultats. ###
        self.extractFeatures()

        self.printFeatures()

    def getUserInfoSQL(self):
        """
		On récupère les posts de l'utilisateur à partir de la BDD, et on en extrait les features nécessaires pour l'apprentissage.
		L'intérêt de cette méthode est qu'on peut solliciter la BDD très vite par rapport à l'API Instagram, ce qui nous permet de faire un
		apprentissage 'rapide'!
		La méthode est cependant très similaire à `self.getUserInfoIG()`.

				Args:
						(none)

				Returns:
						(none)
		"""
        self.sqlClient = SqlClient()

        self.sqlClient.openCursor()
        posts = self.sqlClient.getUserPosts(self.username)

        ###	Initialisation des listes de stockage pour les métriques. ###
        self.initLists()
        self.loadModels()

        ########################
        ### AUDIENCE, MEDIAS ###
        ########################

        self.followings = int(posts[0]['n_following'])
        self.followers = int(posts[0]['n_follower'])
        self.usermentions = int(posts[0]['n_usertags'])
        self.nmedias = int(posts[0]['n_media'])
        self.biography = str(posts[0]['biography'])
        self.category = str(posts[0]['category'])
        self.is_verified = posts[0]['is_verified']

        self.feed = posts

        for post in posts:

            self.likeslist.append(post['n_likes'])
            self.commentslist.append(post['n_comments'])

            #######################################
            ### TIMESTAMPS ET TAUX D'ENGAGEMENT ###
            #######################################

            self.timestamps.append(int(post['timestamp']))
            self.addEngagementRate(n_likes=post['n_likes'],
                                   n_comments=post['n_comments'])

            ##############
            ### IMAGES ###
            ##############

            img = post['image']

            self.imageAnalysis(img)

            ##############
            ### BRANDS ###
            ##############

            ### Pour l'instant on ne s'en sert pas, à ré-utiliser quand on s'intéressera à la détection des placements de produits. ###
            """
			brpsc = self.getBrandPresence(post)
			if brpsc:
				brpscs.extend(brpsc)
			"""

            ################
            ### COMMENTS ###
            ################

            # On récupère les commentaires depuis la BDD, grâce à l'ID du post.
            self.sqlClient.openCursor()
            comments = self.sqlClient.getComments(str(post['id_post']))
            self.sqlClient.closeCursor()

            comments_only = [comment['comment'] for comment in comments]

            ### On parcourt les commentaires du post pour en extraire le "score de commentaires". ###
            self.addCommentScore(comments_only)

        ### Dernière phase: on affecte les variables d'instance (= features) une fois que tous les critères ont été traités. ###

        ################
        ### FEATURES ###
        ################

        self.extractFeatures()

        ### Ici, on cherche à avoir la distorsion des k-means des couleurs du feed. 						###
        ### Parfois, on peut avoir que une ou deux couleurs outputées du k-mean.                            ###
        ### Si on a une erreur, on baisse le nombre de clusters jusqu'à ce que le k-mean puisse être opéré. ###

        self.label = int(post['label'])
        self.testset = post['test_set']

    def loadModels(self):
        """
		Charge les modèles pré-enregistrés pour l'analyse de l'utilisateur.

				Args:
					None
				
				Returns:
					None
		"""

        if not os.path.isfile(os.path.join(comments_model_path)):
            print('Creating comments model...')

            ### Crée le modèle de commentaires s'il n'existe pas. ###
            self.createCommentsModel()

        if not os.path.isfile(os.path.join(biographies_model_path)):
            print('Creating biographies model...')

            ### Crée le modèle de biographies s'il n'existe pas. ###
            self.createBiographiesModel()

        ### Charge le modèle de commentaires. ###
        self.comments_model = pickle.load(open(comments_model_path, 'rb'))

        ### Charge le modèle de biographies. ###
        self.count_vect, self.tfidf_transformer, self.clf = pickle.load(
            open(biographies_model_path, 'rb'))

    def initLists(self):
        """
		Initialise les listes de stockage utilisées pour l'étude du profil.

				Args:
					None
				
				Returns:
					None
		"""
        self.rates = list()
        self.timestamps = list()
        self.comment_scores = list()
        self.brpscs = list()
        self.colorfulness_list = list()
        self.dominant_colors_list = list()
        self.contrast_list = list()
        self.likeslist = list()
        self.commentslist = list()

    def addEngagementRate(self, n_likes, n_comments):
        """
		Ajoute le taux d'engagement du post à la liste des taux d'engagements.

				Args:
					- n_likes (int) : le nombre de likes du post.
					- n_comments (int) : le nombre de commentaires du post.

				Returns:
					None 
		"""

        ### Si l'utilisateur n'a pas de followers, on considère que le taux d'engagement est 0 (au lieu d'infini). ###
        if self.followers == 0:
            engagement_rate = 0

        else:
            ### Pourcentage du taux d'engagement. ###
            k = 100 / self.followers

            ### On distingue plusieurs cas: celui où il y a commentaires et likes, celui où il en manque un des deux, et celui où il n'y a rien. ###
            if n_likes and n_likes > 0:
                if n_comments and n_comments > 0:
                    engagement_rate = (int(n_likes) + int(n_comments)) * k
                else:
                    engagement_rate = int(n_likes) * k

            else:
                if n_comments and n_comments > 0:
                    engagement_rate = int(n_comments) * k
                else:
                    engagement_rate = 0

        ### On ajoute le taux d'engagement du post à la liste de taux d'engagement. ###
        self.rates.append(engagement_rate)

    def imageAnalysis(self, imageIO):
        """
		Effectue une analyse des images du feed de l'utilisateur à partir de l'URL donnée.
		On récupère le code binaire des images, et on y opère les traitements :
		- STD du contraste
		- STD de l'intensité colorimétrique
		- Distorsion des clusters de couleur

				Args:
					- url (str) : l'URL de l'image, à aller récupérer.
				
				Returns:
					None
		"""

        try:
            img = Image.open(BytesIO(imageIO))

            ### On convertit l'image en N&B pour l'étude du contraste. ###
            grayscale_img = img.convert('LA')

            ### On ajoute la couleur dominante du post pour une analyse colorimétrique. ###
            most_dominant_colour = self.getMostDominantColour(img)
            self.dominant_colors_list.append(most_dominant_colour)

            ### On récupère le taux de colorité de l'image, qu'on ajoute à la liste globale si cette première n'est pas nan. ###
            colorfulness = self.getImageColorfulness(img)
            self.colorfulness_list.append(colorfulness)

            ### On récupère le taux de contraste de l'image, qu'on ajoute à la liste globale si cette première n'est pas nan. ###
            contrast = self.getContrast(grayscale_img)
            if not math.isnan(contrast):
                self.contrast_list.append(contrast)
        except Exception as e:
            print(e)

    def addCommentScore(self, comments):
        """
		Génère le score de commentaires pour le post et l'ajoute à la liste de scores de commentaires.

				Args:
					comments (str[]) : la liste des commentaires du post.
				
				Returns:
					None
		"""
        ### On cherche tous les commentaires retournés dans la variable `comments`. ###
        for comment in comments[:10]:
            ### On ne prend que les 10 premier commentaires pour chaque post. ###

            score = self.getCommentScore(comment)
            self.comment_scores.append(score)

    def extractFeatures(self):
        """
		Extrait les features relatives à l'étude.

			Args:
				None

			Returns:
				None
		"""

        self.lastpost = time.time() - max(self.timestamps)
        self.frequency = self.calculateFrequency(len(self.feed),
                                                 min(self.timestamps))
        self.engagement = mean(self.rates)
        self.avglikes = mean(self.likeslist) if len(self.likeslist) > 1 else 0
        self.avgcomments = mean(
            self.commentslist) if len(self.commentslist) > 1 else 0
        self.brandpresence = self.brpscs
        self.brandtypes = self.getBrandTypes(self.brpscs)
        self.commentscore = mean(self.comment_scores) * (1 + stdev(
            self.comment_scores)) if len(self.comment_scores) > 1 else 0
        self.biographyscore = self.getBiographyScore(self.biography)
        self.colorfulness_std = stdev(
            self.colorfulness_list) if len(self.colorfulness_list) > 1 else 0
        self.contrast_std = stdev(
            self.contrast_list) if len(self.contrast_list) > 1 else 0
        self.colors = [[color.lab_l, color.lab_a, color.lab_b]
                       for color in self.dominant_colors_list]
        self.colors_dispersion = self.calcCentroid3d(self.colors)

        while True:
            try:
                if (self.n_clusters == 0):
                    break
                self.codes, self.color_distorsion = scipy.cluster.vq.kmeans(
                    np.array(self.colors), self.n_clusters)
            except Exception as e:
                self.n_clusters = self.n_clusters - 1
                continue
            break

    def printFeatures(self):
        """
		Affiche les différents critères de l'étude.

			Args:
				None
			
			Returns:
				None
		"""

        print('Username : %s' % self.username)
        print('Is verified: %s' % str(self.is_verified))
        print('Category: %s' % str(self.category))
        print('N media: %s' % str(self.nmedias))
        print('Last post: %s' % self.uiGetIlya(max(self.timestamps)))
        print('Frequency: %.2f' % float(self.frequency))
        print('Engagement: %.2f%%' % float(self.engagement))
        print('Average like count: %.2f' % float(self.avglikes))
        print('Average comment count: %.2f' % float(self.avgcomments))
        print('N followings: %s' % self.uiFormatInt(self.followings))
        print('N followers: %s' % self.uiFormatInt(self.followers))
        print('User mentions: %s' % self.uiFormatInt(self.usermentions))
        print('Brand presence: %s' % str(self.brandpresence))
        print('Brand types: %s' % str(self.brandtypes))
        print('Biography score: %.2f' % float(self.biographyscore))
        print('Comments score: %.2f' % float(self.commentscore))
        print('Colorfulness standard deviation: %.2f' %
              float(self.colorfulness_std))
        print('Contrast standard deviation: %.2f' % float(self.contrast_std))
        print('Overall color distorsion : %.2f' % float(self.color_distorsion))

    def uiFormatInt(self, n):
        """
		Conversion du nombre de followers/abonnements en K (mille) et M (million).

		Args:
				n (int) : nombre à convertir en format souhaité.

		Returns:
				(str) Un string formatté.
		"""

        if n > 1000000:
            return '{:.1f}M'.format(n / 1000000)
        elif n > 1000:
            return '{:.1f}K'.format(n / 1000)
        return str(n)

    def uiGetIlya(self, _time):
        """
		Génération du string (exemple) :
		Il y a 15 jours, 0 heure, 36 minutes et 58 secondes.

		Args:
				_time (int) le temps en format POSIX.

		Returns:
				(str) Un string formatté.
		"""

        ### Différence de time POSIX (donc des secondes) entre le temps considéré, et la date actuelle. ###
        ilya = math.floor(time.time() - _time)

        ### Définition des mutliplicateurs jours, heures, secondes. ###
        days_mult = 60 * 60 * 24
        hours_mult = 60 * 60
        minutes_mult = 60

        ### Conversion et troncage des jours, heures, secondes pour le formatttage en chaine de caractère. ###
        days = ilya // days_mult
        hours = (ilya - days * days_mult) // hours_mult
        minutes = (ilya - days * days_mult -
                   hours * hours_mult) // minutes_mult
        seconds = ilya % minutes_mult

        ### On retourne les valeurs en prenant en compte les singuliers et pluriels. ###
        return '%s day%s, %s hour%s, %s minut%s, %s second%s' % (
            days, '' if days in [0, 1] else 's', hours, '' if hours in [0, 1]
            else 's', minutes, '' if minutes in [0, 1] else 's', seconds,
            '' if seconds in [0, 1] else 's')

    def calculateFrequency(self, n, min_time):
        """
		Calcul de la fréquence de post.

		Args:
				n (int) : le nombre de posts à considérer dans l'intervalle de temps donné.
				min_time (int) : le plus vieux post, à comparer avec la date actuelle.

		Returns:
				(int) La fréquence de post.
		"""

        ### Différence de time POSIX (donc des secondes) entre le post le plus ancien, et la date actuelle. ###
        ilya = math.floor(time.time() - min_time)

        ### Calcul de la fréquence de post. ###
        days = ilya // (60 * 60 * 24)
        days = days if days != 0 else 1
        return float(n / days)

    def calcCentroid3d(self, _list):
        """
		Calcul des distances de tous les points au barycentre des points de couleur dans le repère lab*.

		Args:
				_list ((int * 3)[]) : liste de points 3D dont on veut calculer le barycentre.

		Returns:
				(int) La distance moyenne de tous les points du nuage au barycentre de ce dernier.
		"""

        arr = np.array(_list)
        length = arr.shape[0]
        sum_x = np.sum(arr[:, 0])
        sum_y = np.sum(arr[:, 1])
        sum_z = np.sum(arr[:, 2])

        ### Calcul des coordonnées du barycentre. ###
        centroid = np.array([sum_x / length, sum_y / length, sum_z / length])

        ### Calcul des distances de tous les points au barycentre. ###
        distances = [np.linalg.norm(data - centroid) for data in _list]
        return float(mean(distances))

    def getMostDominantColour(self, image):
        """
		Retourne la couleur dominante de l'image.

				Args:
						image (Image PIL) : l'image qu'on considère pour l'étude.

				Returns:
						(tuple) La couleur dominante de l'image dans le repère lab*.
		"""

        ### Définition du nombre de clusters pour les pixels. ###
        NUM_CLUSTERS = 5

        ### On resize l'image pour que les temps de traitement soient réduits. ###

        ar = np.array(image)
        shape = ar.shape
        ar = ar.reshape(scipy.product(shape[:2]), shape[2]).astype(float)

        ### On opère un K-mean sur les pixels de l'image. ###
        codes, dist = scipy.cluster.vq.kmeans(ar, NUM_CLUSTERS)
        vecs, dist = scipy.cluster.vq.vq(ar, codes)
        counts, bins = scipy.histogram(vecs, len(codes))
        index_max = scipy.argmax(counts)

        ### La couleur la plus importante de l'image, déduite du décompte de l'histogramme des couleurs. ###
        peak = codes[index_max]

        ### Conversion de la couleur RGB dans l'espace lab*. ###
        rgb = sRGBColor(*peak)
        return convert_color(rgb, LabColor)

    def getImageColorfulness(self, image):
        """
		Retourne l'intensité colorimétrique de l'image.

				Args:
						image (Image PIL) : l'image qu'on considère pour l'étude.

				Returns:
						(int) L'intensité colorimétrique de l'image.
		"""

        ar = np.array(image)

        ### Transforme l'image pour OpenCV. ###
        open_cv_image = ar[:, :, ::-1].copy()

        ### Performe une analyse des composantes RGB de l'image. ###
        B, G, R, *A = cv2.split(open_cv_image.astype("float"))
        rg = np.absolute(R - G)
        yb = np.absolute(0.5 * (R + G) - B)
        (rbMean, rbStd) = (np.mean(rg), np.std(rg))
        (ybMean, ybStd) = (np.mean(yb), np.std(yb))
        stdRoot = np.sqrt((rbStd**2) + (ybStd**2))
        meanRoot = np.sqrt((rbMean**2) + (ybMean**2))
        return float(stdRoot + (0.3 * meanRoot))

    def getContrast(self, img):
        """
		Retourne le contraste global de l'image en passant par un calcul d'entropie.

				Args:
					img (Image PIL) : l'image N&B qu'on considère pour l'étude (matrice d'entiers allant de 0 à 255).
				
				Returns:
					(int) Le contraste de l'image.
		"""

        ### Transforme l'image pour OpenCV. ###
        ar = np.array(img)

        ### Histogramme de l'image (niveaux de gris). ###
        hist = np.histogram(ar)
        data = hist[0]

        ### Normalisation de l'histogramme. ###
        data = data / data.sum()

        ### On retourne le calcul de l'entropie. ###
        return float(-(data * np.log(np.abs(data))).sum())

    def getBrandPresence(self, post):
        """
		Retourne les mentions utilisateur qui matchent avec les utilisateurs mentionnés dans la description du post.
		
				Args:
					post (dict) : un post Instagram sous forme de dictionnaire.

				Returns:
					(str[]) Le tableau contenant les marques détectées.
		"""

        ### Définition de la liste de stockage des marques. ###
        brands = list()

        ### On essaye de voir s'il y a des marques dans le champ du post. 									  ###
        ### Pour cela, on compare les mentions utilisateurs dans la description du post :                     ###
        ### Les mentions dans la descriptions sont activées par un '@utilisateur'.                            ###
        ### On compare ces dernières avec les utilisateurs tagués sur la photo.                               ###
        ### Si les deux existent, on considère que l'utilisateur a cherché à mettre le profil tagué en avant. ###
        ### S'il n'y a pas de marques, on a une erreur; on pass. 		                                      ###
        try:
            text = post['caption']['text']

            ### On ajoute un '@' pour matcher avec les mentions trouvées dans la description du post. ###
            if hasattr(post, 'usertags'):
                usertags = [
                    '@%s' % user['user']['username']
                    for user in post['usertags']['in']
                ]

                ### On match les mentions utilisateur dans la description du post. ###
                matches = regex.findall(r'@[\w\.]+', text)

                ### Si il y a un utilisateur qui se retrouve à la fois tagué sur la photo et en description du post, alors on l'extrait. 			###
                ### Ce modèle est perfectible, on peut aussi décider de s'occuper uniquement des personnes taguées et/ou des personnes mentionnées. ###
                for match in matches:
                    if match in usertags:
                        brands.append(match.split('@')[1])

            return brands

        except:
            pass

    def getBrandTypes(self, brands):
        """
		Retourne les types de business que sont les 'marques' mentionnées à la fois dans la description du post et en mention utilisateur.

				Args:
					brands (str[]) : le tableau des utilisateurs détectés.
				
				Returns:
					(Counter) Le compteur de types de profils utilisateur (Blog, Photographe, Acteur, etc.), si le type de compte est 'Business' seulement.
		"""

        ### Définition du compteur de marques. ###
        brand_counter = Counter()

        ### On itère sur les noms de marques. ###
        for brand in brands:

            ### On récupère le type de compte (si le compte est de type 'Business' seulement). ###
            self.InstagramAPI.searchUsername(brand)
            brand_full = self.InstagramAPI.LastJson['user']

            ### Si l'utilisateur est 'Business'. ###
            if 'category' in brand_full:
                brand_counter[brand_full['category']] += 1
        return brand_counter

    def getBiographyScore(self, bio):
        """
		Retourne le score de biographie basé sur le modèle de biographies.
		
				Args:
					comment (str): la biographie sous forme de texte.
				
				Returns:
					(int) Le score de qualité de biographie.
		"""

        if not os.path.isfile(os.path.join(biographies_model_path)):
            print('Creating biographies model...')

            ### Crée le modèle de biographies s'il n'existe pas. ###
            self.createBiographiesModel()

        self.count_vect, self.tfidf_transformer, self.clf = pickle.load(
            open(biographies_model_path, 'rb'))

        X_test_counts = self.count_vect.transform([bio])
        self.X_test_tfidf = self.tfidf_transformer.transform(X_test_counts)

        pred = self.clf.predict_proba(self.X_test_tfidf)[0][1]
        return float(pred)

    def getCommentScore(self, comment):
        """
		Retourne le score de commentaire basé sur le modèle de commentaires.
		Les commentaire les plus pertinents pour une photo (= dont les mots importants sont peu utilisés dans le modèle de commentaires) sont privilégiés.
		Les commentaires les plus longs sont privilégiés.
		
				Args:
					comment (str): le commentaire texte.
				
				Returns:
					(int) Le score de qualité de commentaire, situé entre 0 et 1.
		"""

        ### Pickle le modèle (dictionnaire de mots contenus dans les commentaires + leurs occurences) pour ne pas avoir à le générer à chaque run. ###

        ### Liste des scores de commentaires. ###
        word_scores = list()

        ### Regex pour attraper les mots dans différents alphabets, dont les emojis. ###
        for word in regex.compile(r'[@#\p{L}_\u263a-\U0001f645]+').findall(
                comment):

            ### Pré-process le mot. ###
            _word = self.processWordComment(word)

            if _word:

                ### Attribue un score de commentaire inversement proportionnel à ses occurences dans tous les commentaires de la BDD. ###
                if self.comments_model[_word] > 0:
                    word_score = 1 / self.comments_model[_word]

                else:
                    word_score = 1
                word_scores.append(word_score)

        ### S'il n'y a pas de mots, on attribue 0 pour le commentaire. ###
        ### Sinon, on prend la moyenne. 							   ###
        if len(word_scores) > 0:
            comment_score = mean(word_scores)
        else:
            comment_score = 0

        ### On mutliplie le score de commentaires par des composantes paramétriques: on privilégie d'abord les commentaires les plus longs. ###
        k = 1 - math.exp(-self.K * len(word_scores))

        ### Ensuite, on privilégie les commentaires qui on des mots dont l'écart-type des scores est important. 					    ###
        ### La raison derrière cela, est que les stop-words sont des mots qui vont forcément avoir un score faible.                     ###
        ### De ce fait, si les mots-clés du commentaire sont 'importants' = si leur score est élevé, alors l'écart-type sera important. ###
        j = 1 / (1 + math.exp(-self.K_ * (stdev(word_scores) - self.B))
                 ) if len(word_scores) > 1 else 0

        return k * j * comment_score * len(word_scores)

    def createCommentsModel(self):
        """
		Crée le modèle de commentaires.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        self.sqlClient = SqlClient()

        ### Récupère tous les commentaires en base. ###
        self.sqlClient.openCursor()
        comments = self.sqlClient.getAllComments()
        self.sqlClient.closeCursor()

        ### Déclaration des variables locales pour les compteurs. ###
        comment_count = Counter()
        i = 0
        j = 0

        ### On boucle sur les commentaires et on affiche la progression avec TQDM. ###
        for comment in tqdm(comments):
            comment = str(comment)

            ### Trouve tous les mots, dont les emojis et les caractères de différents alphabets. ###
            wordArray = regex.compile(r'[@#\p{L}_\u263a-\U0001f645]+').findall(
                comment)
            length = len(wordArray)

            ### Pour chaque mot trouvé, on le préprocess. ###
            for word in wordArray:
                _word = self.processWordComment(word)

                ### Ajoute au compteur non pas l'occurence simple du mot, mais dans quel contexte celui-là apparaît (se trouve-t-il dans une phrase longue ou courte ?). ###
                if _word:
                    i += 1
                    comment_count[_word] += 1 / length
                else:
                    j += 1
        print('Éléments considérés : %s' % str(i))
        print('Éléments non considérés : %s' % str(j))

        ### Sauvegarde le modèle dans le dossier models. ###
        with open(os.path.join(comments_model_path), 'wb') as outfile:
            pickle.dump(comment_count, outfile)

    def createBiographiesModel(self):
        """
		Crée le modèle de commentaires.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        self.sqlClient = SqlClient()

        ### Récupère toutes les biographies des utilisateurs annotés. ###
        self.sqlClient.openCursor()
        response = self.sqlClient.getAllBiographies()
        self.sqlClient.closeCursor()

        ### Récupération des champs biographies, et labels. ###
        bios = [row['biography'] for row in response]
        labels = [row['label'] for row in response]

        ### Construction du modèle TF-IDF et apprentissage d'un classifieur calibré pour sortir des probabilités. ###
        ### Vecteurs de comptage. ###
        self.count_vect = CountVectorizer()
        X_train_counts = self.count_vect.fit_transform(bios)

        ### Transformateur TF-IDF. ###
        self.tfidf_transformer = TfidfTransformer()
        self.X_train_tfidf = self.tfidf_transformer.fit_transform(
            X_train_counts)

        ### Entraînement du classificateur. ###
        oneVsRest = CalibratedClassifierCV()
        self.clf = oneVsRest.fit(self.X_train_tfidf, labels)

        ### Sauvegarde du modèle. ###
        with open(os.path.join(biographies_model_path), 'wb') as outfile:
            pickle.dump((self.count_vect, self.tfidf_transformer, self.clf),
                        outfile)

    def processWordComment(self, word):
        """
		Pré-processe le commentaire.

				Args:
					word (str) : le mot.

				Return:
					(str) Le mot pré-processé.
		"""

        ### On vérifie si le mot ne commence pas par un @ (mention) ou un # (hashtag). ###
        ### On vérifie si le "mot" n'est pas une ponctuation. 						   ###
        if word[0] in ['#', '@'] or word in [
                '.', '!', '?', ',', ':', ';', '-', '+', '=', '/', '&', '@',
                '$', '_'
        ]:
            word = None

        else:
            try:
                ### Ici ça pète quand il y a du russe ou des emojis. ###
                word = str(word).lower()
            except:
                word = word
        return word

    def cleanNonExistingUsers(self):
        """
		Supprime les utilisateurs en base qui ont changé de nom/n'existent plus.

				Args:
					None
				
				Returns:
					None
		"""
        self.sqlClient = SqlClient()

        self.sqlClient.openCursor()
        usernames = self.sqlClient.getUserNames(0)
        self.sqlClient.closeCursor()

        self.InstagramAPI = InstagramAPI(self.config['Instagram']['user'],
                                         self.config['Instagram']['password'])
        self.InstagramAPI.login()

        for username in tqdm(usernames):

            self.InstagramAPI.searchUsername(username['user_name'])
            user_server = self.InstagramAPI.LastJson

            if user_server['status'] == 'fail':
                self.sqlClient.openCursor()
                self.sqlClient.setLabel(username['user_name'], '-2')
                self.sqlClient.closeCursor()

            time.sleep(1)

    def testCommentScore(self):
        """
		Tooling permettant de tester les scores des commentaires utilisateur.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        while True:

            ### L'utilisateur rentre un nom de compte Instagram pour tester le modèle de commentaires. ###
            text = input(
                'Comment and I will tell your score ! (type \'exit\' to go next) : '
            )

            ### L'utilisateur entre "exit" pour sortir de la boucle. ###
            if text == 'exit':
                break

            ### Sort le score de commentaires pour l'utilisateur en question. ###
            else:
                comment_score = self.getCommentScore(text)
                print(text)
                print(comment_score)