Daten Crawlen mittels Jupyter + Selenium (hier: class)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
selen
User
Beiträge: 3
Registriert: Sonntag 1. Dezember 2019, 04:32

Sonntag 1. Dezember 2019, 05:40

Hi ihr lieben Nerds,

ich würde gerne Daten von Webseiten crawlen, beschäftige mich aber erst seit nen paar Stunden mit der ganzen Thematik... soll heißen, ich bin nen Noob und hab nie was programmiert, bin völlig ahnungslos! Aber nicht aufn Kopf gefallen.

Mit ein paar Youtube Tutorials habe ich es geschafft, Anaconda (inkl. Python und Jupyter) zu installieren und über selenium den webdriver, pandas, ActionChains, Keys, By und time zu importieren. Ich nutze den Chromedriver und kann schon automatisiert Webseiten laden, Textfelder finden und Text eingeben (driver.find_element_by_name("hierbittetexteingeben").send_keys("123textABC"), Pausen einbauen mit time.sleep(x), Keys benutzen (z.B. Enter), MausKlicks auslösen (act.click(xxx).perform())und ich kann auch Elemente finden mittels "class_name", also z.B. driver.find_elements_by_class_name('xxx')

Nun habe ich es mal in der Praxis probiert und eine Website mit u.A. diesem Quelltext gecrawlt:

<div class="address">Berliner Str 1<span class=" ">, 12345</span>
</div>


Der Quelltext enthält also die Anschrift (Berliner Str 1) sowie die Postleitzahl (12345). Es sind noch weitere Anschriften und PLZ im Quelltext vorhanden, insg. 20 stück. Das will ich alles abgreifen und in eine Excel Liste schreiben.

Nun habe ich mit dem Befehl driver.find_elements_by_class_name('address') volle 20 Ergebnisse bekommen, aber sie sind unlesbar. Zum ersten Element bekommt ich z.B. den Output: [<selenium.webdriver.remote.webelement.WebElement (session="171fd5542c6530189140873ef0a15362", element="f8f01714-90ef-4b12-86b5-86816bb6854e")>,

Es scheint, als wäre der Output, also der Text des Elements verschlüsselt, obwohl im Quelltext alles lesbar ist. Sogar ne Stapelverarbeitung in Excel müsste das mit nem schlichten Macro herausfischen können, wozu also der Aufwand? Oder hab ich nen grundsätzlichen Denkfehler? Ich hab verschiedene Varianten von getText und getAttribute probiert, gab immer nen syntax- oder attribute-error. Ich war guter Hoffnung, als Noob nen bisschen Crawlen zu können, fürchte aber nun, an meine Grenzen gestoßen zu sein. Für Hilfe wäre ich umso dankbarer.

Wichtig ist aber dabei, dass ich viel Background-Info brauche. Mit Englisch komm ich klar, die Inder aus den Youtube-Tutorials sind aber teilweise sehr schlecht zu verstehen. Googlen bringt zu allermeist automatisch übersetzte Threads zum Vorschein, die noch schwerer zu verstehen sind als ordentliches Englisch. Ach ja, ich crawle bloß rein privat, nur für mich ... falls das ne Rolle spielt.

Besten Dank und mfG, Selen
__deets__
User
Beiträge: 6861
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 1. Dezember 2019, 09:08

Da ist nichts verschlüsselt. Das sind nur eher interne Repräsentationen.

Hier sind die Dinge die du mit den Elementen machen kannst: https://selenium.dev/selenium/docs/api/ ... ement.html - getText und getAttribute sind nicht dabei.

Und in Zukunft bitte Code und konkrete Fehlermeldungen posten.
selen
User
Beiträge: 3
Registriert: Sonntag 1. Dezember 2019, 04:32

Sonntag 1. Dezember 2019, 20:38

Danke schonmal,

ich denke ich habe herausgefunden, warum ich die Elemente zwar finden aber nicht deren Text darstellen lassen konnte: Beide Elemente stehen zwar im Quelltext, werden aber auf der Webseite nicht lesbar dargestellt. Die Textausgabe ist einfach leer ... mit sichtbaren Elementen habe ich es hingegen hinbekommen. Hat hier noch jemand eine Idee wie man den selenium webdriver dazu bekommen kann, auch "unsichtbare" Texte ausgeben zu können?

Danke und mfG
xXSkyWalkerXx1
User
Beiträge: 302
Registriert: Mittwoch 27. Juni 2018, 17:39

Sonntag 1. Dezember 2019, 21:29

Also wenn du Daten von Webseiten scrappen willst, dann empfehle ich dir die "requests" und "beautifulsoup" Module. Damit ist es echt leicht.
__deets__
User
Beiträge: 6861
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sonntag 1. Dezember 2019, 21:45

Kannst du die nicht un-hiden mit selenium?
selen
User
Beiträge: 3
Registriert: Sonntag 1. Dezember 2019, 04:32

Dienstag 3. Dezember 2019, 00:57

Hallo und Danke für eure Antworten.

ich habe mal beautifulsoup geladen und versucht, dessen Funktionen zu Ergründen. Leider ist die youtube-Dokumentation schon etwas zu fortgeschritten für mich. So richtig weiß ich nicht, wie ich es dazu bekomme, etwas für mich zu machen.
Die Sache mit dem Un-Hiden scheint knifflig zu sein. Ein Drop-down Menü hat beispielsweise versteckte Elemente ... diese können aber dennoch angezeigt werden. Ich rede von Elementen, deren Anzeige garnicht vorgesehen ist.

Machen wir es mal etwas praktischer: Ich will die Adressen von den Restaurants erfahren, die über Liferando in meinem PLZ Bereich ausliefern. Diese sollen mal in meinem Navi gespeichert werden, um auf dem Heimweg direkt tel. bestellen zu können, das ist oftmals günstiger.

Also, zu meiner Postleitzahl 99999 gibt es 18 Restaurants, eines davon ist McDonalds. McDonalds wird natürlich namentlich genannt, dessen Adresse wird aber nur im Quellcode geführt zur Bestimmung der Distanz zum Besteller. Das sieht dann so aus:

Code: Alles auswählen

<div class="detailswrapper">
                <h2 class="restaurantname">
                    <[b]a class="restaurantname"[/b] href="/mcdonalds-rdz-testshop" itemprop="name">
                        [b]McDonald's®[/b]                    </a>
                </h2>

                                    <div itemprop="review" itemscope="" itemtype="http://schema.org/Review">
                        <meta itemprop="name" content="McDonald's®">
                        <span itemprop="reviewRating" itemscope="" itemtype="http://schema.org/Rating">
                <meta itemprop="worstRating" content="1">
                <meta itemprop="ratingValue" content="0">
                <meta itemprop="bestRating" content="5">
                <meta itemprop="reviewCount" content="0">
            </span>
                    </div>
                
                <div class="kitchens">
                    Amerikanisch, Burger, Eiscreme                </div>
                <div class="bottomwrapper details">
                    <div class="delivery js-delivery-container">

                                                    <div class="avgdeliverytime avgdeliverytimefull open">Ab 11:30</div>
                            <div class="avgdeliverytime avgdeliverytimeabbr openAbbr">Ab 11:30</div>
                                                
                        <div class="delivery-cost js-delivery-cost">GRATIS</div>

                                                    <div class="min-order">Min. 10,00 €</div>
                        
                                                    <div class="label-promoted js-label-promoted">Gesponsert</div>
                        
                                            </div>

                                            <div class="pickup hidden wrapper-open-distance">

                                                            <div class="open openpickup">Ab 11:30</div>
                            
                            <div class="distance">5.00 KM</div>

                                                            <div class="address">Drygalski-Allee 51<span class=" ">, 81477</span>
                                </div>
                            
                                                            <div class="label-promoted js-label-promoted-pickup">Gesponsert</div>
                                                    </div>
                                    </div>
            </div>
Lasse ich Selenium nun so crawlen:

Code: Alles auswählen

name = driver.find_elements_by_xpath('//a[@class="restaurantname"]')
bekommt ich bei

Code: Alles auswählen

print(name[i].text)
den Output: McDonald's® ... ebenso auch für die weiteren 17 Restaurants. Also alles fein.

Soweit so gut, nun zur Anschrift welche auf Liferando generell für jederman einsehbar ist. Mit der Zeile

Code: Alles auswählen

anschrift = driver.find_elements_by_xpath('//div[@class="address"]')
gibt es 18x einen Ergebnis ... aber der Output von anschrift.text ist leer. Bei der Postzeitzahl wäre ich nichtmal sicher, wie Selenium die finden sollte. Ich habe folgendes probiert

Code: Alles auswählen

plz = driver.find_elements_by_xpath('//span[@class=" "]')
... auch hier gibts 18x Output aber keinerlei output per plz.text

Was soll ich in meiner Ahnungslosigkeit noch sagen? Darf man überhaupt Adressen sammeln ... ich denk mal schon, ist ja alles öffentlich, oder ..?

Wer noch Ausdauer mit nem Noob hat und sich mit WebCrawling bzw. WebScraping auskennt, dem wäre ich für Ratschläge dankbar! Ich will diese Adressen ... und noch viele weitere

MfG, Selen
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 07:46

Ich würde dir da auf jeden Fall BeautifulSoup empfehlen.

Hier mal ein bisschen Code zum anschauen und testen für dich:

Code: Alles auswählen

from bs4 import BeautifulSoup

SOURCE = """<div class="detailswrapper">
                <h2 class="restaurantname">
                    <[b]a class="restaurantname"[/b] href="/mcdonalds-rdz-testshop" itemprop="name">
                        [b]McDonald's®[/b]                    </a>
                </h2>

                                    <div itemprop="review" itemscope="" itemtype="http://schema.org/Review">
                        <meta itemprop="name" content="McDonald's®">
                        <span itemprop="reviewRating" itemscope="" itemtype="http://schema.org/Rating">
                <meta itemprop="worstRating" content="1">
                <meta itemprop="ratingValue" content="0">
                <meta itemprop="bestRating" content="5">
                <meta itemprop="reviewCount" content="0">
            </span>
                    </div>
                
                <div class="kitchens">
                    Amerikanisch, Burger, Eiscreme                </div>
                <div class="bottomwrapper details">
                    <div class="delivery js-delivery-container">

                                                    <div class="avgdeliverytime avgdeliverytimefull open">Ab 11:30</div>
                            <div class="avgdeliverytime avgdeliverytimeabbr openAbbr">Ab 11:30</div>
                                                
                        <div class="delivery-cost js-delivery-cost">GRATIS</div>

                                                    <div class="min-order">Min. 10,00 €</div>
                        
                                                    <div class="label-promoted js-label-promoted">Gesponsert</div>
                        
                                            </div>

                                            <div class="pickup hidden wrapper-open-distance">

                                                            <div class="open openpickup">Ab 11:30</div>
                            
                            <div class="distance">5.00 KM</div>

                                                            <div class="address">Drygalski-Allee 51<span class=" ">, 81477</span>
                                </div>
                            
                                                            <div class="label-promoted js-label-promoted-pickup">Gesponsert</div>
                                                    </div>
                                    </div>
            </div>"""


soup = BeautifulSoup(SOURCE, "html.parser")
restaurants = soup.find_all(itemprop="name")
adresses = soup.find_all("div", {"class": "address"})

for restaurant in restaurants:
    restaurant_name = restaurant.get("content")
    print(f"Restaurant: {restaurant_name}")

for adress in adresses:
    restaurant_adress, restaurant_post_code = adress.get_text().split(",")
    print(f"Anschrift: {restaurant_adress}")
    print(f"PLZ: {restaurant_post_code}")

bin zwar selber ein ziemlicher Anfänger aber wenn Fragen diesbezüglich sind kannst du mir gerne schreiben, oder einfach den Thread hier fortsetzen (dann ist die Chance höher dass ein paar kluge Köpchen ab und zu mal drüberschauen).
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 09:00

Hier mal als Beispiel mit request, die Daten habe ich dann in ein Dictionary gespeichert:

Code: Alles auswählen

from bs4 import BeautifulSoup
import requests

URL = "https://www.lieferando.de/lieferservice-trier-54290"
HEADERS = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"} #Hier muss dein UserAgent hin als Value, dafür einfach bei Google "my user agent" eingeben

page = requests.get(URL, headers=HEADERS)
soup = BeautifulSoup(page.content, "html.parser")

restaurants = soup.find_all(itemprop="name")
adresses = soup.find_all("div", {"class": "address"})

restaurants_with_adresses = {}
for restaurant, adress in zip(restaurants, adresses):
    restaurants_with_adresses[restaurant.get_text().strip()] = adress.get_text()[:-1].split(",") #[:-1] um /n abzuschneiden

for key, value in restaurants_with_adresses.items():
    print(f"""Restaurant:{key}
PLZ: {value[1]}
Adresse: {value[0]}\n""")

Die URL müsstest du dann mit Selenium holen, da die nicht immer gleich endet.

Manchmal so:
lieferservice-stadtname-12345

Manchmal so:
lieferservice-12345

Außerdem ist bei dem Code oben noch der Fehler, dass er den Namen irgendwie nicht richtig ausgegeben bekommt für das zweite Element im Dictionary, aber woran das liegt weiß ich leider nicht.


#edit: Gerade gemerkt dass die Zuordnung im Dictionary nicht richtig ist, also der Name stimmt nicht mit der Anschrift überein. Aber das soll ja nur ein Beispiel zur Veranschaulichung sein. (Falls trotzdem jemand eine Idee zu den zwei Problemen hat gerne drunter schreiben)
Benutzeravatar
__blackjack__
User
Beiträge: 4681
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dienstag 3. Dezember 2019, 10:39

@Jankie: Nur nach ``itemprop="name"`` zu suchen ist keine so gute Idee, denn das "name"-Property haben sowohl Restaurants als auch Rewiews.

Das nächste Problem ist nach den beiden Sachen, Namen und Adressen, getrennt zu suchen. Und das im *gesamten* Dokument. Da können dann unterschiedlich viele Ergebnisse heraus kommen, und man weiss dann auch gar nicht was zusammengehört, also welche Adresse zu welchem Restaurant.

Als erstes würde ich die Restaurantliste anhand der eindeutigen ID `irestaurantlist` eingrenzen und nur innerhalb dieses Elements weitersuchen. Und da dann die Elemente mit dem `itemtype` http://schema.org/Restaurant. Da drin kann man dann für jedes Restaurant den Namen und die Adresse heraus suchen.

Das Splitten am "," geht auf diese Weise nur wenn innerhalb des Adressteils vor der Postleitzahl kein Komma vorkommt.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 11:39

@__blackjack__:

Stimmt, ich hoffe der Ansatz ist richtiger:

Code: Alles auswählen

from bs4 import BeautifulSoup
import requests

URL = "https://www.lieferando.de/lieferservice-trier-54290"

HEADERS = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"}
page = requests.get(URL, headers=HEADERS)
soup = BeautifulSoup(page.content, "html.parser")

restaurants_info = soup.find_all("div", id=lambda value: value and value.startswith("irestaurant"))


restaurants_with_adresses = {}
for restaurant_info in restaurants_info:
    for restaurant in restaurant_info.find_all('div',{'itemtype':'http://schema.org/Restaurant'}):
        restaurant_name = restaurant.find("meta")["content"]# evtl.  restaurant.find{"class": "restaurantname"}).get_text().strip()
        restaurant_adress_with_postcode = restaurant.find('div',{'class':"address"}).get_text()
        restaurant_postcode = restaurant_adress_with_postcode[-6:][:-1] #[-6] da die Letzten 6 Zeichen immer die PLZ sind, [:-1] um den Zeilenumbruch zu entfernen
        restaurant_adress = restaurant_adress_with_postcode[:-8] #Alles außer die letzten 8 Zeichen (PLZ+Zeilenumbruch)
        restaurants_with_adresses[restaurant_name] = restaurant_postcode, restaurant_adress

allerdings bringt er mir immer ein Ausnahme: TypeError 'NoneType' object is not subscriptable. In der Zeile wo ich den Namen bestimme (Zeile 16). Wenn ich die auskommentiere kommt sonst noch ein Ausnahme: AttributeError 'NoneType' object has no attribute 'get_text' in Zeile 17 wo ich die Adresse auslese.


Wenn ich nicht mit dem Komma splitten sollte, kann ich dann einfach die Strings abschneiden so wie oben? Die PLZ ist ja in Deutschland immer gleichlang.

Und ich weiß nie wie ich die Variablen eindeutig benennen soll...
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 13:06

Habe den Fehler gelöst, aber ziemlich unsauber wahrscheinlich. Es lag daran dass restaurant_info.find_all('div',{'itemtype':'http://schema.org/Restaurant'}) als letztes Element ein None lieferte, hab das dann einfach abgeschnitten, hat jemand eine andere Idee?

Code: Alles auswählen

from bs4 import BeautifulSoup
import requests

URL = "https://www.lieferando.de/lieferservice-berlin-10243"
HEADERS = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"}


def create_dict(all_restaurant_infos):
    restaurants_with_adresses = {}
    for restaurant_info in all_restaurant_infos:
        for restaurant in restaurant_info.find_all('div',{'itemtype':'http://schema.org/Restaurant'})[:-1]:
            restaurant_name = restaurant.find("meta")["content"]
            restaurant_adress_with_postcode = restaurant.find('div',{'class':"address"}).get_text()
            restaurant_postcode = restaurant_adress_with_postcode[-6:][:-1] #[-6] da die Letzten 6 Zeichen immer die PLZ sind, [:-1] um den Zeilenumbruch zu entfernen
            restaurant_adress = restaurant_adress_with_postcode[:-8] #Alles außer die letzten 8 Zeichen (PLZ+Zeilenumbruch)
            restaurants_with_adresses[restaurant_name] = restaurant_postcode, restaurant_adress
    return restaurants_with_adresses


def show_results(dictionary):
    for key, value in dictionary.items():
        print(f"Restaurant: {key}")
        print(f"PLZ:        {value[1]}")
        print(f"Adresse:    {value[0]}\n")

def main():
    page = requests.get(URL, headers=HEADERS)
    soup = BeautifulSoup(page.content, "html.parser")
    restaurants_info = soup.find_all("div", id=lambda value: value and value.startswith("irestaurant"))
    show_results(create_dict(restaurants_info))


main()
Variablen und Methodennamen sind meiner Meinung nach immer noch nicht optimal aber ich weiß nicht wie ich es sonst nennen sollte.

#edit: Glaube der Fehler liegt doch woanders, da wenn ich das [:-1] entferne was ich hinzugefügt habe kommt eine Ausnahme: TypeError 'NoneType' object is not subscriptable in Zeile 12
Benutzeravatar
__blackjack__
User
Beiträge: 4681
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dienstag 3. Dezember 2019, 14:02

@Jankie: `find_all()` auf eine ID ist nicht sinnvoll und `startswith()` irgendwie auch nicht. Was soll das denn alles finden?
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 14:15

Naja jedes "Element" (Also das Feld wo alle Angaben zum Restaurant stehen) auf der Ergebnisseite fängt halt mit so einer ID an und dann wollte ich quasi jedes Element einzeln abarbeiten. Wüsste nicht wie ich das sonst lösen kann.


Bild
Benutzeravatar
__blackjack__
User
Beiträge: 4681
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Dienstag 3. Dezember 2019, 14:30

@Jankie: Damit findest Du auch die ID `irestaurantlist`, denn auch die fängt mit `irestaurant` an. Und das `div` enthält alle Restaurants. Kann es sein das Du damit alles doppelt bekommst? Denn Du suchst ja alles das eine ID mit `irestaurant` am Anfang hat, und *da drin* dann alle <div> mit dem `itemtype` http://schema.org/Restaurant. Bei `irestaurantlist` macht das Sinn und sollte alle Elemente finden, und dann gehst Du noch mal ”alle” Elemente in jedem Element mit der einer ID mit dem Muster `irestaurantXXXX` durch, was immer genau *ein* Restaurant sein sollte, aber eben eines das schon durch das Element das alle Restaurants enthält erschlagen wurde.
“Give a man a fire and he's warm for a day, but set fire to him and he's warm for the rest of his life.”
— Terry Pratchett, Jingo
Jankie
User
Beiträge: 180
Registriert: Mittwoch 26. September 2018, 14:06

Dienstag 3. Dezember 2019, 15:13

Hab die Antwort leider nur Teilweise verstanden. Ich weiß nicht genau wie ich da am besten vorgehe, ist die Möglichkeit hier wieder ein Stückchen näher?

Code: Alles auswählen

from bs4 import BeautifulSoup
import requests

URL = "https://www.lieferando.de/lieferservice-berlin-10243"
HEADERS = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0"}


def create_dict(restaurants):
    restaurants_with_adresses = {}
    for restaurant in restaurants[:-1]:
        restaurant_name = restaurant.find("a", {'class':"restaurantname"}).get_text().strip()
        restaurant_adress_with_postcode = restaurant.find("div", {'class':"address"}).get_text()
        restaurant_postcode = restaurant_adress_with_postcode[-6:][:-1] #[-6] da die Letzten 6 Zeichen immer die PLZ sind, [:-1] um den Zeilenumbruch zu entfernen
        restaurant_adress = restaurant_adress_with_postcode[:-8]#Alles außer die letzten 8 Zeichen (PLZ+Zeilenumbruch)
        restaurant_food = restaurant.find('div',{'class':"kitchens"}).get_text().strip()
        restaurants_with_adresses[restaurant_name] = restaurant_postcode, restaurant_adress, restaurant_food
    return restaurants_with_adresses
    
def show_results(dictionary):
    for key, value in dictionary.items():
        print(f"Restaurant: {key}")
        print(f"PLZ:        {value[0]}")
        print(f"Adresse:    {value[1]}")
        print(f"Essen:      {value[2]}\n")


def main():
    page = requests.get(URL, headers=HEADERS)
    soup = BeautifulSoup(page.content, "html.parser")
    restaurantlist = soup.find('div',id = 'irestaurantlist')
    show_results(create_dict(restaurantlist.find_all(itemtype="http://schema.org/Restaurant")))

main()
Antworten