Instagram Crawler findet nur begrenzt Beiträge

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
Zephry
User
Beiträge: 4
Registriert: Donnerstag 16. April 2020, 12:55

Hi Zusammen,

habe erst vor kurzen von anderen Programmiersprachen auf Python gewechselt und wollte als erstes Projekt mal einen Instagram Crawler schreiben.
Soweit würde er auch funktionieren, wenn man jedoch einen Account mit ca. über 40 Beiträgen angibt, findet das Programm irgendwann keine Bilder mehr.
Hier mein Code:

Code: Alles auswählen

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from time import sleep


options = webdriver.ChromeOptions()
options.add_argument('headless')
options.add_argument("disable-gpu")
options.add_argument("--log-level=3")
print('Chrome Sitzung wird geöffnet...')
browser = webdriver.Chrome('chromedriver.exe', options=options)

# browser = webdriver.Chrome('chromedriver.exe')
# browser.set_window_position(-10000,0)

print('Angeforderte Seite wird geladen...')
browser.get('https://www.instagram.com/accounts/login/')

sleep(1)

browser.find_element_by_name('username').send_keys('Username')
browser.find_element_by_name('password').send_keys('Password')
browser.find_element_by_name('password').send_keys(Keys.ENTER)

print('Login abgeschlossen...')
sleep(5)

browser.get('https://www.instagram.com/best_joudlerz_')

# articels = browser.find_elements_by_xpath("(//span[@class='g47SY '])").get_attribute('innerHTML')
articels = browser.find_element_by_xpath("(//span[@class='g47SY '])").get_attribute('innerHTML')
num_articels = ""

for i in articels:
    if i != ".":
        num_articels = num_articels + i

articels = int(num_articels)

found = []

for i in range(articels):
    
    try:
        found.append(browser.find_element_by_xpath("(//img[@class='FFVAD'])[" + str(i+1) + "]"))
        print(i+1, " from", articels)
        
    except:
        print("Scroll...")
        browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        sleep(3)
    
for k in found:
    print(k.get_attribute('src'))
    print("\n")
Wollte erst mal grob mein Programm planen bevor ich es mit Klassen schreibe...

Zur Erklärung:
Instagram zeigt ja die ersten 12 Beitäge an, wenn man nach unten Scrollt werden die nächsten, sofern vorhanden, per Ajax geladen.
Mein Programm sucht also solang Beiträge, bis diese nicht mehr existieren. Dann scrollt das Script nach unten, sodass die nächsten Beiträge geladen werden usw.
Klappt ja auch, nur ab den 40 / 43 Beitag werden dann keine Bilder mehr gefunden.
Wenn ich Chrome auf sichtbar stell, werden die Beiträge jedoch korrekt geladen.

Könnte mir da jemand helfen?
Vielen Dank!
Zephry
User
Beiträge: 4
Registriert: Donnerstag 16. April 2020, 12:55

Denke ich hab nun den Fehler gefunden:

Hab das ganze nämlich mal mit JavaScript gemacht und siehe da: das gleiche Verhalten!
Jedoch kommt z.B. bei

Code: Alles auswählen

document.getElementsByClassName('FFVAD')[47]
ein anderer Tag aus, als wenn man nun weiter runter scrollt.
Instagram "verdeckt" also die Beiträge, die weiter oben im Browser sind, wenn der Benutzer (oder halt das Script) scrollt.

Die Lösung für das Script poste ich nachher noch!
Zephry
User
Beiträge: 4
Registriert: Donnerstag 16. April 2020, 12:55

Hier der funktionierende Code:

Code: Alles auswählen

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from time import sleep

class crawl:
    def __init__(self, username, password, account):
        self.username = username
        self.password = password
        self.account = account

    def get_post(self):
        self.create_chrome_session()
        self.login()
        self.get_site()
        self.get_posts()
        
    def create_chrome_session(self):
        print("Chrome Sitzung wird vorbereitet...")
        options = webdriver.ChromeOptions()
        #options.add_argument('headless')
        #options.add_argument("disable-gpu")
        #options.add_argument("--log-level=3")
        
        try:
            print("Chrome Sitzung wird geöffnet...")
            self.browser = webdriver.Chrome('chromedriver.exe', options=options)
        except:
            print("\nDer Google Chrome Treiber wurde nicht gefunden...\nDownloade den zu passenden Treiber und verschiebe ihn mit dem Namen 'chromedriver.exe' in das aktuelle Verzeichnis")
            quit()
            
    def login(self):
        print('Login wird versucht...')
        self.browser.get('https://www.instagram.com/accounts/login/')
        sleep(1)
        self.browser.find_element_by_name('username').send_keys(self.username)
        self.browser.find_element_by_name('password').send_keys(self.password)
        self.browser.find_element_by_name('password').send_keys(Keys.ENTER)
        sleep(6)
        
        try:
            self.browser.find_element_by_xpath("(//div[@class='_47KiJ'])")
        except:
            print("\nBenutzername oder Passwort waren nicht korrekt...")
            quit()
            
        print('Login erfolgreich')
            
    def get_site(self):
        try:
            print("'", self.account, "' wird gesucht...")
            self.browser.get(self.account)
        except:
            print("\nEs scheint so, als ob '", self.account, "' keine gültige Instagram-Adresse sei\nBitte verwende folgenden Pattern: https://instagram.com/USERNAME")

    def get_posts(self):
        try:
            articels = self.browser.find_element_by_xpath("(//span[@class='g47SY '])").get_attribute('innerHTML')
        except:
            print("\nBenutzer wurde nicht gefunden...")
            quit()
        
        num_articels = ""

        for i in articels:
            if i != ".":
                num_articels = num_articels + i

        articels = int(num_articels)

        found = []
        iterat = 1
        i = 0
        
        while i < articels:
            
            try:
                
                tag = self.browser.find_element_by_xpath("(//img[@class='FFVAD'])[" + str(iterat) + "]").get_attribute('src')
                if tag not in found:
                    
                    found.append(tag)
                    print("[", i, " /", articels, "]")
                    
                else:
                    i = i - 1
                
            except:
                self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                sleep(3)
                iterat = 0
                i = i - 1
                
            iterat = iterat + 1
            i = i + 1
            
        counter = 1
        for k in found:
            print(counter, k)
            print("\n")
            counter = counter + 1

        print(counter)

c = crawl('Benutezrname', 'Passwort', 'https://instagram.com/USERNAME')
c.get_post()
quit()
Benutzeravatar
__blackjack__
User
Beiträge: 13080
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Zephry: Die Klasse ist sehr komisch. Das sieht eher aus wie normale Funktionen die aus irgend einem nicht ersichtlichen Grund in eine Klasse gestopft wurden.

Auf Modulebene sollte nur Code stehen, der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Klassennamen werden in Python ”MixedCase” geschrieben. Also `Crawl`, wobei das vom Inhalt eher ein Funktionsname wäre, weil es eine Tätigkeit beschreibt. Als Klasse würde das `Crawler` heissen. Wobei wie gesagt die Klasse IMHO überhaupt gar keinen Sinn macht.

`quit()` gibt es so eigentlich gar nicht. Das existiert zwar global, weil das für die interaktive Python-Shell dokumentiert ist, aber nicht für Programme. Es ist auch total unsauber und unübersichtlich welche Methoden/Funktionen hier die Macht haben einfach so das gesamte Programm abzubrechen. Das `quit()` am Ende des Programs ist auch überflüssig. Man verwendet am besten gar keine Aufrufe die das Programm abbrechen, sondern strukturiert den Programmablauf ordentlich, so dass er zu einem ”natürlichen" Ende kommt. Falls man dem aufrufenden Prozess einen anderen Rückgabecode als 0 liefern möchte, *dann* kann man `sys.exit(return_code)` dafür verwenden. In der `main()`-Funktion, oder einer die semantisch Nahe mit dem Hauptprogramm verwandt ist. Nicht in Funktionen die einzeln für sich eine Programmlogik bezogene Tätigkeit durchführen.

Die ganzen `print()`-Ausgaben gehören eigentlich auch nicht in Funktionen mit Programmlogik. Da könnte man Logging betreiben, aber keine unbedingte Kommunikation mit dem Benutzer.

`get_post()` ist als Name inhaltlich Falsch. Weder das `get` stimmt — bei so einem Namen erwartet man, dass die Methode/Funktion etwas als Ergebnis liefert — noch `post` als Einzahl stimmt, und die Methode/Funktion macht auch noch deutlich mehr vorarbeit, die man nicht bei dem Namen vermuten würde.

Bei `get_posts()` haben wir wieder einen irreführenden Namen, weil die Methode/Funktion überhaupt gar nichts zurück gibt. Sollte sie vielleicht, denn sie sammelt ja Daten aus der Webseite, die als Ergebnis taugen würden.

Das `articles` und `num_articles` erst an eine Zeichenkette gebunden werden um dann nach der Schleife über die Zeichen von `articles` den gleichen Namen an eine Zahl zu binden ist extrem verwirrend. `i` ist auch kein Name für was man für etwas anderes als eine ganze Zahl verwenden sollte, *insbesondere* wenn das auch noch eine Laufvariable ist. In der gleichen Funktion wird dann später `i` auch an ganze Zahlen gebunden. Das man in Python innerhalb der gleichen Funktion den selben Namen nacheinander an alle möglichen Werte mit den verschiedensten Datentypen binden *kann*, heisst nicht, dass das auch nur annähernd eine gute Idee ist das auch zu *tun*.

Das hier:

Code: Alles auswählen

        num_articels = ""

        for i in articels:
            if i != ".":
                num_articels = num_articels + i

        articels = int(num_articels)
Ist eine recht umständliche und nicht leicht lesbare Art das hier zu schreiben:

Code: Alles auswählen

        articels = int(articels.replace(".", ""))
Wobei auch hier immer noch gilt, dass es eher keine gute Idee ist, dass `articels` vorher an eine Zeichenkette und nachher an eine Zahl gebunden ist.

`found` ist IMHO zu generisch benannt, weil man sich erst den Code anschauen muss um eine Idee davon zu bekommen was diese Liste dann tatsächlich enthält. `image_paths` oder so etwas in der Richtung wäre besser. Und da die beiden Operationen darauf hinzufügen von Elementen und testen ob Element schon enthalten sind, würde sich hier eine Menge statt einer Liste anbieten.

`i` und `iterat` sind auch keine guten Namen. Das eine ist der Bildindex bezogen auf alle Bilder, und das andere der Index relativ zum angezeigten Webseitenausschnitt. Das sollte man den Namen mindestens ansatzweise entnehmen können.

Wobei `i` auch redundant ist, denn das ist immer die Länge von `found` wenn ich das richtig sehe‽

Zeichenketten und Werte mit ``+`` und `str()` zusammenstückeln ist eher BASIC als Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode, und ab Python 3.6 f-Zeichenkettenliterale.

Wenn man zusätzlich zu den Elementen eines iterierbaren Objekts noch eine laufende Zahl braucht, nimmt man die `enumerate()`-Funktion dafür.

Ganz wichtig: Nur Ausnahmen behandeln, die man auch erwartet und für die die Behandlung die man da macht auch wirklich garantiert sinnvoll ist. Nackte ``except``\s ohne konkrete Ausnahmen darf man eigentlich nur benutzen wenn man die Ausnahme protokollieren möchte und sie dann wieder auslöst, oder wenn an der Stelle 100% sicher ist, dass danach kein Code mehr ausgeführt wird.

Ungetesteter Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
from time import sleep

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

#
# FIXME All ``except``\s should only handle explicitly know exception and not
# just *all* possible exceptions!
#


def create_chrome_session():
    options = webdriver.ChromeOptions()
    # options.add_argument("headless")
    # options.add_argument("disable-gpu")
    # options.add_argument("--log-level=3")
    return webdriver.Chrome("chromedriver.exe", options=options)


def login(browser, username, password):
    browser.get("https://www.instagram.com/accounts/login/")
    sleep(1)
    browser.find_element_by_name("username").send_keys(username)
    password_element = browser.find_element_by_name("password")
    password_element.send_keys(password)
    password_element.send_keys(Keys.ENTER)
    sleep(6)
    try:
        browser.find_element_by_xpath("(//div[@class='_47KiJ'])")
        return True
    except:
        return False


def get_posts(browser):
    try:
        article_count_as_text = browser.find_element_by_xpath(
            "(//span[@class='g47SY '])"
        ).get_attribute("innerHTML")
    except:
        return None
    else:
        article_count = int(article_count_as_text.replace(".", ""))

        image_paths = set()
        image_index = 1  # Index within viewport.
        #
        # FIXME Potential endless loop.
        #
        while len(image_paths) < article_count:
            try:
                image_path = browser.find_element_by_xpath(
                    f"(//img[@class='FFVAD'])[{image_index}]"
                ).get_attribute("src")
            except:
                browser.execute_script(
                    "window.scrollTo(0, document.body.scrollHeight);"
                )
                image_index = 1
                sleep(3)
            else:
                image_paths.add(image_path)
                #
                # TODO Doesn't belong here. Replace with logging or callback.
                #
                print(f"[{len(image_paths)}/{article_count}]")
                image_index += 1

        return image_paths


def main():
    crawler = Crawler(
        "Benutezrname", "Passwort", "https://instagram.com/USERNAME"
    )

    print("Chrome Sitzung wird vorbereitet und geöffnet...")
    try:
        browser = create_chrome_session()
    except:
        print(
            "\nDer Google Chrome Treiber wurde nicht gefunden...\n"
            "Downloade den zu passenden Treiber und verschiebe ihn mit dem"
            " Namen 'chromedriver.exe' in das aktuelle Verzeichnis"
        )
    else:
        print("Login wird versucht...")
        if not login(browser, "Benutzername", "Passwort"):
            print("\nBenutzername oder Passwort waren nicht korrekt...")
        else:
            print("Login erfolgreich")
            user_url = "https://instagram.com/USERNAME"
            print(f"{user_url!r} wird gesucht...")
            try:
                browser.get(user_url)
            except:
                print(
                    f"\nEs scheint so, als ob {user_url!r} keine gültige"
                    f" Instagram-Adresse sei\n"
                    f"Bitte verwende folgendes Pattern:"
                    f" https://instagram.com/USERNAME"
                )
            else:
                image_paths = get_posts(browser)
                if image_paths is None:
                    print("\nBenutzer wurde nicht gefunden...")
                else:
                    for number, image_path in enumerate(image_paths, 1):
                        print(f"{number} {image_path}\n")


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten