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

X Webautomatisierung und Scraping mit Selenium

Bei kleinen Selenium -Projekten verhindern wir die automatische Erkennung nicht, sondern versuchen, das menschliche Verhalten zu imitieren.

11 August 2023 Aktualisiert 11 August 2023
post main image
https://unsplash.com/@nixcreative

Wenn Sie Daten aus dem Web auslesen wollen, müssen Sie wissen, was Sie tun. Sie wollen einen Zielserver nicht mit Anfragen überlasten. Wenn Sie dies von einem einzigen Standort aus tun, einem IP address, könnten Sie eine (vorübergehende) Sperre erhalten.

Wenn Sie im großen Stil scrapen wollen, sollten Sie einen speziellen Dienst wie ZenRows, ScrapFly, WebScrapingAPI, ScrapingAnt usw. nutzen. Sie distverteilen Ihre Anfragen auf viele Systeme, jedes mit einer einzigartigen IP address, was bedeutet, dass der Zielserver denkt, dass viele verschiedene (menschliche) Clients auf ihn zugreifen.

Aber hey, manchmal wollen wir nur ein paar Daten auslesen, sagen wir, wir wollen jeden Tag ein paar neue Beiträge von Leuten auf einer Social-Media-Plattform wie X auslesen.

In diesem Beitrag werden wir Selenium verwenden. Es gibt Lösungen wie Puppeteer oder Playwright, aber Selenium gibt es seit 2004, es ist das am weitesten verbreitete Tool für automatisierte Tests und hat eine große Community.

Ich werde mit einigen Überlegungen beginnen und mit etwas Arbeitscode enden. Der Code automatisiert das Einloggen in X, und dann das Durchsuchen und Verschrotten einiger Nachrichten. Ich weiß, dass sich morgen alles ändern kann, aber heute funktioniert es.

Wie immer benutze ich Ubuntu 22.04.

Scraping von X mit Selenium

Kürzlich hat X entschieden, dass man sich anmelden muss, um Beiträge zu sehen. Das finde ich nicht gut. Die meiste Zeit lese ich nur die Beiträge einiger weniger Leute, indem ich die Browsererweiterung 'Breakthrough Twitter Login Wall' benutze. Das funktioniert nicht mehr.

Und manchmal scrape ich einige Daten, aber nicht Hunderttausende von Tweets. Ich glaube, viele Entwickler machen das so. Es gibt fast 30 Millionen Softwareentwickler auf der Welt. Nehmen wir nun an, dass einer von tausend einmal im Jahr einige X Posts scrapen, und zwar im Durchschnitt jeweils 1000 Posts, für kleine Projekte, zum Testen und zum Spaß (Lernen). Das sind 30.000 Scraper pro Jahr, 100 Scraper pro Tag, also 100.000 Beiträge pro Tag. Das ist nicht wirklich viel, wenn man bedenkt, dass jeden Tag Millionen von Beiträgen in . X veröffentlicht werden, aber dieses Konto ist nur zum Schreiben gedacht. Wenn wir Daten abrufen wollen, können wir nur die X -Website verwenden und mit Tools wie Selenium darauf zugreifen.

Selenium ist ein sehr leistungsfähiges Tool für automatisierte Tests, kann aber auch für Web Scraping verwendet werden. Es wurde 2004 veröffentlicht und hat eine große Community. Andere beliebte Tools, die Sie in Betracht ziehen sollten, sind Puppeteer und Playwright.

Menschliches Verhalten imitieren

Ich würde sagen, es ist nicht illegal, eine Webseite zu scrapen, solange Sie die gescrapten Daten nicht in einer erkennbaren Form veröffentlichen. Allerdings kann ich mir vorstellen, dass einige Unternehmen den Zugang zu ihren Webseiten und Diensten für automatisierte Software sperren.

Was genau ist automatisierte Software und wie kann sie erkannt werden? Die naheliegendste Prüfung besteht darin, festzustellen, ob sich das Verhalten eines Besuchers vom menschlichen Verhalten unterscheidet. Das bedeutet in der Regel, dass sich Ihre automatisierte Software wie ein Mensch verhalten sollte. Geben Sie etwas ein und warten Sie ein paar Sekunden, klicken Sie auf eine Schaltfläche und warten Sie ein paar Sekunden, scrollen Sie nach unten und warten Sie ein paar Sekunden, usw. Ein Mensch kann ein Formular nicht in 50 Millisekunden ausfüllen ...

Betrachten wir ein anderes Szenario, in dem Selenium verwendet wird, um Seiten einer Website per Sprachsteuerung zu öffnen. Selbst wenn die Website erkennt, dass wir Selenium verwenden, scheint dies völlig legitim zu sein, denn es ist ganz klar, dass die Aktionen von einem Menschen ausgeführt werden.

Der Besuch einer Website und der Kauf eines billigen Produkts ist menschliches Verhalten, aber der Kauf aller billigen Produkte in großen Mengen ist verdächtig. Vor allem, wenn dies Tag für Tag geschieht.

Das bedeutet, dass unsere Software in erster Linie menschliches Verhalten imitieren sollte. Etwas tun und eine zufällige Anzahl von Sekunden warten, etwas anderes tun und eine zufällige Anzahl von Sekunden warten, usw.

Browser-Auswahl

Selenium unterstützt viele Browser. Für dieses Projekt verwende ich Chrome. Der Grund ist ganz einfach: Chrome ist der meistgenutzte Browser, nicht nur von Internet users, sondern auch von Entwicklern. Auf meinem Entwicklerrechner verwende ich Firefox, Chromium und (manchmal) Brave. Das bedeutet, dass ich Chrome ausschließlich für dieses Projekt verwenden kann. Wenn Sie Chrome auch zum Surfen im Internet verwenden, können Sie ein separates Profil für die Scraping-Anwendung erstellen.

Scraper-Bot-Erkennung

Ein Scraper-Bot ist ein Tool oder ein Stück Code, das zum Extrahieren von Daten aus Webseiten verwendet wird. Web Scraping ist nicht neu, und viele Unternehmen haben Maßnahmen ergriffen, um den Zugriff automatisierter Tools auf ihre Websites zu verhindern. Der Artikel "How to Avoid Bot Detection with Selenium" (siehe Links unten) listet mehrere Möglichkeiten auf, um die Erkennung von Bots zu vermeiden.

Wenn wir nichts tun, ist es sehr einfach, unseren Selenium -Bot zu entdecken. Solange wir uns wie ein Mensch verhalten, ist das nicht unbedingt schlecht. Wir zeigen der Zielwebsite, dass wir entweder Skript-Kiddies sind oder dass wir wollen, dass sie merken, dass wir eine Automatisierung verwenden.

Dennoch können uns einige Unternehmen aus ihren eigenen legitimen Gründen blockieren. Seien wir ehrlich, ein guter Bot kann plötzlich ein sehr schlechter Bot werden.

Es gibt einige Websites, mit denen wir feststellen können, ob unser Bot als automatisierte Software erkannt wird:

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

Hier finden Sie einen Beispielcode, mit dem Sie dies ausprobieren können:

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

Werden wir blockiert werden?

Je mehr wir versuchen, die Erkennung unseres Selenium -Bots zu verhindern, desto verdächtiger erscheinen wir den Bot-Detektoren.

Es gibt ein Projekt "undetected_chromedriver" (siehe Links unten), das versucht, einen Bot zu entwickeln, der auf Selenium basiert und nicht entdeckt werden kann. Wenn er jedoch entdeckt wird, kann die Ziel-Website beschließen, Sie zu blockieren, denn ... warum sollte jemand unerkennbar sein wollen?

Unternehmen, die Bot-Detektoren entwickeln oder Dienste für Bot-Detektoren anbieten, verfolgen Projekte wie "undetected_chromedriver" sehr genau und versuchen, alle Methoden zur Verhinderung der Erkennung zu unterlaufen.

Auch Aktualisierungen sind ein Problem. Eine neue Version von Selenium oder Chrome kann die Maßnahmen zur Verhinderung der Erkennung zunichte machen. Aber auch wenn Chrome lange Zeit nicht aktualisiert wird, kann dies verdächtig sein und zu einer Entdeckung führen.

Was sollten wir also tun? Nichts, ein wenig, oder zum Beispiel 'undetected_chromedriver' verwenden? Wenn Sie große Datenmengen scrapen wollen, verwenden Sie am besten einen Scraping-Dienst wie die oben genannten. Für kleine Projekte schlage ich vor, dass Sie nichts tun, um die Erkennung zu verhindern und zu sehen, was passiert.

Müssen wir die Anmeldung wirklich automatisieren?

Anstatt die Anmeldung und das Einloggen über unser Skript zu automatisieren, können wir uns auch manuell über den Browser anmelden und dann die Daten aus dem Browser (Profildaten, Sitzung) verwenden, wenn wir uns mit X. verbinden. Aber ... unsere Anwendung ist in diesem Fall nicht vollständig automatisiert. Und hey, wir benutzen Selenium, das Web-Automatisierungswerkzeug (!).

Über den Code

Der nachstehende Code automatisiert die Anmeldung bei X und kratzt dann einige Beiträge eines ausgewählten user. Sobald dies erledigt ist, wird das Skript beendet. Es verwendet ein separates Profil für den Webbrowser, was bedeutet, dass es die normale Browsernutzung nicht beeinträchtigen sollte. Wenn die Anmeldung erfolgreich war und das Skript existiert, meldet es sich beim nächsten Start des Skripts nicht mehr an, sondern beginnt sofort mit der Auswahl des user und dem Scraping der Beiträge.

Die Beiträge des ausgewählten user werden in eine Datei "posts.txt" geschrieben. Das bedeutet, dass der Code die Teile der Beiträge nicht extrahiert. Sie können dafür BeautifulSoup verwenden.

Erkennen von Seiten

Unser Skript befasst sich mit den folgenden Seiten:

Wenn Sie nicht eingeloggt sind:

  • Startseite
  • Login - E-Mail eingeben
  • Einloggen - Passwort eingeben
  • Login - username eingeben (bei ungewöhnlicher Aktivität)

Wenn eingeloggt:

  • Eingeloggte Seite

Die Seite "Ungewöhnliche Aktivitäten" wird manchmal angezeigt, z. B. wenn Sie bereits über einen anderen Browser eingeloggt sind, und zwar vor der Seite "Passwort eingeben".

Was wir in einer Schleife tun:

  • Extrahieren der Sprache
  • Erkennen, welche Seite angezeigt wird
  • Ausführen der Aktion für die Seite

Wenn Sie sich auf der Startseite befinden, leiten wir Sie einfach zur Anmeldeseite weiter. Alle anderen nicht eingeloggten Seiten haben einen Titel, ein "Formularfeld" und eine "Formularschaltfläche". Das bedeutet, dass wir sie auf ähnliche Weise behandeln können.

Und wenn wir von Seiten sprechen, reden wir nicht von URLs, sondern von dem, was auf dem Bildschirm angezeigt wird. X ist alles Javascript.

Der Detektor "Auf welcher Seite befinden wir uns?

Dies ist wahrscheinlich der "spannendste" Teil des Codes. Zunächst wird eine Liste von sich gegenseitig ausschließenden 'presence_of_element_located'-Elementen erstellt:

  • Profil-Schaltfläche => eingeloggt
  • Anmelden-Button mit Text 'Anmelden' => 'home_page'
  • <h1> Text 'Anmelden bei X' => 'login_page_1_email'
  • <h1> text 'Geben Sie Ihr Passwort ein' => 'login_page_2_password'
  • <h1> text 'Geben Sie Ihre Telefonnummer oder Ihren userNamen ein' => 'login_page_3_username'

Dann warten wir darauf, dass mindestens eines der Elemente gefunden wird:

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

Sobald wir das Element haben, können wir die Seite bestimmen. Und wenn wir die Seite haben, können wir die entsprechende Aktion ausführen.

Oops: 'Etwas ist schief gelaufen. Versuchen Sie, neu zu laden.

Beim Einloggen erscheint diese Meldung manchmal. Ich sehe sie zu zufälligen Zeiten, daher frage ich mich, ob sie absichtlich eingebaut worden ist. Der Code erzeugt einen Timeout-Fehler, wenn dies geschieht. Sie müssen das Skript neu starten oder eine eigene Wiederholungsfunktion implementieren.

Sprachen

Es gibt zwei Sprachen, mit denen wir umgehen müssen: die Sprache des nicht angemeldeten Benutzers und die Sprache des Kontos. Oft sind sie identisch. Der folgende Code läuft für die Sprachen Englisch und Niederländisch, auch gemischt. Die Sprache wird aus der Seite extrahiert.

Um eine neue Sprache hinzuzufügen, fügen Sie sie zu:

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

und fügen Sie die Texte zu den Seiten hinzu:

    # pages
    self.pages = [
        ...
    ]

Natürlich müssen Sie die Texte zuerst mit Hilfe der manuellen Anmeldung nachschlagen.

Um den Browser in einer anderen Sprache zu starten, entfernen Sie zunächst das Profilverzeichnis des Browsers:

rm -R browser_profile

und starten Sie dann das Skript, nachdem (!) Sie das neue Gebietsschema eingestellt haben:

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

XPath Suche

Wenn Sie vom Stamm des Dokuments aus suchen wollen, beginnen Sie XPath mit '//'.
Wenn Sie relativ zu einem bestimmten Element suchen wollen, beginnen Sie XPath mit './/'.

Beiträge

Im Moment werden die sichtbaren Beiträge als HTML in die Datei 'posts.txt' geschrieben. Um mehr Beiträge zu erhalten, implementieren Sie Ihre eigene Scroll-Down-Funktion.

Extrahieren von Daten aus Beiträgen

Dies ist Ihnen überlassen. Ich schlage vor, Sie verwenden BeautifulSoup.

Der Code

Falls Sie es ausprobieren wollen, hier ist der Code. Fügen Sie vor der Ausführung Ihre Kontodaten und den Suchnamen hinzu.

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

Zusammenfassung

In diesem Beitrag haben wir Selenium verwendet, um die Anmeldung bei X zu automatisieren und eine Reihe von Beiträgen zu extrahieren. Um Elemente zu finden, verwenden wir hauptsächlich (relativ) XPaths. Wir versuchen nicht, die Tatsache zu verbergen, dass wir Automatisierung verwenden, sondern versuchen, uns so zu verhalten wie ein Mensch. Der Code unterstützt mehrere Sprachen. Der Code funktioniert heute, kann aber morgen aufgrund von Text- und Namensänderungen versagen.

Links / Impressum

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.