Python Un scraper DofuSama pour les indices de chasses au trésors.

Inscrit
22 Mars 2017
Messages
26
Reactions
12
#1
Hello,
J'ai pas mal lurké ici pour de l'inspiration, du coup je me dis qu'il est temps de contribuer un peu.

J'avais rien d'intéressant à faire aujourd'hui, du coup j'ai sorti une petite classe Python 2 pour récupérer des positions d'indices de chasse sur DofuSama.

J'ai pondu ça en quelques heures en ayant jamais fait de scraping avant, et je suis au taf, donc j'ai pas pu vraiment le tester, donc y'aura certainement une paire de bugs. Mais en forçant pas trop dessus, ça devrait marcher assez bien.

Vous aurez besoin de la lib Selenium (python -m pip install selenium) et de chromedriver.exe (en le mettant soit dans le PATH, soit direct dans le même répertoire que le code)

Python:
# -*- coding: UTF-8 -*-
from selenium import webdriver
import time

class Sama:
    """
    Une API discount pour DofuSama
    """
    def __init__(self, headless=True):
        """
        Constructeur, fait le lien avec le site par une instance de driver Selenium.
        Nécessite d'avoir chromedriver.exe dans le PATH ou dans le même fichier que ce script.
        Ça devrait pouvoir marcher avec des drivers FF ou IE, pas testé.
        :param headless: si False, vous aurez la fenêtre de navigateur ouverte. Pas utile, à moins de vouloir regarder comment c'est branlé dedans
        """
        print 'Initialisation...'
        start = time.time()
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument('headless')
        self.driver = webdriver.Chrome(chrome_options=options)
        self.driver.get("http://www.dofusama.fr/treasurehunt/rechercher-un-indice.html")
        self.driver.find_element_by_xpath('//*[@id="jBox-overlay"]/div').click()
        print 'Scraper initialisé en ' + str(round(time.time() - start, 1)) + ' secondes\n'

    def get_directions(self, coords, clue, direction):
        """
        La fonction que ça te dis où est l'indice.
        :param coords: Coordonnées actuelles du joueur
        :param clue: L'indice. Doit être EXACTEMENT le même que ceux de la liste DofuSama. Eventuellement je rendrai ça plus souple
        :param direction: Direction de l'indice doit être 'N', 'S', 'E', 'O', idem, y'a moyen de rendre ça plus sympa à utiliser
        :return: La position de l'indice ; une petite description de comment y aller ; le temps que ça a pris pour l'avoir
        """
        start = time.time()
        self.driver.find_element_by_id('map_pos_x').clear()
        self.driver.find_element_by_id('map_pos_x').send_keys(coords[0])
        self.driver.find_element_by_id('map_pos_y').clear()
        self.driver.find_element_by_id('map_pos_y').send_keys(coords[1])
        self.driver.find_element_by_xpath(
            '//*[@id="clue-finder-form"]/div[1]/div/div[2]/div/span/span[1]/span/ul/li/input').clear()
        self.driver.find_element_by_xpath(
            '//*[@id="clue-finder-form"]/div[1]/div/div[2]/div/span/span[1]/span/ul/li/input').send_keys(clue)

        opt_list = self.driver.find_elements_by_class_name('select2-results__option')
        for opt in opt_list:
            if opt.get_attribute('innerHTML') == clue:
                opt.click()
                break  # Déso pas déso

        direction = ['O', 'N', 'S', 'E'].index(direction) + 1
        self.driver.find_element_by_xpath(
            '//*[@id="clue-finder-form"]/div[1]/div/div[3]/div/button[{}]/i'.format(str(direction))).click()

        time.sleep(1)
        description = self.driver.find_element_by_xpath('//*[@id="clue-finder-form"]/div[2]/div[1]/strong[3]').strip().encode('utf-8')
        next_pos = self.driver.find_element_by_xpath('//*[@id="clue-finder-form"]/div[2]/div[1]/strong[2]').strip().encode('utf-8')
        return next_pos, description, round(time.time() - start, 1)

    def get_directions_pretty(self, coords, clue, direction):
        """
        En gros pareil que get_directions(), mais avec des prints à la pelle. Procédure.
        :param coords: Coordonnées actuelles du joueur
        :param clue: L'indice. Doit être EXACTEMENT le même que ceux de la liste DofuSama. Eventuellement je rendrai ça plus souple
        :param direction: Direction de l'indice doit être 'N', 'S', 'E', 'O', idem, y'a moyen de rendre ça plus sympa à utiliser
        :return: La position de l'indice ; une petite description de comment y aller ; le temps que ça a pris pour l'avoir
        """
        print '-----------------------------'
        print 'Coordonnées actuelles : ' + str([coords[0], coords[1]])
        print 'Direction : ' + ['Ouest', 'Nord', 'Sud', 'Est'][['O', 'N', 'S', 'E'].index(direction)]
        print 'Indice Recherché : ' + clue + '\n'

        next_pos, description, duration = self.get_directions(coords, clue, direction)

        print 'L\'indice se trouve en ' + next_pos + ', ' + description
        print 'Trouvé en ' + str(time) + ' secondes\n'

    def destroy(self):
        """
        Termine le processus chromedriver.exe. Vachement important de l'appeller en fin de programme
        :return:
        """
        self.driver.quit()

sm = Sama()

sm.get_directions((6, 10), 'Souche qui ne repousse pas', 'N')
sm.get_directions((6, 9), 'Souche qui ne repousse pas', 'O')

sm.destroy()

__author__ =  'Ugdha'

Si jamais ça intéresse du monde, je pourrais passer un peu plus de temps dessus, histoire de rendre ça bien stable.

La bise,
Ugdha
 

Lakh92

Membre Actif
Inscrit
24 Decembre 2009
Messages
118
Reactions
0
#2
Hello, sympa cette initiative.
Une question que je me pose cela dit, pourquoi passer par Selenium au lieu de faire des requêtes HTTP ?
Utiliser Selenium te fait introduire une grosse dépendance là où tu en voudrais (je pense) un minimum.
La forme de ton projet est sympa puisque ça tient dans une seule classe. Dommage de rajouter une dépendance par dessus.

Bonne initiative en tout cas.
 
Inscrit
22 Mars 2017
Messages
26
Reactions
12
#3
Oké, update.

Donc @Lakh92 a raison, les dépendances c'est chiant, donc j'ai scrapé toutes les coordonnées de tous les indices et je les ai rangées dans un JSON.
Ça a l'avantage d'être super léger, et rapide à l'utilisation (en gros instantané au lieu de 1-2 secondes)
Par contre, le JSON contient un snapshot des coordonées de dofusama, donc il ne suivra pas les évolutions et corrections des indices du site.

Du coup le code :
Python:
# -*- coding: UTF-8 -*-
import json


class ClueFinder:
    def __init__(self):
        with open('Clues.json', 'r') as f:
            self.clues = json.load(f)
            f.close()

    def get_directions(self, input_coords, clue, direction, pretty=False):
        formatted_clue = ''.join([i if ord(i) < 128 else '-' for i in clue]).replace('"', '-').replace("'", '-').replace(' ', '-').replace('---', '-').replace('--', '-').lower()

        try:
            all_clue_coords = self.clues[formatted_clue]
        except Exception:
            raise Exception('Clue does not exist')

        if direction.lower() == 'n':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[0] == coord[0] and input_coords[1]-10 <= coord[1] <= input_coords[1]-1)], key=lambda pos: pos[1], reverse=True)
        elif direction.lower() == 's':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[0] == coord[0] and input_coords[1]+1 <= coord[1] <= input_coords[1]+10)], key=lambda pos: pos[1])
        elif direction.lower() == 'o':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[1] == coord[1] and input_coords[0]-10 <= coord[0] <= input_coords[0]-1)], key=lambda pos: pos[1], reverse=True)
        elif direction.lower() == 'e':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[1] == coord[1] and input_coords[0]+1 <= coord[0] <= input_coords[0]+10)], key=lambda pos: pos[1])
        else:
            raise Exception('Direction must be "n", "s", "e", "o"')

        if clue_pos:
            clue_pos = clue_pos[0]
        else:
            raise Exception('Clue not found within 10 tile in that direction : ' + direction)

        distance = max(abs(clue_pos[0]-input_coords[0]), abs(clue_pos[1]-input_coords[1]))

        if pretty:
            print 'Coordonnées de départ : ' + str(input_coords)
            print 'Indice : ' + clue
            print 'Direction : ' + direction
            print 'L\'indice est à {} cases, en {}.\n'.format(distance, tuple(clue_pos))

        return clue_pos, distance

CF = ClueFinder()
CF.get_directions((5, -26), 'Souche qui ne repousse pas', 'n', True)
CF.get_directions((1, -4), 'Tonneau', 's', True)
CF.get_directions((5, -26), 'Souche qui ne repousse pas', 'o')
CF.get_directions((5, -26), 'Souche qui ne repousse pas', 'e')

Donc le truc cool c'est qu'on a plus besoin de modules hyper lourds.

EDIT : Le script pour ce qui veulent l’exécuter depuis un shell
Python:
# -*- coding: UTF-8 -*-
import json
import sys
from ast import literal_eval


class ClueFinder:
    def __init__(self):
        with open('Clues.json', 'r') as f:
            self.clues = json.load(f)
            f.close()

    def get_directions(self, input_coords, clue, direction, pretty=False):
        formatted_clue = ''.join([i if ord(i) < 128 else '-' for i in clue]).replace('"', '-').replace("'", '-').replace(' ', '-').replace('---', '-').replace('--', '-').lower()

        try:
            all_clue_coords = self.clues[formatted_clue]
        except Exception:
            raise Exception('Clue does not exist')

        if direction.lower() == 'n':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[0] == coord[0] and input_coords[1]-10 <= coord[1] <= input_coords[1]-1)], key=lambda pos: pos[1], reverse=True)
        elif direction.lower() == 's':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[0] == coord[0] and input_coords[1]+1 <= coord[1] <= input_coords[1]+10)], key=lambda pos: pos[1])
        elif direction.lower() == 'o':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[1] == coord[1] and input_coords[0]-10 <= coord[0] <= input_coords[0]-1)], key=lambda pos: pos[1], reverse=True)
        elif direction.lower() == 'e':
            clue_pos = sorted([coord for coord in all_clue_coords if (input_coords[1] == coord[1] and input_coords[0]+1 <= coord[0] <= input_coords[0]+10)], key=lambda pos: pos[1])
        else:
            raise Exception('Direction must be "n", "s", "e", "o"')

        if clue_pos:
            clue_pos = clue_pos[0]
        else:
            raise Exception('Clue not found within 10 tile in that direction : ' + direction)

        distance = max(abs(clue_pos[0]-input_coords[0]), abs(clue_pos[1]-input_coords[1]))

        if pretty:
            print 'Coordonnées de départ : ' + str(input_coords)
            print 'Indice : ' + clue
            print 'Direction : ' + direction
            print 'L\'indice est à {} cases, en {}.\n'.format(distance, tuple(clue_pos))

        return clue_pos, distance


if __name__ == "__main__":
    CF = ClueFinder()
    if len(sys.argv) == 4:
        CF.get_directions(literal_eval(sys.argv[1]), sys.argv[2], sys.argv[3])
    elif len(sys.argv) == 5:
        CF.get_directions(literal_eval(sys.argv[1]), sys.argv[2], sys.argv[3], sys.argv[4] == 'y')
    else:
        print 'Usage : <(x,y)> <clue (- instead of spaces)> <direction> <pretty : y/n (optionnal)>'
Kenavo,
Ugdha
 

Pièces jointes

Dernière édition:

Lakh92

Membre Actif
Inscrit
24 Decembre 2009
Messages
118
Reactions
0
#4
J'aime beaucoup la mise à jour.
Je pense que du coup la question restante est est-ce que ça vaut le coup d'attendre un petit peu plus longtemps pour avoir des informations à jour ?
Comme dit dans mon premier message, peut-être que tu devrais regarder au niveau des requêtes HTTP à faire pour récupérer l'information que tu veux. Ici, je pense que la rapidité d'exécution est pas si importante que ça et que sacrifier quelques secondes pour avoir une information à jour est peut-être plus judicieux. À voir..
En tout cas bien joué pour cette mise à jour, et bonne chance pour la suite.
 
Inscrit
22 Mars 2017
Messages
26
Reactions
12
#5
C'est vrai que le faire en requêtes HTTP serait surement la meilleure solution, mais je m'y connais très peu en WebDev, et du coup je suis assez paummé par la façon dont le formulaire est rempli. J'avoue que j'ai un peu choisi la solution de facilité.

Après, avoir un JSON fixe a l'avantage de pas exploser la bande passante du site sans générer de revenu pour le créateur, surtout si t'as 40 bots qui courent partout et qui font des requêtes toutes les 10 secondes.

Et puis il peut-être mis à jour un truc genre toutes les semaines, ça suffit largement, et c'est pas franchement compliqué à faire.
 
Haut Bas