Attendez que la page soit chargée avec Selenium WebDriver pour Python

182

Je veux gratter toutes les données d'une page implémentée par un scroll infini. Le code python suivant fonctionne.

for i in range(100):
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(5)

Cela signifie que chaque fois que je fais défiler vers le bas, je dois attendre 5 secondes, ce qui est généralement suffisant pour que la page termine le chargement du contenu nouvellement généré. Mais cela peut ne pas être efficace en termes de temps. La page peut finir de charger le nouveau contenu dans les 5 secondes. Comment puis-je détecter si la page a fini de charger le nouveau contenu à chaque fois que je fais défiler vers le bas? Si je peux détecter cela, je peux à nouveau faire défiler vers le bas pour voir plus de contenu une fois que je sais que la page a fini de charger. C'est plus efficace en temps.

Apogne
la source
1
Il peut être utile d'en savoir un peu plus sur la page. Les éléments sont-ils séquentiels ou prévisibles? Vous pouvez attendre le chargement des éléments en vérifiant la visibilité en utilisant id ou xpath
user2272115
Je suis en train de
parcourir
1
duplication possible de Détecter de manière
fiable
Est-ce que cela répond à votre question? Attendre le chargement de la page dans Selenium
Matej J

Réponses:

235

Le webdriverva attendre qu'une page se charge par défaut via la .get()méthode.

Comme vous recherchez peut-être un élément spécifique comme l'a dit @ user227215, vous devriez utiliser WebDriverWaitpour attendre un élément situé dans votre page:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException

browser = webdriver.Firefox()
browser.get("url")
delay = 3 # seconds
try:
    myElem = WebDriverWait(browser, delay).until(EC.presence_of_element_located((By.ID, 'IdOfMyElement')))
    print "Page is ready!"
except TimeoutException:
    print "Loading took too much time!"

Je l'ai utilisé pour vérifier les alertes. Vous pouvez utiliser toute autre méthode de type pour trouver le localisateur.

MODIFIER 1:

Je dois mentionner que le webdriverva attendre qu'une page se charge par défaut. Il n'attend pas le chargement à l'intérieur des cadres ou les requêtes ajax. Cela signifie que lorsque vous utilisez .get('url'), votre navigateur attendra que la page soit complètement chargée, puis passera à la commande suivante dans le code. Mais lorsque vous postez une demande ajax, webdrivern'attendez pas et il est de votre responsabilité d'attendre un laps de temps approprié pour que la page ou une partie de la page se charge; il y a donc un module nommé expected_conditions.

Zeinab Abbasimazar
la source
3
J'obtenais l'argument "find_element () après * doit être une séquence, pas WebElement" changé en "WebDriverWait (navigateur, délai) .until (EC.presence_of_element_located ((By.ID," IdOfMyElement ")))" voir manuel sélénium- python.readthedocs.org/en/latest/waits.html
fragles
2
Le commentaire de @fragles et la réponse de David Cullen ont fonctionné pour moi. Peut-être que cette réponse acceptée pourrait être mise à jour en conséquence?
Michael Ohlrogge
6
Passer browser.find_element_by_id('IdOfMyElement')fait NoSuchElementExceptionmonter a. La documentation dit de passer un tuple qui ressemble à ceci: (By.ID, 'IdOfMyElement'). Voir ma réponse
David Cullen
2
J'espère que cela aide quelqu'un d'autre parce que ce n'était pas clair pour moi au départ: WebDriverWait renverra en fait un objet Web sur lequel vous pouvez ensuite effectuer une action (par exemple click()), lire du texte, etc. J'avais l'impression erronée que c'était juste a causé une attente, après quoi vous deviez toujours trouver l'élément. Si vous faites une attente, puis un élément de recherche après, le sélénium se trompera car il essaiera de trouver l'élément pendant que l'ancienne attente est toujours en cours de traitement (j'espère que cela a du sens). En bout de ligne, vous n'avez pas besoin de trouver l'élément après avoir utilisé WebDriverWait - c'est déjà un objet.
Ben Wilson
1
@Gopgop Wow c'est tellement moche n'est pas un commentaire constructif. Qu'est-ce que c'est moche? Comment pourrait-il être amélioré?
Modus Tollens
73

Essayer de passer find_element_by_id au constructeur pour presence_of_element_located(comme indiqué dans la réponse acceptée ) a provoqué NoSuchElementExceptionune levée. J'ai dû utiliser la syntaxe du commentaire de fragles :

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

driver = webdriver.Firefox()
driver.get('url')
timeout = 5
try:
    element_present = EC.presence_of_element_located((By.ID, 'element_id'))
    WebDriverWait(driver, timeout).until(element_present)
except TimeoutException:
    print "Timed out waiting for page to load"

Cela correspond à l' exemple de la documentation . Voici un lien vers la documentation de By .

David Cullen
la source
2
Je vous remercie! oui, c'était nécessaire pour moi aussi. L'ID n'est pas le seul attribut qui peut être utilisé, pour obtenir la liste complète, utilisez help (By). Par exemple, j'ai utiliséEC.presence_of_element_located((By.XPATH, "//*[@title='Check All Q1']"))
Michael Ohlrogge
C'est ainsi que cela fonctionne pour moi aussi! J'ai écrit une réponse supplémentaire en développant les différents localisateurs disponibles avec l' Byobjet.
J0ANMM
J'ai posté une question de suivi traitant des attentes où différentes pages peuvent être chargées, et pas toujours la même page: stackoverflow.com/questions/51641546
...
48

Retrouvez ci-dessous 3 méthodes:

readyState

Vérification de l'état de la page prête (non fiable):

def page_has_loaded(self):
    self.log.info("Checking if {} page is loaded.".format(self.driver.current_url))
    page_state = self.driver.execute_script('return document.readyState;')
    return page_state == 'complete'

La wait_forfonction d'assistance est bonne, mais elle click_through_to_new_pageest malheureusement ouverte à la condition de concurrence où nous parvenons à exécuter le script dans l'ancienne page, avant que le navigateur n'ait commencé à traiter le clic, et page_has_loadedrenvoie simplement vrai tout de suite.

id

Comparaison des nouveaux identifiants de page avec l'ancien:

def page_has_loaded_id(self):
    self.log.info("Checking if {} page is loaded.".format(self.driver.current_url))
    try:
        new_page = browser.find_element_by_tag_name('html')
        return new_page.id != old_page.id
    except NoSuchElementException:
        return False

Il est possible que la comparaison des identifiants ne soit pas aussi efficace que l'attente d'exceptions de référence périmées.

staleness_of

En utilisant la staleness_ofméthode:

@contextlib.contextmanager
def wait_for_page_load(self, timeout=10):
    self.log.debug("Waiting for page to load at {}.".format(self.driver.current_url))
    old_page = self.find_element_by_tag_name('html')
    yield
    WebDriverWait(self, timeout).until(staleness_of(old_page))

Pour plus de détails, consultez le blog de Harry .

Kenorb
la source
Pourquoi dites-vous que ce self.driver.execute_script('return document.readyState;')n'est pas fiable? Cela semble fonctionner parfaitement pour mon cas d'utilisation, qui attend qu'un fichier statique se charge dans un nouvel onglet (qui est ouvert via javascript dans un autre onglet au lieu de .get ()).
Arthur Hebert
1
@ArthurHebert Pourrait ne pas être fiable en raison de la condition de race, j'ai ajouté une citation pertinente.
kenorb
23

Comme mentionné dans la réponse de David Cullen , j'ai toujours vu des recommandations d'utiliser une ligne comme la suivante:

element_present = EC.presence_of_element_located((By.ID, 'element_id'))
WebDriverWait(driver, timeout).until(element_present)

Il était difficile pour moi de trouver quelque part tous les localisateurs possibles pouvant être utilisés avec le By , donc j'ai pensé qu'il serait utile de fournir la liste ici. Selon Web Scraping with Python par Ryan Mitchell:

ID

Utilisé dans l'exemple; trouve les éléments par leur attribut HTML id

CLASS_NAME

Utilisé pour rechercher des éléments par leur attribut de classe HTML. Pourquoi cette fonction CLASS_NAMEn'est-elle pas simplementCLASS ? L'utilisation du formulaire object.CLASS créerait des problèmes pour la bibliothèque Java de Selenium, où .classest une méthode réservée. Afin de garder la syntaxe Selenium cohérente entre les différentes langues, a CLASS_NAMEété utilisée à la place.

CSS_SELECTOR

Recherche les éléments par leur classe, leur identifiant ou leur nom de balise, à l'aide du #idName , .className, tagNameconvention.

LINK_TEXT

Recherche les balises HTML par le texte qu'elles contiennent. Par exemple, un lien indiquant "Suivant" peut être sélectionné à l'aide de (By.LINK_TEXT, "Next").

PARTIAL_LINK_TEXT

Semblable à LINK_TEXT , mais correspond à une chaîne partielle.

NAME

Recherche les balises HTML par leur attribut de nom. Ceci est pratique pour les formulaires HTML.

TAG_NAME

Recherche les balises HTML par leur nom de balise.

XPATH

Utilise une expression XPath ... pour sélectionner les éléments correspondants.

J0ANMM
la source
5
La documentation de By répertorie les attributs qui peuvent être utilisés comme localisateurs.
David Cullen
1
C'était ce que je cherchais! Merci! Eh bien, maintenant cela devrait être plus facile à trouver car Google m'envoyait à cette question, mais pas à la documentation officielle.
J0ANMM
Merci pour la citation du livre. C'est beaucoup plus clair que la documentation.
ZygD
11

En passant, au lieu de faire défiler 100 fois vers le bas, vous pouvez vérifier s'il n'y a plus de modifications dans le DOM (nous sommes dans le cas où le bas de la page est chargé paresseux AJAX)

def scrollDown(driver, value):
    driver.execute_script("window.scrollBy(0,"+str(value)+")")

# Scroll down the page
def scrollDownAllTheWay(driver):
    old_page = driver.page_source
    while True:
        logging.debug("Scrolling loop")
        for i in range(2):
            scrollDown(driver, 500)
            time.sleep(2)
        new_page = driver.page_source
        if new_page != old_page:
            old_page = new_page
        else:
            break
    return True
raffaem
la source
C'est utile. Mais que représente le 500? Est-il assez grand pour atteindre la fin de la page?
Moondra
C'est le montant que la page doit défiler ... vous devez le définir aussi haut que possible. Je viens de découvrir que ce nombre me suffisait, car il fait défiler la page vers le bas jusqu'à ce que les éléments AJAX soient chargés paresseusement, ce qui
oblige à
Cela aide lorsque vous essayez de vous assurer que tous les commentaires sur un problème dans gitlab sont entièrement chargés.
bgStack15
7

Avez-vous essayé driver.implicitly_wait. C'est comme un paramètre pour le pilote, donc vous ne l'appelez qu'une seule fois dans la session et cela dit essentiellement au pilote d'attendre le laps de temps donné jusqu'à ce que chaque commande puisse être exécutée.

driver = webdriver.Chrome()
driver.implicitly_wait(10)

Donc, si vous définissez un temps d'attente de 10 secondes, il exécutera la commande dès que possible, en attendant 10 secondes avant d'abandonner. J'ai utilisé cela dans des scénarios de défilement similaires, donc je ne vois pas pourquoi cela ne fonctionnerait pas dans votre cas. J'espère que cela est utile.

Pour pouvoir corriger cette réponse, je dois ajouter un nouveau texte. Veillez à utiliser un «w» minuscule dans implicitly_wait.

seeiespi
la source
Quelle est la différence entre implicitly wait et webdriverwait?
song0089
4

Que diriez-vous de mettre WebDriverWait dans la boucle While et d'attraper les exceptions.

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

browser = webdriver.Firefox()
browser.get("url")
delay = 3 # seconds
while True:
    try:
        WebDriverWait(browser, delay).until(EC.presence_of_element_located(browser.find_element_by_id('IdOfMyElement')))
        print "Page is ready!"
        break # it will break from the loop once the specific element will be present. 
    except TimeoutException:
        print "Loading took too much time!-Try again"
Rao
la source
vous n'avez pas besoin de la boucle?
Corey Goldberg
4

Ici, je l'ai fait en utilisant un formulaire assez simple:

from selenium import webdriver
browser = webdriver.Firefox()
browser.get("url")
searchTxt=''
while not searchTxt:
    try:    
      searchTxt=browser.find_element_by_name('NAME OF ELEMENT')
      searchTxt.send_keys("USERNAME")
    except:continue
Ahmed Abdelmalek
la source
1

Vous pouvez le faire très simplement avec cette fonction:

def page_is_loading(driver):
    while True:
        x = driver.execute_script("return document.readyState")
        if x == "complete":
            return True
        else:
            yield False

et lorsque vous voulez faire quelque chose une fois le chargement de la page terminé, vous pouvez utiliser:

Driver = webdriver.Firefox(options=Options, executable_path='geckodriver.exe')
Driver.get("https://www.google.com/")

while not page_is_loading(Driver):
    continue

Driver.execute_script("alert('page is loaded')")
NaabNuts
la source