angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

X automatisation du web et scraping avec Selenium

Pour les petits projets Selenium , nous n'empêchons pas la détection automatique, mais nous essayons d'imiter le comportement humain.

11 août 2023 Mise à jour 11 août 2023
post main image
https://unsplash.com/@nixcreative

Lorsque vous souhaitez récupérer des données sur le web, vous devez savoir ce que vous faites. Vous ne voulez pas surcharger un serveur cible avec des requêtes. Si vous le faites à partir d'un seul endroit, un IP address, vous risquez d'être banni (temporairement).

Si vous voulez scraper beaucoup, envisagez d'utiliser un service dédié tel que ZenRows, ScrapFly, WebScrapingAPI, ScrapingAnt, etc. Ils dist répartissent vos requêtes sur de nombreux systèmes, chacun avec une IP address unique, ce qui signifie que le serveur cible pensera qu'il est accédé par de nombreux clients (humains) différents.

Mais parfois, nous voulons simplement récupérer un peu de données, disons que nous voulons récupérer les nouveaux messages de quelques personnes sur une plateforme de médias sociaux comme X tous les jours.

Dans cet article, nous utiliserons Selenium. Il existe des solutions comme Puppeteer ou Playwright, mais Selenium existe depuis 2004, c'est l'outil le plus utilisé pour les tests automatisés et il a une grande communauté.

Je commencerai par quelques considérations et terminerai par du code fonctionnel. Le code automatise la connexion à X, , puis la recherche et la mise au rebut de certains messages. Je sais que tout peut changer demain, mais aujourd'hui cela fonctionne.

Comme toujours, j'utilise Ubuntu 22.04.

Scraping à partir de X avec Selenium

Récemment, X a décidé que vous deviez vous connecter pour voir les messages. Je n'aime pas cela. La plupart du temps, je ne lis que les messages de quelques personnes, en utilisant l'extension de navigateur 'Breakthrough Twitter Login Wall'. Cette extension ne fonctionne plus.

Et parfois, je récupère quelques données, mais pas des centaines de milliers de tweets. Je pense que de nombreux développeurs font cela. Il y a près de 30 millions d'ingénieurs logiciels dans le monde. Supposons maintenant qu'un sur mille scrape parfois quelques X posts une fois par an, qui scrappent en moyenne 1000 posts chacun, pour de petits projets, des tests et du plaisir (apprentissage). Cela fait 30 000 scrapeurs par an, 100 scrapeurs par jour, soit 100 000 messages par jour. Ce n'est pas beaucoup si l'on considère les millions d'articles publiés chaque jour. . X dispose toujours d'un compte gratuit API , mais celui-ci est en écriture seule. Si nous voulons récupérer des données, nous ne pouvons utiliser que le site web X et y accéder avec des outils comme Selenium.

Selenium est un outil très puissant pour les tests automatisés, mais il peut également être utilisé pour le web scraping. Il a été publié en 2004 et dispose d'une large communauté. D'autres outils populaires que vous pourriez considérer sont Puppeteer et Playwright.

Imitation du comportement humain

Je dirais qu'il n'est pas illégal de récupérer une page web tant que vous ne diffusez pas les données récupérées sous une forme reconnaissable. Cela dit, je peux imaginer que certaines entreprises bloquent l'accès à leurs pages web et à leurs services pour les logiciels automatisés.

Qu'est-ce qu'un logiciel automatisé et comment peut-on le détecter ? La vérification la plus évidente consiste à déterminer si le comportement d'un visiteur diffère du comportement humain. Cela signifie généralement que votre logiciel automatisé doit se comporter comme un humain. Tapez quelque chose et attendez quelques secondes, cliquez sur un bouton et attendez quelques secondes, défilez vers le bas et attendez quelques secondes, etc. Un humain ne peut pas remplir un formulaire en 50 millisecondes...

Considérons un autre scénario, dans lequel Selenium est utilisé pour ouvrir les pages d'un site web à l'aide de la commande vocale. Même si le site web détecte que nous utilisons Selenium, cela semble parfaitement légitime car il est très clair que les actions sont effectuées par un humain.

Se rendre sur un site web et acheter un produit bon marché est un comportement humain, mais acheter tous les produits bon marché en grandes quantités est suspect. Surtout si cela se produit jour après jour.

Cela signifie que notre logiciel doit avant tout imiter le comportement humain. Faites quelque chose et attendez un nombre aléatoire de secondes, faites autre chose et attendez un nombre aléatoire de secondes, etc.

Sélection du navigateur

Selenium supporte de nombreux navigateurs. Pour ce projet, j'utilise Chrome. La raison en est évidente : Chrome est le navigateur le plus utilisé, non seulement par les user internautes, mais aussi par les développeurs. Sur ma machine de développement, j'utilise Firefox, Chromium, et (parfois) Brave. Cela signifie que je peux utiliser Chrome exclusivement pour ce projet. Si vous utilisez Chrome également pour la navigation web, vous pouvez créer un profil séparé pour l'application de scraping.

Détection d'un scraper bot

Un scraper bot est un outil ou un morceau de code utilisé pour extraire des données de pages web. Le scraping n'est pas un phénomène nouveau et de nombreuses organisations ont pris des mesures pour empêcher les outils automatisés d'accéder à leurs sites web. L'article 'How to Avoid Bot Detection with Selenium', voir les liens ci-dessous, énumère plusieurs façons d'éviter la détection des robots.

Si nous ne faisons rien, il est très facile de détecter notre robot Selenium . Tant que nous nous comportons comme un humain, ce n'est pas forcément une mauvaise chose. Nous montrons au site web cible que nous sommes des script kiddies ou que nous voulons qu'ils remarquent que nous utilisons une certaine automatisation.

Néanmoins, certaines entreprises peuvent nous bloquer pour des raisons légitimes. Soyons réalistes, un bon bot peut soudainement devenir un très mauvais bot.

Il existe quelques sites que nous pouvons utiliser pour déterminer si notre robot est reconnu comme un logiciel automatisé :

  • https://fingerprint.com/products/bot-detection
  • https://pixelscan.net
  • https://nowsecure.nl/#relax

Voici un exemple de code que vous pouvez utiliser pour essayer ceci :

# bot_detect_test.py
import os
import time

# selenium
from selenium import webdriver
# chromedriver_binary, see pypi.org
import chromedriver_binary

# configure webdriver
from selenium.webdriver.chrome.options import Options
webdriver_options = Options()
# use a common screen format
webdriver_options.add_argument('window-size=1920,1080')
# profile
user_data_dir = os.path.join(os.getcwd(), 'selenium_data')
webdriver_options.add_argument('user-data-dir=' + user_data_dir)
# allow popups
webdriver_options.add_experimental_option('excludeSwitches', ['disable-popup-blocking']);


def main():
    with webdriver.Chrome(options=webdriver_options) as wd:
        wd.get('https://fingerprint.com/products/bot-detection')
        time.sleep(8)
        wd.save_screenshot('fingerprint.png')
    print(f'ready')

if __name__ == '__main__':
    main()

Serons-nous bloqués ?

Plus nous essayons d'empêcher la détection de notre robot Selenium , plus nous risquons de paraître suspects aux yeux des détecteurs de robots.

Il existe un projet "undetected_chromedriver", voir les liens ci-dessous, qui tente de fournir un bot, basé sur Selenium, qui ne peut pas être détecté. Mais s'il est détecté, le site web cible peut décider de vous bloquer car ... pourquoi quelqu'un voudrait-il être indétectable ?

Les entreprises qui développent des détecteurs de bots ou qui proposent des services de détection de bots suivent de près des projets tels que "undetected_chromedriver" et tentent de déjouer toutes les méthodes de prévention de la détection qui sont mises en œuvre.

Les mises à jour posent également problème. Une nouvelle version de Selenium ou de Chrome peut rompre les mesures de prévention de la détection. Mais le fait de ne pas mettre à jour Chrome pendant une longue période peut également être détectable et suspect.

Que faire alors ? Rien, un peu, ou utiliser par exemple 'undetected_chromedriver' ? Si vous voulez faire du scraping à grande échelle, le meilleur moyen est d'utiliser un service de scraping comme ceux mentionnés ci-dessus. Pour les petits projets, je vous suggère de ne rien faire pour éviter la détection et de voir ce qui se passe.

Avons-nous vraiment besoin d'automatiser la connexion ?

Au lieu d'automatiser la connexion et de se connecter via notre script, nous pouvons également nous connecter manuellement à l'aide du navigateur, puis utiliser les données du navigateur (données de profil, session) lors de la connexion à X. Cela permet d'économiser beaucoup de code. Mais ... notre application n'est pas entièrement automatisée dans ce cas. Et puis, nous utilisons Selenium, l'outil d'automatisation du web ( !).

A propos du code

Le code ci-dessous automatise la connexion à X et scrape ensuite quelques posts d'un user sélectionné. Une fois cette opération effectuée, le script se termine. Il utilise un profil distinct pour le navigateur web, ce qui signifie qu'il ne devrait pas interférer avec l'utilisation normale du navigateur. Si la connexion a réussi et que le script existe, la prochaine fois que le script est lancé, il ne se connecte plus mais commence immédiatement à sélectionner le user et à récupérer les messages.

Les messages du user sélectionné sont déposés dans un fichier 'posts.txt'. Cela signifie que le code n'extrait pas les parties des messages. Vous pouvez utiliser BeautifulSoup pour cela.

Reconnaissance des pages

Notre script traite les pages suivantes :

Lorsqu'on n'est pas connecté :

  • Page d'accueil
  • Login - entrer l'email
  • Login - entrer le mot de passe
  • Login - entrer le nom user (en cas d'activité inhabituelle)

Une fois connecté :

  • Page de connexion

La page "Activité inhabituelle" s'affiche parfois, par exemple lorsque vous êtes déjà connecté via un autre navigateur, avant la page "Saisir le mot de passe".

Ce que nous faisons en boucle :

  • Extraire la langue
  • Détecter la page affichée
  • Effectuer l'action correspondant à la page

Lorsque vous êtes sur la page d'accueil, nous redirigeons simplement vers la page de connexion. Toutes les autres pages non connectées ont un titre, un "champ de formulaire" et un "bouton de formulaire". Cela signifie que nous pouvons les traiter de la même manière.

Et lorsque nous parlons de pages, nous ne parlons pas d'URL, mais de ce qui est affiché à l'écran. X est tout Javascript.

Le détecteur "sur quelle page sommes-nous ?

Il s'agit probablement du morceau de code le plus "excitant". Nous commençons par créer une liste d'éléments 'presence_of_element_located' mutuellement exclusifs :

  • Bouton de profil => connecté
  • Bouton de connexion avec texte 'Sign in' => 'home_page'
  • <h1> texte 'Se connecter à X' => 'login_page_1_email'
  • <h1> texte 'Entrez votre mot de passe' => 'login_page_2_password'
  • <h1> text 'Entrez votre numéro de téléphone ou username' => 'login_page_3_username'

Nous attendons ensuite qu'au moins un des éléments soit localisé :

    wait = WebDriverWait(
        ....
    )
    elem = wait.until(EC.any_of(*tuple(or_conditions)))

Une fois que nous avons l'élément, nous pouvons déterminer la page. Une fois que nous avons l'élément, nous pouvons déterminer la page, et une fois que nous avons la page, nous pouvons exécuter l'action appropriée.

Oups : "Quelque chose s'est mal passé. Essayez de recharger".

Ce message apparaît parfois lors de la connexion. Je le vois à des moments aléatoires, et je me demande s'il n'a pas été intégré à dessein. Le code génère une erreur de temporisation lorsque cela se produit. Vous devez redémarrer le script ou implémenter votre propre fonction de réessai.

Les langues

Il existe deux langages avec lesquels nous devons composer : le langage de l'utilisateur non connecté et le langage du compte. Il s'agit souvent de la même langue. Le code ci-dessous fonctionne pour les langues anglaise et néerlandaise, même mélangées. La langue est extraite de la page.

Pour ajouter une nouvelle langue, ajoutez-la à :

    # languages
    self.available_langs = ['en', 'nl']

et ajoutez les textes aux pages :

    # pages
    self.pages = [
        ...
    ]

Bien entendu, vous devez d'abord consulter les textes en utilisant la connexion manuelle.

Pour démarrer le navigateur dans une autre langue, supprimez d'abord le répertoire du profil du navigateur :

rm -R browser_profile

puis lancez le script après avoir ( !) défini la nouvelle locale :

bash -c 'LANGUAGE=nl_NL.UTF-8 python x_get_posts.py'

XPath recherche

Si vous souhaitez effectuer une recherche à partir de la racine du document, commencez XPath par '//'.
Si vous souhaitez effectuer une recherche relative à un élément particulier, commencez XPath par './/'.

Messages

Actuellement, les messages visibles sont écrits sous la forme HTML dans le fichier 'posts.txt'. Pour obtenir plus d'articles, implémentez votre propre fonction de défilement.

Extraction des données d'un message

C'est à vous de décider. Je vous suggère d'utiliser BeautifulSoup.

Le code

Au cas où vous voudriez essayer, voici le code. Avant de l'exécuter, assurez-vous d'ajouter les données de votre compte et le nom de la recherche.

# x_get_posts.py
import logging
import os
import random
import sys
import time

# selenium
from selenium import webdriver
# using chromedriver_binary
import chromedriver_binary  # Adds chromedriver binary to path
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import selenium.common.exceptions as selenium_exceptions

from selenium.webdriver.chrome.options import Options

# configure webdriver: hide gui, window size, full-screen
webdriver_options = Options()
#webdriver_options.add_argument('--headless')
# use a common screen format
webdriver_options.add_argument('window-size=1920,1080')
# use a separate profile
user_data_dir = os.path.join(os.getcwd(), 'browser_profile')
webdriver_options.add_argument('user-data-dir=' + user_data_dir)
# allow popups
webdriver_options.add_experimental_option('excludeSwitches', ['disable-popup-blocking']);
# force language
webdriver_options.add_argument('--lang=nl-NL');

def get_logger(
    console_log_level=logging.DEBUG,
    file_log_level=logging.DEBUG,
    log_file=os.path.splitext(__file__)[0] + '.log',
):
    logger_format = '%(asctime)s %(levelname)s [%(filename)-30s%(funcName)30s():%(lineno)03s] %(message)s'
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    if console_log_level:
        # console
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(console_log_level)
        console_handler.setFormatter(logging.Formatter(logger_format))
        logger.addHandler(console_handler)
    if file_log_level:
        # file
        file_handler = logging.FileHandler(log_file)
        file_handler.setLevel(file_log_level)
        file_handler.setFormatter(logging.Formatter(logger_format))
        logger.addHandler(file_handler)
    return logger

logger = get_logger(
    file_log_level=None,
)
logger.debug('START')
logger.debug(f'user_data_dir = {user_data_dir}')


class SeleniumBase:
    def __init__(
        self,
        logger=None,
        wd=None,
    ):
        self.logger = logger
        self.wd = wd

        self.web_driver_wait_timeout = 10
        self.web_driver_wait_poll_frequency = 1
        self.min_wait_for_next_page = 3.
        
    def get_elem_attrs(self, elem, dump=False, dump_header=None):
        elem_attrs = self.wd.execute_script('var items = {}; for (index = 0; index < arguments[0].attributes.length; ++index) { items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value }; return items;', elem)
        if dump:
            if dump_header is not None:
                self.logger.debug(f'{dump_header}')
            for k, v in elem_attrs.items():
                self.logger.debug(f'attrs {k}: {v}')
        return elem_attrs

    def random_wait(self, min_wait_secs=None):
        self.logger.debug(f'(min_wait_secs = {min_wait_secs})')
        min_wait_secs = min_wait_secs or 2.
        wait_secs = min_wait_secs + 2 * random.uniform(0, 1)
        self.logger.debug(f'wait_secs = {wait_secs:.1f}')
        time.sleep(wait_secs)

    def elem_click_wait(self, elem, before_min_wait=None, after_min_wait=None):
        self.logger.debug(f'(, before_min_wait = {before_min_wait}, after_min_wait = {after_min_wait})')
        if before_min_wait is not None:
            self.random_wait(min_wait_secs=before_min_wait)
        elem.click()
        if after_min_wait is not None:
            self.random_wait(min_wait_secs=after_min_wait)

    def elem_send_keys_wait(self, elem, value, before_min_wait=None, after_min_wait=None):
        self.logger.debug(f'(, value = {value}, before_min_wait = {before_min_wait}, after_min_wait = {after_min_wait})')
        if before_min_wait is not None:
            self.random_wait(min_wait_secs=before_min_wait)
        elem.send_keys(value)
        if after_min_wait is not None:
            self.random_wait(min_wait_secs=after_min_wait)

    def set_form_field_value_and_click(self, form_field_elem=None, form_field_value=None, form_button_elem=None):
        self.logger.debug(f'(form_field_elem = {form_field_elem}, form_field_value = {form_field_value}, form_button_elem = {form_button_elem})')
        if form_field_elem is not None and form_field_value is not None:
            self.random_wait(min_wait_secs=1.)
            form_field_elem.send_keys(form_field_value)
        if form_button_elem is not None:
            self.random_wait(min_wait_secs=.5)
            form_button_elem.click()
            self.random_wait(min_wait_secs=self.min_wait_for_next_page)

    def wait_for_element(
        self, 
        xpath,
        parent_xpath=None,
        parent_attr_check=None,
        visible=True,
    ):
        self.logger.debug(f'xpath = {xpath}, parent_xpath = {parent_xpath}, parent_attr_check = {parent_attr_check}, visible = {visible}')

        wait = WebDriverWait(
            self.wd,
            timeout=self.web_driver_wait_timeout,
            poll_frequency=self.web_driver_wait_poll_frequency,
            ignored_exceptions=[
                selenium_exceptions.ElementNotVisibleException,
                selenium_exceptions.ElementNotSelectableException,
            ],
        )
        try:
            if visible:
                elem = wait.until(EC.visibility_of_element_located((By.XPATH, xpath)))
            else:
                elem = wait.until(EC.presence_of_element_located((By.XPATH, xpath)))
        except selenium_exceptions.TimeoutException as e:
            self.logger.exception(f'TimeoutException, e = {e}, e.args = {e.args}')
            raise
        except Exception as e:
            self.logger.exception(f'other exception, e = {e}')
            raise

        self.get_elem_attrs(elem, dump=True, dump_header='elem')

        if parent_xpath is None:
            self.logger.debug(f'elem.tag_name = {elem.tag_name}')
            return elem
            
        parent_elem = elem.find_element(By.XPATH, parent_xpath)
        self.get_elem_attrs(parent_elem, dump=True, dump_header='parent_elem')
        if parent_attr_check is not None:
            for k, v_expected in parent_attr_check.items():
                v = parent_elem.get_attribute(k)
                if v != v_expected:
                    raise Exception(f'parent_elem = {parent_elem}, attr k = {k}, v = {v} != v_expected = {v_expected}')
        self.logger.debug(f'parent_elem.tag_name = {parent_elem.tag_name}')
        return parent_elem
    
    def wait_for_elements(self, xpath):
        self.logger.debug(f'xpath = {xpath}')

        wait = WebDriverWait(
            self.wd,
            timeout=self.web_driver_wait_timeout,
            poll_frequency=self.web_driver_wait_poll_frequency,
            ignored_exceptions=[
                selenium_exceptions.ElementNotVisibleException,
                selenium_exceptions.ElementNotSelectableException,
            ],
        )
        try:
            elems = wait.until(EC.presence_of_all_elements_located((By.XPATH, xpath)))
        except selenium_exceptions.TimeoutException as e:
            self.logger.exception(f'TimeoutException, e = {e}, e.args = {e.args}')
            raise
        except Exception as e:
            self.logger.exception(f'other exception, e = {e}')
            raise
        elems_len = len(elems)
        self.logger.debug(f'elems_len = {elems_len}')
        return elems


class XGetPosts(SeleniumBase):
    def __init__(
        self,
        logger=None,
        wd=None,
        account=None,
        search_for=None,
    ):
        super().__init__(
            logger=logger,
            wd=wd,
        )
        self.account = account
        self.search_for = search_for

        # langs are in the <html> tag
        self.available_langs = ['en', 'nl']

        # pages
        self.pages = [
            {
                'name': 'home_page',
                'texts': {
                    'en': {
                        'login_button': 'Sign in',
                    },
                    'nl': {
                        'login_button': 'Inloggen',
                    },
                },
                'page_processor': self.home_page,
                'login_button_text_xpath': '//a[@role="link"]/div/span/span[text()[normalize-space()="' + '{{login_button_text}}' + '"]]'
            },
            {
                'name': 'login_page_1_email',
                'texts': {
                    'en': {
                        'title': 'Sign in to X',
                        'form_button': 'Next',
                    },
                    'nl': {
                        'title': 'Registreren bij X',
                        'form_button': 'Volgende',
                    },
                },
                'page_processor': self.login_page_1_email,
                'form_field_xpath': '//input[@type="text" and @name="text" and @autocomplete="username"]',
                'form_field_value': self.account['email'],
                'form_button_text_xpath': '//div[@role="button"]/div/span/span[text()[normalize-space()="' + '{{form_button_text}}' + '"]]'

            },
            {
                'name': 'login_page_2_password',
                'texts': {
                    'en': {
                        'title': 'Enter your password',
                        'form_button': 'Log in',
                    },
                    'nl': {
                        'title': 'Voer je wachtwoord in',
                        'form_button': 'Inloggen',
                    },
                },
                'page_processor': self.login_page_2_password,
                'form_field_xpath': '//input[@type="password" and @name="password" and @autocomplete="current-password"]',
                'form_field_value': self.account['password'],
                'form_button_text_xpath': '//div[@role="button"]/div/span/span[text()[normalize-space()="' + '{{form_button_text}}' + '"]]',
            },
            {
                'name': 'login_page_3_username',
                'texts': {
                    'en': {
                        'title': 'Enter your phone number or username',
                        'form_button': 'Next',
                    },
                    'nl': {
                        'title': 'Voer je telefoonnummer of gebruikersnaam',
                        'form_button': 'Volgende',
                    },
                },
                'page_processor': self.login_page_3_username,
                'form_field_xpath': '//input[@type="text" and @name="text" and @autocomplete="on"]',
                'form_field_value': self.account['username'],
                'form_button_text_xpath': '//div[@role="button"]/div/span/span[text()[normalize-space()="' + '{{form_button_text}}' + '"]]',
            },
            {
                'name': 'loggedin_page',
                'texts': {
                    'en': {
                    },
                    'nl': {
                    },
                },
                'page_processor': self.loggedin_page,
            },
        ]

        self.page_name_pages = {}
        for page in self.pages:
            self.page_name_pages[page['name']] = page

    def get_page_lang(self, url):
        self.logger.debug(f'(url = {url})')

        # get lang from html tag
        html_tag_xpath = '//html[@dir="ltr"]'
        html_tag_elem = self.wait_for_element(
            xpath=html_tag_xpath,
        )

        # lang must be present and available
        lang = html_tag_elem.get_attribute('lang')
        if lang not in self.available_langs:
            raise Exception(f'lang = {lang} not in  available_langs = {self.available_langs}')
        self.logger.debug(f'lang = {lang}')
        return lang

    def which_page(self, url, lang):
        self.logger.debug(f'(url = {url}, lang = {lang})')

        # construct list of items that identify:
        # - not logged in pages
        # - logged in
        or_conditions = []
        for page in self.pages:
            page_name = page['name']
            if page_name == 'home_page':
                # check for 'Sign in' button
                login_button_text = page['texts'][lang]['login_button']
                xpath = '//a[@href="/login" and @role="link" and @data-testid="loginButton"]/div[@dir="ltr"]/span/span[text()[normalize-space()="' + login_button_text + '"]]'
                self.logger.debug(f'xpath = {xpath}')
                or_conditions.append( EC.visibility_of_element_located((By.XPATH, xpath)) )

            elif page_name == 'login_page_1_email':
                # check for <h1> title
                title_text = page['texts'][lang]['title']
                xpath = '//h1/span/span[text()[normalize-space()="' + title_text + '"]]'
                self.logger.debug(f'xpath = {xpath}')
                or_conditions.append( EC.visibility_of_element_located((By.XPATH, xpath)) )

            elif page_name == 'login_page_2_password':
                # check for <h1> title
                title_text = page['texts'][lang]['title']
                xpath = '//h1/span/span[text()[normalize-space()="' + title_text + '"]]'
                self.logger.debug(f'xpath = {xpath}')
                or_conditions.append( EC.visibility_of_element_located((By.XPATH, xpath)) )

            elif page_name == 'login_page_3_username':
                # check for <h1> title
                title_text = page['texts'][lang]['title']
                xpath = '//h1/span/span[text()[normalize-space()="' + title_text + '"]]'
                self.logger.debug(f'xpath = {xpath}')
                or_conditions.append( EC.visibility_of_element_located((By.XPATH, xpath)) )

        # check if logged in using profile button
        xpath = '//a[@href="/' + self.account['screen_name'] + '" and @role="link" and @data-testid="AppTabBar_Profile_Link"]'
        self.logger.debug(f'xpath = {xpath}')
        or_conditions.append( EC.visibility_of_element_located((By.XPATH, xpath)) )

        # or_conditions
        self.logger.debug(f'or_conditions = {or_conditions}')

        wait = WebDriverWait(
            self.wd,
            timeout=self.web_driver_wait_timeout,
            poll_frequency=self.web_driver_wait_poll_frequency,
            ignored_exceptions=[
                selenium_exceptions.ElementNotVisibleException,
                selenium_exceptions.ElementNotSelectableException,
            ],
        )
        try:
            elem = wait.until(EC.any_of(*tuple(or_conditions)))
        except selenium_exceptions.TimeoutException as e:
            self.logger.exception(f'selenium_exceptions.TimeoutException, e = {e}, e.args = {e.args}')
            raise
        except Exception as e:
            self.logger.exception(f'other exception, e = {e}')
            raise

        # not logged in and a known page, ... or .... logged in
        self.logger.debug(f'elem.tag_name = {elem.tag_name}')
        self.get_elem_attrs(elem, dump=True)

        page = None
        if elem.tag_name == 'a':
            href = elem.get_attribute('href')
            if href == '/login':
                page = self.page_name_pages['home_page']
            else:
                data_testid = elem.get_attribute('data-testid')
                if data_testid == 'AppTabBar_Profile_Link':
                    page = self.page_name_pages['loggedin_page']
        elif elem.tag_name == 'span':
            elem_text = elem.text
            self.logger.debug(f'elem_text = {elem_text}')
            if elem_text == self.page_name_pages['home_page']['texts'][lang]['login_button']:
                page = self.page_name_pages['home_page']
            elif elem_text == self.page_name_pages['login_page_1_email']['texts'][lang]['title']:
                page = self.page_name_pages['login_page_1_email']
            elif elem_text == self.page_name_pages['login_page_2_password']['texts'][lang]['title']:
                page = self.page_name_pages['login_page_2_password']
            elif elem_text == self.page_name_pages['login_page_3_username']['texts'][lang]['title']:
                page = self.page_name_pages['login_page_3_username']
        elif elem.tag_name == 'h1':
            pass
        self.logger.debug(f'page = {page}')
        if page is None:
            raise Exception(f'page is None')
        return page

    def process_page(self, url):
        self.logger.debug(f'(url = {url})')

        lang = self.get_page_lang(url)
        page = self.which_page(url, lang)

        page_processor = page['page_processor']
        if page_processor is None:
            raise Exception(f'page_processor is None')

        return page_processor(page, url, lang)

    def login_page_123_processor(self, page, url, lang):
        self.logger.debug(f'(page = {page}, url = {url}, lang = {lang}')

        # get (optional) form_field and form_button
        form_field_xpath = page.get('form_field_xpath')
        form_field_value = page.get('form_field_value')
        form_field_elem = None
        if form_field_xpath is not None:
            form_field_elem = self.wait_for_element(
                xpath=form_field_xpath,
            )
        form_button_elem = self.wait_for_element(
            xpath=page['form_button_text_xpath'].replace('{{form_button_text}}', page['texts'][lang]['form_button']),
            parent_xpath='../../..',
            parent_attr_check={
                'role': 'button',
            },
        )
        # enter form_field_value and click
        self.set_form_field_value_and_click(
            form_field_elem=form_field_elem, 
            form_field_value=form_field_value,
            form_button_elem=form_button_elem,
        )
        # return current_url
        current_url = self.wd.current_url
        self.logger.debug(f'current_url = {current_url}')
        return current_url

    def home_page(self, page, url, lang):
        self.logger.debug(f'page = {page}, url = {url}, lang = {lang}')

        login_button_elem = self.wait_for_element(
            xpath=page['login_button_text_xpath'].replace('{{login_button_text}}', page['texts'][lang]['login_button']),
            parent_xpath='../../..',
            parent_attr_check={
                'role': 'link',
            },
        )
        href = login_button_elem.get_attribute('href')
        self.logger.debug(f'href = {href}')

        # redirect
        self.wd.get(href)
        self.random_wait(min_wait_secs=self.min_wait_for_next_page)

        # return current_url
        current_url = self.wd.current_url
        self.logger.debug(f'current_url = {current_url}')
        return current_url

    def login_page_1_email(self, page, url, lang):
        return self.login_page_123_processor(page, url, lang=lang)

    def login_page_2_password(self, page, url, lang):
        return self.login_page_123_processor(page, url, lang=lang)

    def login_page_3_username(self, page, url, lang):
        return self.login_page_123_processor(page, url, lang=lang)

    def loggedin_page(self, page, url, lang):
        self.logger.debug(f'page = {page}, url = {url}, lang = {lang}')

        # locate search box
        xpath = '//form[@role="search"]/div/div/div/div/label/div/div/input[@type="text" and @data-testid="SearchBox_Search_Input"]'
        search_field_elem = self.wait_for_element(xpath)

        # type the search item
        self.elem_send_keys_wait(
            search_field_elem,
            self.search_for,
            before_min_wait=2,
            after_min_wait=self.min_wait_for_next_page,
        )

        # locate search result option buttons
        xpath = '//div[@role="listbox" and starts-with(@id, "typeaheadDropdown-")]/div[@role="option" and @data-testid="typeaheadResult"]/div[@role="button"]'
        button_elems = self.wait_for_elements(xpath)

        # find search_for in options
        xpath = './/span[text()[normalize-space()="' + self.search_for + '"]]'
        found = False
        for button_elem in button_elems:
            try:
                elem = button_elem.find_element(By.XPATH, xpath)
                found = True
                break
            except selenium_exceptions.NoSuchElementException:
                continue
        if not found:
            raise Exception(f'search_for = {search_for} not found in typeaheadDropdown')

        # click the found item
        self.elem_click_wait(
            button_elem,
            before_min_wait=2,
            after_min_wait=self.min_wait_for_next_page,
        )

        # slurp posts visibility_of_element_located
        xpath = '//article[@data-testid="tweet"]'
        posts = self.wait_for_elements(xpath)

        # dump posts
        post_html_items = []
        posts_len = len(posts)
        visible_posts_len = 0
        for post in posts:
            self.logger.debug(f'tag = {post.tag_name}')
            self.logger.debug(f'aria-labelledby = {post.get_attribute("aria-labelledby")}')
            self.logger.debug(f'data-testid = {post.get_attribute("data-testid")}')
            self.logger.debug(f'post.is_displayed() = {post.is_displayed()}')
            if not post.is_displayed():
                continue
            visible_posts_len += 1

            # expand to html
            post_html = post.get_attribute('outerHTML')
            post_html_items.append(post_html)

        if posts_len > 0:
            with open('posts.txt', 'w') as fo:
                fo.write('\n'.join(post_html_items))

        self.logger.debug(f'posts_len = {posts_len}')
        self.logger.debug(f'visible_posts_len = {visible_posts_len}')
        self.random_wait(min_wait_secs=5)
        sys.exit()

def main():

    # your account
    account = {
        'email': 'johndoe@example.com',
        'password': 'secret',
        'username': 'johndoe',
        'screen_name': 'JohnDoe',
    }
    # your search
    search_for = '@elonmusk'

    with webdriver.Chrome(options=webdriver_options) as wd:
        x = XGetPosts(
            logger=logger,
            wd=wd,
            account=account,
            search_for=search_for,
        )
        url = 'https://www.x.com'
        wd.get(url)
        x.random_wait(min_wait_secs=x.min_wait_for_next_page)
        url = wd.current_url

        while True:
            logger.debug(f'url = {url}')
            url = x.process_page(url)
        logger.debug(f'ready ')

if __name__ == '__main__':
    main()

Résumé

Dans cet article, nous avons utilisé Selenium pour automatiser la connexion à X et extraire un certain nombre d'articles. Pour trouver des éléments, nous utilisons principalement (relativement) XPaths. Nous n'essayons pas de cacher le fait que nous utilisons l'automatisation, mais nous essayons de nous comporter autant qu'un humain. Le code supporte plusieurs langues. Le code fonctionne aujourd'hui mais peut échouer demain en raison de changements dans les textes et les noms.

Liens / crédits

ChromeDriver - WebDriver for Chrome
https://sites.google.com/a/chromium.org/chromedriver/capabilities

Playwright
https://playwright.dev/python

Puppeteer
https://pptr.dev

ScrapingAnt - Puppeteer vs. Selenium - Which Is Better? + Bonus
https://scrapingant.com/blog/puppeteer-vs-selenium

Selenium with Python
https://selenium-python.readthedocs.io/index.html

Selenium with Python - 5. Waits
https://selenium-python.readthedocs.io/waits.html

Tracking Modified Selenium ChromeDriver
https://datadome.co/bot-management-protection/tracking-modified-selenium-chromedriver

undetected_chromedriver
https://github.com/ultrafunkamsterdam/undetected-chromedriver

WebScrapingAPI
https://www.webscrapingapi.com

WebScrapingAPI
https://www.webscrapingapi.com

ZenRows - How to Avoid Bot Detection with Selenium
https://www.zenrows.com/blog/selenium-avoid-bot-detection

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.