Webcrawler: Bessere Lösung?!

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.
Antworten
Benutzeravatar
andie39
User
Beiträge: 152
Registriert: Dienstag 7. Dezember 2021, 16:32

Frohe Weihnachten.

Ich habe eine Artikel heute gelesen, bei dem jemand eine Webseite nach einem bestimmten Menü abfragt und sich selber eine Mail sendet:

https://www.codementor.io/@gergelykovcs ... -fcrhuhn45

Dabei kam mir folgender Gedanke bei diesem Teil des Skripts:

Code: Alles auswählen

the_one = 'borzaska'
flength = len(the_one)
available = False

for food in foods:
    for i in range(len(food.text)):
        chunk = food.text[i:i+flength].lower()
        if chunk == the_one:
            available = True
  
 

Er sucht ja in den Menüs nach dem Gericht: „borzaska“ aber nur aufgrund der Anzahl der Zeichen.
Das würde aber auch andere Gerichte mit der gleichen Anzahl auswerfen.

Dabei habe ich mir gedacht, dass es doch besser geht und wollte eure Meinung dazu:

Mein Gedanke ist das man direkt nach dem Menü sucht:

for borzaska in foods:


Oder sehe ich das falsch?
nezzcarth
User
Beiträge: 1765
Registriert: Samstag 16. April 2011, 12:47

Ich stimme dir zu: Für solche praktischen Zwecke mit einer überschaubaren Textmenge reicht eigentlich etwas der Form 'if "needle" in haystack: …'. Man könnte die innere Schleife komplett ersetzen durch so etwas wie:

Code: Alles auswählen

available = the_one in food.text.lower()
Bzw. beide Schleifen.

Code: Alles auswählen

available = any(the_one in food.text.lower() for food in foods)
(Den Rest des Codes habe ich mir nicht angeschaut)

Die Variante in dem Blogpost ist eine Mischung aus einem Sliding Window (was man eher in händisch implementierten Stringalgorithmen antrifft oder als Optimierung) und den eingebauten Features von Python. Ich weiß nicht, ob das als Optimierung gedacht ist, ob die/der Autor*in eher von Sprachen her kommt, wo man so etwas per Hand implementieren müsste, oder was sonst der Grund sein könnte. Aus meiner Sicht ist das aber vmtl. nicht nötig, wenn ich nichts übersehe.

EDIT: Beispiele ergänzt.
Benutzeravatar
__blackjack__
User
Beiträge: 14078
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@andie39: Das siehst Du falsch. Das findet nur "borzaska" und nicht alles was genau so lang ist. Und ist unnötig kompliziert und ineffizient. Zum einen weil es ``in`` nachprogrammiert, aber in Python und mit vielen Kopien von Teilzeichenketten, und weil es nach einem Treffer immer noch weiter sucht statt die Suche abzubrechen. Denn wenn `available` einmal `True` ist, kann sich da durch weitersuchen nichts mehr dran ändern.

Der Autor vergleicht dann den Wahrheitswert explizit mit `True` — wo ja wieder der Wert heraus kommt, den `available` sowieso schon hatte. Und später dann das ``for i in range(len(sequence)):`` „anti-pattern“. Das ist offensichtlich kein Python-Programmierer.

Hier ist das gesamte Skript an einem Stück:

Code: Alles auswählen

#! python3
import bs4, requests, smtplib

# ------------------- E-mail list ------------------------
toAddress = ['example1@email.com','example2@email.com']
# --------------------------------------------------------

#Download page
getPage = requests.get('http://www.somesite.com/menu')
getPage.raise_for_status() #if error it will stop the program

#Parse text for foods
menu = bs4.BeautifulSoup(getPage.text, 'html.parser')
foods = menu.select('.foodname')

the_one = 'borzaska' # This is the name of the food you are looking for
flength = len(the_one)
available = False

for food in foods:
    for i in range(len(food.text)):
        chunk = food.text[i:i+flength].lower()
        if chunk == the_one:
            available = True

if available == True:
    conn = smtplib.SMTP('smtp.gmail.com', 587) # smtp address and port
    conn.ehlo() # call this to start the connection
    conn.starttls() # starts tls encryption. When we send our password it will be encrypted.
    conn.login('youremail@gmail.com', 'appkey')
    conn.sendmail('youremail@gmail.com', toAddress, 'Subject: Borzaska Alert!\n\nAttention!\n\nYour favourite food is available today!\n\nBon apetite!:\nFood Notifier V1.0')
    conn.quit()
    print('Sent notificaton e-mails for the following recipients:\n')
    for i in range(len(toAddress)):
        print(toAddress[i])
    print('')
else:
    print('Your favourite food is not available today.')
Ich würde da die She-Bang-Zeile ändern, damit das nicht nur unter Windows läuft.

Die Importe sollten laut PEP8 einzelne Anweisungen sein.

Namensschreibweisen halten sich nicht an die Konventionen.

`toAddress` ist Einzahl, steht aber für mehrere Adressen. Und das ist eine Konstante, die man entsprechend KOMPLETT_GROSS schreiben sollte.

`getPage` ist inhaltlich falsch, weil der Name nach einer Funktion/Methode klingt, nicht nach der Antwort eines HTTP-Servers.

`the_one` wäre auch besser eine Konstante am Anfang des Quelltextes, denn das ist ja etwas das der Benutzer an seinen Geschmack (hihi, Wortspiel) anpassen müsste. Und der Name ist zwar ein bisschen witzig, aber nicht sehr aussagekräftig.

Dann sähe die Schleife als Zwischenstand so aus:

Code: Alles auswählen

    favourite_dish_name = FAVOURITE_DISH_NAME.lower()
    foods = bs4.BeautifulSoup(response.text, "html.parser").select(".foodname")
    available = False
    for food in foods:
        if favourite_dish_name in food.text.lower():
            available = True
            break
Das kann man mit `any()` und einem Generatorausdrück deutlich kürzen:

Code: Alles auswählen

    favourite_dish_name = FAVOURITE_DISH_NAME.lower()
    foods = bs4.BeautifulSoup(response.text, "html.parser").select(".foodname")
    available = any(favourite_dish_name in food.text.lower() for food in foods)
Und eigentlich braucht man `available` dann nicht mehr wenn man den Ausdruck direkt in das ``if`` als Bedingung einsetzt.

`smtplib.SMTP`-Objekte sind Kontextmanager, das sollte man also mit ``with`` verwenden.

Die Anmeldedaten wären auch wieder besser am Anfang des Quelltextes als Konstanten aufgehoben.

Ich würde dann ungefähr hier raus kommen:

Code: Alles auswählen

#!/usr/bin/env python3
import smtplib

import bs4
import requests


MENU_URL = "http://www.somesite.com/menu"
FAVOURITE_DISH_NAME = "Borzaska"
SENDER = "youremail@gmail.com"
RECIPIENTS = ["example1@email.com", "example2@email.com"]
SMTP_SERVER = "smtp.gmail.com"
SMTP_PORT = 587
SMTP_USERNAME = SENDER
SMTP_PASSWORD = "********"


def main():
    response = requests.get(MENU_URL)
    response.raise_for_status()

    favourite_dish_name = FAVOURITE_DISH_NAME.lower()
    foods = bs4.BeautifulSoup(response.text, "html.parser").select(".foodname")
    if any(favourite_dish_name in food.text.lower() for food in foods):
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
            smtp.ehlo()
            smtp.starttls()
            smtp.login(SMTP_USERNAME, SMTP_PASSWORD)
            smtp.sendmail(
                SENDER,
                RECIPIENTS,
                (
                    f"Subject: {FAVOURITE_DISH_NAME} Alert!\n"
                    f"\n"
                    f"Attention!\n"
                    f"\n"
                    f"Your favourite food is available today!\n"
                    f"\n"
                    f"Bon apetite!:\n"
                    f"Food Notifier V1.0"
                ),
            )
        print("Sent notificaton email(s) to the following recipients:\n")
        for recipient in RECIPIENTS:
            print(recipient)
        print()
    else:
        print("Your favourite food is not available today.")


if __name__ == "__main__":
    main()
Wobei ich heutzutage nicht mehr davon ausgehen würde, dass das jeder SMTP-Server annimmt, mit nur einem "Subject"-Header. Das mag legal sein, oder zumindest tolerabel aus technischer Sicht, aber es gibt einfach zu viele schlechter Spammer-Skripte und kein ordentlicher E-Mail-Client sendet nur mit dem "Subject"-Header und sonst nix, so dass das für Server ein sinnvolles Filterkriterium sein kann. Und wenn es der SMTP-Server über den die Nachricht versendet wird annimmt, kann das immer noch ein Grund für einen anderen Server auf dem Weg zum Empfänger diese Mail auszusortieren.

Ich würde das übrigens nicht als Crawler bezeichnen. Crawler verarbeiten die Seite und folgen dann Links auf der Seite um weitere Seiten zu verarbeiten. Nur eine Seite nach Informationen absuchen nennt man üblicherweise Scraper.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
andie39
User
Beiträge: 152
Registriert: Dienstag 7. Dezember 2021, 16:32

Hallo,

es geht nicht um das gesamte Programm.
Nur um den Teil dessen wonach gesucht wird.
Aktuell würde er ja immer, wenn ein Gericht die gleiche Länge hat, wie das gesuchte Gericht eine Meldung machen, da er doch nur nach der Länge der Zeichenfolge sucht.

Es geht aber ja um ein spezifisches Gericht, dass benannt ist. Also ist doch die explizite Suche danach besser. Oder?
Benutzeravatar
__blackjack__
User
Beiträge: 14078
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@andie39: Nein, er sucht ja eben nicht nur nach der Länge!

Code: Alles auswählen

    for i in range(len(food.text)):
        chunk = food.text[i:i+flength].lower()
        if chunk == the_one:
            available = True
In der Schleife wird aus `food.text` immer eine Teilzeichenkette mit der Länge ausgeschnitten und dann mit `the_one` verglichen! Hier mal der Fall das `the_one` den Wert "kohl" und `food_text` den Wert "Grünkohlsuppe" hat:

Code: Alles auswählen

#!/usr/bin/env python3
from snoop import snoop


@snoop
def main():
    the_one = "kohl"
    food_text = "Grünkohlsuppe"

    flength = len(the_one)
    available = False
    for i in range(len(food_text)):
        chunk = food_text[i : i + flength].lower()
        if chunk == the_one:
            available = True

    return available


if __name__ == "__main__":
    main()
Ausgabe:

Code: Alles auswählen

16:58:28.44 >>> Call to main in File "forum10.py", line 6
16:58:28.44    6 | def main():
16:58:28.44    7 |     the_one = "kohl"
16:58:28.44 .......... the_one = 'kohl'
16:58:28.44    8 |     food_text = "Grünkohlsuppe"
16:58:28.44 .......... food_text = 'Grünkohlsuppe'
16:58:28.44   10 |     flength = len(the_one)
16:58:28.44 .......... flength = 4
16:58:28.44   11 |     available = False
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 0
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'grün'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 1
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'rünk'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 2
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'ünko'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 3
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'nkoh'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 4
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'kohl'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   15 |             available = True
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 5
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'ohls'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 6
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'hlsu'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 7
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'lsup'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 8
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'supp'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 9
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'uppe'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 10
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'ppe'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 11
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'pe'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44 .......... i = 12
16:58:28.44   13 |         chunk = food_text[i : i + flength].lower()
16:58:28.44 .............. chunk = 'e'
16:58:28.44   14 |         if chunk == the_one:
16:58:28.44   12 |     for i in range(len(food_text)):
16:58:28.44   17 |     return available
16:58:28.44 <<< Return value from main: True
Hier sieht man schön wie `chunk` ein ”Fenster” in "Grühnkohlsuppe" ist, das da quasi ”durchgeschoben” wird, und das bei i = 4 der Vergleich positiv ist. Und danach trotzdem noch weitergetestet wird. Und dann auch noch wenn `chunk` kürzer als das Suchwort ist.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
andie39
User
Beiträge: 152
Registriert: Dienstag 7. Dezember 2021, 16:32

Oh stimmt. Er vergleicht chunk ja mit dem Wert the one und der ist ja der Name des Gerichts.

Aber ist das aufteilen nötig?
Er könnte doch direkt nach dem Namen des Gerichts suchen oder nicht?
Antworten