Python-Skript von GUI aus beenden/starten können sowie Ausgabe anzeigen und zwei Werte aus der GUI übergeben

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
Chris87
User
Beiträge: 13
Registriert: Freitag 21. Februar 2020, 23:33

Hallo,

ich habe erst vor ein paar Tagen angefangen ein Python-Skript (Webscraper mit Beautifulsoup) zu schreiben und würde gerne eine Benutzeroberfläche hinzufügen.

Was die Benutzeroberfläche können soll:
- Ich möchte zwei Werte angeben: Checkbox Ja/Nein oder 1/0 und ein Feld in dem man eine Zahl eingeben kann. Diese beiden Werte sollen im Skript berücksichtigt werden.
- Ich möchte das Skript mit einem Button "Start" starten können und mit einem Button "Stop" stoppen können.
- Die Ausgabe (Prints) soll in der GUI in Echtzeit angezeigt werden.

Ich habe dazu ein ähnliches Beispiel mit PySimpleGUI gefunden:
https://pysimplegui.readthedocs.io/en/l ... ent-window

Bild

Im Endeffekt möchte ich etwas ähnliches erreichen. Jedoch mit einer Checkbox, einem Inputfeld und zwei Buttons (Start/Stop) und die Ausgabe des Skripts soll im "Script output..." passieren.

Ich verstehe jedoch nicht, wozu das Modul "subprocess" benötigt wird und wie ich mein Skript mit einem Button starten und wieder stoppen kann und die beiden Werte in Variablen speichern kann.

Mein Skript sieht momentan folgendermaßen aus:

Code: Alles auswählen

from bs4 import BeautifulSoup
from multiprocessing import Pool
from time import perf_counter, sleep
import requests
import json
import re
import webbrowser
import lxml

open_webbrowser = 0 #Legt fest, ob der Webbrowser automatisch bei einem Treffer geöffnet werden soll (1=Ja, 0=Nein)
process_limit = 8 #Legt die Anzahl gleichzeitiger Prozesse fest

headers = {
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
}

proxies = {
    'http': 'socks5://127.0.0.1:9150',
    'https': 'socks5://127.0.0.1:9150'
}

def scrape(list):
	*Hier werden die Links aus der Liste gescraped*
	*Wenn eine Bedingung erreicht wird, soll ein Webbrowser mit dem Link geöffnet werden. Dazu die Einstellung open_webbrowser*

if __name__ == "__main__":
    while True:
        
        with open('list.json', 'r') as f:
            list = json.load(f)
            print('* Liste geladen *')
            
        start_time = perf_counter()
        
        p = Pool(process_limit)
        p.map(scrape, list)
        p.terminate()
        p.join()

        end_time = perf_counter()
        execution_time = "{:.2f}".format(end_time - start_time)
        print('- Dauer des Durchlaufs: ' + execution_time + ' Sekunden')
Vielleicht kann mir jemand auf die Sprünge helfen, wie ich das am besten erreichen kann.

Vielen Dank.

Chris87
Benutzeravatar
sparrow
User
Beiträge: 4538
Registriert: Freitag 17. April 2009, 10:28

Mit subprocess kann man externe Programme ausführen. Das ist in deinem Fall aber unnötigt, denn du hast ja ein Python-Script, das kannst du direkt einbinden.
Was du vor hast, ist nicht trivial. Das ist GUI-Programmierung sowieso selten.

Das Stichwort ist Nebenläufigkeit.
Wenn du den Crawler in einen eigenen Thread auslagerst, musst du darauf achten, dass er selbst nie die GUI verändern darf. Du musst also einen Kommunikationsweg benutzen, damit nur der Hauptthread der GUI sich selbst aktualisiert und auf Signale aus dem Thread reagiert. Stichwort queue. In umgekehrter Richtung könntest du den Crawler so sogar pausieren lassen.

Wie gesagt. Trivial ist das nicht und erfordert Einarbeitung.
Chris87
User
Beiträge: 13
Registriert: Freitag 21. Februar 2020, 23:33

Ok, vielen Dank für deinen Beitrag. Ich denke das ist dann für den Anfang doch etwas zu schwer für mich und eigentlich auch nicht zwingend notwendig.

Ich werde für für beiden Werte einfach eine Konfigurationsdatei anlegen und diese Datei dann auslesen.
Benutzeravatar
__blackjack__
User
Beiträge: 14051
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Chris87: Anmerkungen zu dem gezeigten Code:

Da sind einige Importe die nicht benutzt werden wo ich jetzt einfach mal davon ausgehe, das die im nicht geigten Code von der `scrape()`-Funktion eine Rolle spielen, aber `lxml` *und* `bs4` zu importieren sieht verdächtig nach einem nicht genutzen Import von `lxml` aus.

Konstanten werden per Konvention KOMPLETT_GROSS geschrieben. Siehe Style Guide for Python Code.

Für Wahrheitswerte wie bei `OPEN_WEBBROWSER` sollte man keine Zahlen missbrauchen, dafür gibt es den Typ `bool` mit den literalen Werten `True` und `False`.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Der Code im ``if __name__ ...``-Zweig müsste also in einer Funktion verschwinden. Üblicherweise wird die `main()` genannt.

Namen sollten aussagekräftig sein, das sind einbuchstabige Namen recht selten. Wenn man `file` oder `pool` meint, sollte man nicht `f` oder `p` schreiben.

Textdateien sollte man immer mit einer expliziten Kodierungsangabe öffnen. Gerade bei JSON-Dateien kann das zu einem Problem werden, denn die sind UTF-8 kodiert, da sollte man sich also erst recht nicht darauf verlassen was Python da für das System ermittelt.

`list` ist der Name des eingebauten Datentyps für Listen, den sollte man nicht an etwas anderes binden. Der Name ist auch viel zu generisch, denn der Leser will ja nicht wissen ob das eine Liste ist, sondern was diese Liste enthält.

`Pool.map()` ist die falsche Funktion wenn man überhaupt gar nicht an der Liste interessiert ist die da als Ergebnis geliefert wird. Wenn es nur darum geht Aufgaben asynchron zu starten macht man das mit `apply_async()`.

Als nächstes ist dann `terminate()` falsch, denn das sorgt dafür das alle Prozesse beendet werden, das darf man nicht machen wenn es noch Aufgaben gibt die gerade laufen oder noch gar nicht gestartet wurden, die man aber gerne abgearbeitet haben möchte. In dem Fall ruft man `close()` auf dem `Pool` auf, damit man dann mit `join()` auf das Ende dieser laufenden und geplanten Aufgaben warten kann.

`start_time` und `end_time` sind Sekundenzahlen und `execution_time` ist dann plötzlich eine Zeichenkette. Das kommt etwas überraschend. Komisch ist auch die Aufteilung das `execution_time` eine Zeichenkette mit `format()` erzeugt und das dann mit ``+`` zu einer weiteren Zeichenkette zusammengestückelt wird. Warum wird *da* nicht `format()` verwendet?

Code: Alles auswählen

#!/usr/bin/env python3
import json
import re
import webbrowser
from multiprocessing import Pool
from time import perf_counter, sleep

import lxml  # TODO Wirklich notwendig?
import requests
from bs4 import BeautifulSoup

OPEN_WEBBROWSER = False
"""
Legt fest, ob der Webbrowser automatisch bei einem Treffer geöffnet werden soll.
"""

PROCESS_LIMIT = 8
"""Legt die Anzahl gleichzeitiger Prozesse fest."""

HEADERS = {
    "user-agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
        " AppleWebKit/537.36 (KHTML, like Gecko)"
        " Chrome/77.0.3865.120 Safari/537.36"
    ),
    "Accept": (
        "text/html,application/xhtml+xml,application/xml;"
        "q=0.9,image/webp,image/apng,*/*;"
        "q=0.8,application/signed-exchange;"
        "v=b3"
    ),
    "Accept-Encoding": "gzip, deflate, br",
    "Accept-Language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}

PROXIES = {
    "http": "socks5://127.0.0.1:9150",
    "https": "socks5://127.0.0.1:9150",
}


def scrape(urls):
    """
    Hier werden die Links aus `urls` gescraped.
    
    Wenn eine Bedingung erreicht wird, soll ein Webbrowser mit dem Link geöffnet
    werden. Dazu die Einstellung `OPEN_WEBBROWSER`.
    """


def main():
    while True:
        with open("urls.json", "r", encoding="utf-8") as file:
            urls = json.load(file)
        print("* URLs geladen *")

        start_time = perf_counter()
        with Pool(PROCESS_LIMIT) as pool:
            for url in urls:
                pool.apply_async(scrape, [url])
            pool.close()
            pool.join()
        end_time = perf_counter()

        print(f"- Dauer des Durchlaufs: {end_time - start_time:.2f} Sekunden")


if __name__ == "__main__":
    main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Chris87
User
Beiträge: 13
Registriert: Freitag 21. Februar 2020, 23:33

@__blackjack__:

Vielen Dank für deinen Anmerkungen. Wie gesagt, ich bin noch ein Anfänger, von daher helfen mir deine Hinweise schon sehr viel weiter. Bis auf die Sache mit dem Multiprocessing leuchtet mir auch alles ein.
Ich habe deine Änderungen nun so übernommen.

Die Module "bs4" und "lxml" verwende ich in der Tat in der scrape()-Funktion.

Ich habe aber dann noch zwei Fragen:
Du benutzt " statt ', hat das auch etwas mit dem Style Guide zu tun?

Ich lade nun die beiden Werte für OPEN_WEBBROWSER und PROCESS_LIMIT aus einer config.json.

Und mir ist aufgefallen, dass alles was ich außerhalb der main()-Funktion mache, bei jedem Prozess ausgeführt wird. Das liegt wohl am Multiprocessing unter Windows.
Daher habe das laden der config.json auch mit in die main()-Funktion verschoben und definiere dort die beiden Werte OPEN_WEBBROWSER und PROCESS_LIMIT. Da OPEN_WEBBROWSER aber erst in der scrape()-Funktion verwendet wird, bekomme ich nun diese Fehler angezeigt.
Undefined variable 'OPEN_WEBBROWSER'
Unused variable 'OPEN_WEBBROWSER'
Hast du da eventuell auch einen Tipp für mich?

Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 14051
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Chris87: Wofür benutzt Du denn `lxml`? Da ist doch nur eine Funktion definiert (`get_include()`), die auch nicht nach öffentlicher API aussieht. Ich vermute sehr stark das Du das Modul `lxml` nicht verwendest, also das der Import davon keinen Sinn macht. Die wirklich sinnvoll verwendbaren Sachen stecken ja in Untermodulen.

Der Style-Guide macht keine Empfehlung ob man " oder ' verwenden sollte. Früher habe ich immer ' genommen weil Python selbst das bei der `repr()`-Darstellung von Zeichenketten bevorzugt, aber ich fand dann das Argument einleuchtend das '', ", und ''' sehr leicht verwechselt werden können, also zwei einzelne ', ein ", und drei '. Wenn man (fast) immer " verwendet ist die Gefahr für so etwas kleiner. Einzelne ' verwende ich noch wenn in der Zeichenkette " vorkommen und man sich so das escapen sparen kann.

Was man in der `main()`-Funktion an lokale Namen bindet, sind dann ja keine Konstanten mehr, also auch nicht KOMPLETT_GROSS.

Alles was eine Funktion (oder Methode) ausser Konstanten benötigt wird als Argument übergeben. Du müsstest die beiden Werte als mit beim Aufruf bzw. für den Aufruf als Argumente neben der URL mitgeben.

Was ist denn bei Sache mit dem Multiprocessing unklar?
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Chris87
User
Beiträge: 13
Registriert: Freitag 21. Februar 2020, 23:33

@__blackjack__:

Vielen Dank für deine Antwort.

Ich verwende "lxml" anstelle von "html.parser" für Beautifulsoup. "lxml" soll beim Parsen von HTML am schnellsten sein.

Code: Alles auswählen

soup = BeautifulSoup(html, 'lxml')
Den Wert "open_webbrowser" übergebe ich dann von main()-Funktion als Argument an die scrape()-Funktion? Ist das nicht irgendwie übertrieben nur für den einen fixen Wert? An sich fand ich die Lösung als Konstante ja schöner, aber wenn ich die Werte von außerhalb lade, dann sollte das ja in einer Funktion passieren, oder?

Naja, ich kenne mich zu wenig aus, um beurteilen zu können, welche Lösung beim Multiprocessing richtig oder besser ist. Ich habe deine Änderung so übernommen, da du schon Recht damit haben wirst. Von der Performance scheint es jedoch keinen Unterschied zu machen.
Ich hatte meine Lösung aus diesem Artikel: https://medium.com/datadriveninvestor/s ... 434ff310c5

Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 14051
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Chris87: Du verwendest also das Modul `lxml` *nicht* in Deinem Modul, brauchst das also auch nicht zu importieren. Du sagst `BeautifulSoup` mit der Zeichenkette "lxml", dass `BeautifulSoup` das `lxml`-Package verwenden soll und irgendwo in einem der Module aus denen `bs4` wird dann auch ein Import von `lxml.html` gemacht, aber das passiert eben dort, egal ob Du da ein ``import lxml`` stehen hast und das `lxml` dann für nichts verwendest oder nicht. In dem Modul `lxml` ist wie schon gesagt sowieso nichts definiert was man normalerweise braucht/verwendet. Dazu müsste man schon eines der Untermodule aus dem Package importieren die tatsächlich Funktionen und Klassen enthalten mit denen man dann diverse Dinge mit XML und HTML anstellen kann.

Werte von ausserhalb laden sollte in einer Funktion passieren, ja. Aber was heisst ”übertrieben”? Es ist ja nicht so, dass das übergeben eines zusätzlichen Wahrheitswertes beim Funktionsaufruf viel Speicherplatz oder Rechenaufwand bedeuten würde. Gemessen am Gesamtaufwand für das abarbeiten eines Aufrufs mit Netzkommunikation, parsen von HTML, und scrapen der Informationen ist das verschwindend gering. Wahrscheinlich nicht mal wirklich messbar, weil das alleine bei den Zeitschwankungen der Netzkommunikation im Rauschen untergehen dürfte.

`Pool.map()` erzeugt halt eine Liste. Die in Deinem Code aber gar nicht weiter verwendet wird, sondern nach dem sie erstellt wurde mit einem Eintrag pro URL, einfach verworfen wird. Das würde ich als ”Missbrauch” dieser Funktion sehen.

Das Laufzeitverhalten kann auch anders sein als das einzelne anwenden der Funktion für jede URL. `Pool.map()` teilt die URLs in Batches ein und gibt die dann an die Prozesse. Das spart zwar Kommunikation zwischen den Prozessen, andererseits werden die Prozesse dann auch nur voll ausgenutzt wenn die Verarbeitungszeit für jeden Batch ungefähr gleich ist, weil zum Beispiel die Verarbeitungszeit für eine URL immer ungefähr gleich ist. Eine URL die ”hängt” hält beispielsweise alle URLs auf, die im Batch die danach kommen, auch wenn Prozesse im Pool frei währen die nichts mehr zu tun haben. Die konkrete Berechnung der Batch-Grösse ist ein Implementierungsdetail, aber CPython würde in den beiden Rechenbeispielen aus dem Medium-Artikel (100 URLs und 10 oder 20 Worker) immer 2 URLs zu einem Batch zusammenfassen und immer wenn ein Workerprozess frei ist dem zwei URLs zuteilen.

Unterschiedlich ist auch das Verhalten bei Ausnahmen. `map()` bricht ab sobald *ein* `scrape()`-Aufruf eine Ausnahme ausgelöst und an den Prozess mit dem `Pool` zurückgemeldet hat. Wenn man die Funktionen mit `apply_async()` unabhängig voneinander aufruft, dann hat eine Ausnahme in einem `scrape()`-Aufruf keine Auswirkungen auf alle anderen Aufrufe.

`terminate()` ist dazu da wenn die Worker-Prozesse noch *nicht* alle fertig sind, man aber trotzdem beenden will. Und da gelten dann auch all die Warnungen die bei `Process.terminate()` stehen, nämlich das die Prozesse die noch laufen hart beendet werden und man beispielsweise nicht davon ausgehen kann das ``finally:``-Zweige, Kontextmanager, oder `atexit()`-Handler abgearbeitet werden. In den Beispielen aus dem Artikel macht das auch gar keinen Sinn, denn `map()` blockiert, womit es danach gar nichts zu terminieren gibt, weil zu dem Zeitpunkt in dem Code garantiert alle Worker in dem Pool fertig sind.

Wenn man nicht `map()` sondern `apply_async()` verwendet ist es genau umgekehrt — es ist nach der Schleife Wahrscheinlich gar kein Worker mit der ersten Aufgabe durch, so dass ein `terminate()` alles killt bevor es überhaupt zum Zug kam.

Was weder beim Code aus dem Artikel noch bei mir behandelt wird sind Netzverbindungen die ”ewig” hängen. Das müsste man in der `scrape()`-Funktion handhaben.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten