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

X Automatización web y scraping con Selenium

En los pequeños proyectos Selenium no impedimos la detección automática, pero intentamos imitar el comportamiento humano.

11 agosto 2023 Actualizado 11 agosto 2023
post main image
https://unsplash.com/@nixcreative

Cuando se quieren extraer datos de la Web, hay que saber lo que se hace. No quieres sobrecargar un servidor de destino con peticiones. Si lo haces desde un único sitio, un IP address, podrías recibir un baneo (temporal).

Si quieres hacer un scrape grande, considera usar un servicio dedicado como ZenRows, ScrapFly, WebScrapingAPI, ScrapingAnt, etc. distribuyen tus peticiones a través de un montón de sistemas, cada uno con un IP address único, lo que significa que el servidor de destino pensará que está siendo accedido por muchos clientes (humanos) diferentes.

Pero bueno, a veces sólo queremos raspar un poco de datos, digamos que queremos raspar las nuevas publicaciones de algunas personas en una plataforma de medios sociales como X cada día.

En este post, usaremos Selenium. Hay soluciones como Puppeteer o Playwright, pero Selenium existe desde 2004, es la herramienta más utilizada para pruebas automatizadas y tiene una gran comunidad.

Empezaré con algunas consideraciones y terminaré con algo de código de trabajo. El código automatiza el inicio de sesión en X, y luego busca y desecha algunos mensajes. Sé que todo puede cambiar mañana, pero hoy esto funciona.

Como siempre estoy ejecutando Ubuntu 22.04.

Scraping desde X con Selenium

Recientemente X decidió que debes iniciar sesión para ver los mensajes. Esto no me gusta. La mayor parte del tiempo sólo leo los mensajes de unas pocas personas, utilizando la extensión del navegador 'Breakthrough Twitter Login Wall'. Esto ya no funciona.

Y a veces raspo algunos datos, pero no cientos de miles de tweets. Creo que muchos desarrolladores hacen esto. Hay casi 30 millones de ingenieros de software en el mundo. Ahora supongamos que uno de cada mil a veces scrapea algunos X posts una vez al año, que scrapean una media de 1000 posts cada uno, para pequeños proyectos, pruebas y diversión (aprendizaje). Es decir, 30.000 scrapers al año, 100 scrapers al día, 100.000 mensajes al día. Esto no es mucho teniendo en cuenta los millones de mensajes diarios. X sigue teniendo una cuenta API gratuita, pero sólo de escritura. Si queremos raspar datos, sólo podemos utilizar el sitio web de X y acceder a él con herramientas como Selenium.

Selenium es una herramienta muy potente para pruebas automatizadas, pero también se puede utilizar para el web scraping. Se lanzó en 2004 y cuenta con una gran comunidad. Otras herramientas populares que puedes considerar son Puppeteer y Playwright.

Imitar el comportamiento humano

Yo diría que no es ilegal raspar una página web siempre y cuando no se publiquen los datos raspados de forma reconocible. Dicho esto, me imagino a algunas empresas bloqueando el acceso a sus páginas web y servicios al software automatizado.

¿Qué es exactamente el software automatizado y cómo puede detectarse? La comprobación más obvia es determinar si el comportamiento de un visitante difiere del comportamiento humano. Esto suele significar que el software automatizado debe comportarse como un humano. Escriba algo y espere unos segundos, haga clic en un botón y espere unos segundos, desplácese hacia abajo y espere unos segundos, etc. Un humano no puede rellenar un formulario en 50 milisegundos...

Consideremos otro escenario, en el que Selenium se utiliza para abrir páginas de un sitio web mediante control por voz. Incluso si el sitio web detecta que estamos utilizando Selenium, esto parece perfectamente legítimo porque está muy claro que las acciones son realizadas por un humano.

Recorrer un sitio web y comprar un producto barato es un comportamiento humano, pero comprar todos los productos baratos en grandes cantidades es sospechoso. Especialmente si esto ocurre día tras día.

Esto significa que lo principal que debe hacer nuestro software es imitar el comportamiento humano. Hacer algo y esperar un número aleatorio de segundos, hacer otra cosa y esperar un número aleatorio de segundos, etc.

Selección del navegador

Selenium soporta muchos navegadores. Para este proyecto, estoy usando Chrome. La razón es obvia: Chrome es el navegador más utilizado, no sólo por internet users, sino también por los desarrolladores. En mi máquina de desarrollador, utilizo Firefox, Chromium y (a veces) Brave. Esto significa que puedo utilizar Chrome exclusivamente para este proyecto. Si utilizas Chrome también para la navegación web, puedes crear un perfil independiente para la aplicación de scraping.

Detección de bots

Un bot de raspado es una herramienta o pieza de código utilizada para extraer datos de páginas web. El scraping web no es nuevo y muchas organizaciones han tomado medidas para evitar que herramientas automatizadas accedan a sus sitios web. En el artículo 'How to Avoid Bot Detection with Selenium', ver enlaces más abajo, se enumeran varias formas de evitar la detección de bots.

Si no hacemos nada, es muy fácil que detecten nuestro bot Selenium . Mientras nos comportemos como un humano, esto puede no ser malo. Mostramos al sitio web de destino que somos script kiddies o que queremos que noten que estamos usando alguna automatización.

Aún así, algunas empresas pueden bloquearnos por sus propias razones legítimas. Seamos realistas, un buen bot de repente puede convertirse en un bot muy malo.

Hay algunos sitios que podemos utilizar para determinar si nuestro bot es reconocido como software automatizado:

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

Aquí tienes un ejemplo de código que puedes utilizar para probarlo:

# 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()

¿Nos bloquearán?

Cuanto más intentemos evitar la detección de nuestro bot Selenium , más sospechosos pareceremos a los detectores de bots.

Hay un proyecto 'undetected_chromedriver', ver enlaces más abajo, que intenta ofrecer un bot, basado en Selenium, que no puede ser detectado. Pero si se detecta, el sitio web de destino puede decidir bloquearlo porque... ¿por qué querría alguien ser indetectable?

Las empresas que desarrollan detectores de bots u ofrecen servicios de detección de bots, siguen de cerca proyectos como 'undetected_chromedriver' e intentan vencer cualquier método de prevención de detección que se implemente.

Las actualizaciones también son un problema. Una nueva versión de Selenium o Chrome puede romper las medidas de prevención de detección. Pero no actualizar Chrome durante mucho tiempo también puede ser detectable y sospechoso.

Entonces, ¿qué debemos hacer? ¿Nada, un poco, o usar por ejemplo 'undetected_chromedriver'? Si quieres hacer un scraping grande, lo mejor es utilizar un servicio de scraping como los mencionados anteriormente. Para proyectos pequeños te sugiero que no hagas nada para evitar la detección y veas qué pasa.

¿Realmente necesitamos automatizar el inicio de sesión?

En lugar de automatizar el inicio de sesión e iniciar sesión a través de nuestro script, también podemos iniciar sesión manualmente utilizando el navegador, y luego utilizar los datos del navegador (datos de perfil, sesión) cuando se conecta a X. Esto ahorra un montón de código. Pero ... nuestra aplicación no está totalmente automatizada en este caso. Y oye, estamos usando Selenium, la herramienta de automatización web (!).

Sobre el código

El código a continuación automatiza el inicio de sesión a X y luego raspa algunos puestos de un user seleccionado. Una vez hecho esto, el script se cierra. Utiliza un perfil separado para el navegador web, lo que significa que no debería interferir con el uso normal del navegador. Si el inicio de sesión se ha realizado correctamente y el script existe, la próxima vez que se inicie el script ya no se iniciará la sesión, sino que empezará inmediatamente a seleccionar el user y a extraer los mensajes.

Los mensajes del user seleccionado se vuelcan en un archivo 'posts.txt'. Esto significa que el código no extrae las partes de los mensajes. Para ello, puede utilizar BeautifulSoup .

Reconocimiento de páginas

Nuestro script se ocupa de las siguientes páginas:

Cuando no se ha iniciado sesión:

  • Página de inicio
  • Login - introducir email
  • Inicio de sesión - introduzca la contraseña
  • Login - introduzca username (en actividad inusual)

Si ha iniciado sesión:

  • Página de inicio de sesión

La 'Página de actividad inusual' se muestra a veces, por ejemplo cuando ya has iniciado sesión a través de otro navegador, antes de la página de 'introducir contraseña'.

Lo que hacemos en un bucle

  • Extraer el idioma
  • Detectar qué página se muestra
  • Realizar la acción correspondiente a la página

En la página de inicio, simplemente redirigimos a la página de inicio de sesión. Todas las demás páginas de no inicio de sesión tienen un título, un "campo de formulario" y un "botón de formulario". Esto significa que podemos manejarlas de manera similar.

Y cuando hablamos de páginas, no estamos hablando de URLs, sino de lo que se muestra en la pantalla. X es todo Javascript.

El detector "en qué página estamos

Esta es probablemente la parte más "emocionante" del código. Primero creamos una lista de elementos 'presence_of_element_located' mutuamente excluyentes:

  • Profile-button => conectado
  • Log in botón con texto 'Sign in' => 'home_page'
  • <h1> text 'Iniciar sesión en X' => 'login_page_1_email'
  • <h1> text 'Introduce tu contraseña' => 'login_page_2_password'
  • <h1> text 'Introduce tu número de teléfono o username' => 'login_page_3_username'

A continuación esperamos a que se localice al menos uno de los elementos:

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

Una vez que tenemos el elemento, podemos determinar la página. Y una vez que tenemos la página, podemos ejecutar la acción apropiada.

Oops: 'Algo ha ido mal. Intente recargar'.

Al iniciar sesión, este mensaje aparece a veces. Lo veo en momentos aleatorios, así que me pregunto si ha sido construido a propósito. El código genera un error de tiempo de espera cuando esto sucede. Necesitas reiniciar el script o implementar tu propia función de reintento.

Idiomas

Hay dos lenguajes con los que debemos tratar, el lenguaje de no-logueado y el lenguaje de la cuenta. A menudo son el mismo. El código siguiente se ejecuta para los idiomas inglés y neerlandés, incluso mezclados. El idioma se extrae de la página.

Para añadir un nuevo idioma, añádelo a:

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

y añada los textos a las páginas:

    # pages
    self.pages = [
        ...
    ]

Por supuesto, primero debe buscar los textos mediante el inicio de sesión manual.

Para iniciar el navegador en otro idioma, primero elimine el directorio del perfil del navegador:

rm -R browser_profile

y luego inicie el script después (!) de establecer la nueva configuración regional:

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

XPath búsqueda

Si quieres buscar desde la raíz del documento, empieza XPath con '//'.
Si quieres buscar relativo a un elemento en particular, empieza XPath con './/'.

Entradas

Por el momento, los mensajes visibles se escriben como HTML en el archivo 'posts.txt'. Para obtener más entradas, implementa tu propia función de desplazamiento hacia abajo.

Extraer datos de un mensaje

Esto depende de ti. Te sugiero que utilices BeautifulSoup.

El código

Por si quieres probar, aquí tienes el código. Antes de ejecutarlo, asegúrate de añadir los datos de tu cuenta y el nombre de búsqueda.

# 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()

Resumen

En este post hemos utilizado Selenium para automatizar el inicio de sesión en X y extraer una serie de entradas. Para encontrar elementos, utilizamos sobre todo (relativamente) XPaths. No tratamos de ocultar el hecho de que utilizamos la automatización, pero tratar de comportarse tanto como un ser humano. El código admite varios idiomas. El código funciona hoy pero puede fallar mañana debido a cambios en los textos y en la nomenclatura.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.