Panda Dataframe: Mehrere Elemente einer Zelle in einzelne Zeilen aufteilen

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
catalina_2099
User
Beiträge: 3
Registriert: Donnerstag 30. November 2023, 22:15

Hallo zusammen,

ich bin erst vor kurzem in die Programmierung mit Python eingestiegen und arbeite derzeit an meinem ersten Webscrape-Projekt.
Im Rahmen meines Projektes möchte ich Rezepte von der Internetseite Chefkoch.de extrahieren und die relevanten Daten in einem Panda Dataframe abbilden.
Ich extrahiere z.B. den Rezeptnamen, Schwierigkeitsgrad, Nährwerte. Vor allem aber liegt der Fokus auf den Zutaten (Ingredients) und den entsprechenden Mengenangaben (Quantities).
Mit meinem bisherigen Code habe ich es geschafft alle Zutaten und Mengenangaben für die Rezepte zu extrahieren. Allerdings befindet sich die Auflistung der Zutaten und Mengenangaben alle innerhalb einer Zelle.


Ziel ist es, dass die jeweiligen Zutaten und Mengenangaben eines Rezeptes jeweils in einer Zeile stehen und nicht mehr alle gesammelt in einer Zelle. Entsprechend der Zeilenlänge der Zutaten bzw. Mengenangaben würde man dann die Werte der anderen Spalten einfach entsprechend duplizieren.

Unter diesem Link findet ihr sowohl meinen aktuellen Code, CSV als auch ein Beispiel wie meine CSV am Ende aussehen sollte.
https://we.tl/t-419yNKJFYr


Leider komme ich hier nicht weiter..Ich bin für jeden Tipp sehr dankbar!

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

@catalina_2099: Tipps: Programmieren bedeutet das Problem in kleinere Teilprobleme zu zerlegen. Und diese Teilprobleme wieder in kleinere Teilprobleme. Solange bis sich Teilprobleme mit einer Funktion mit wenigen Zeilen Code lösen lassen. So eine Funktion schreibt man dann, und testet sie. Wenn sie funktioniert, geht man zur nächsten Funktion. Und kommt dann zu Teilproblemen die sich mit einer Funktion lösen lassen die Funktionen für kleinere Teilprobleme benutzt. Und so setzt man die getesteten Teillösungen zusammen, bis man das Gesamtproblem gelöst hat.

Da ist es schon mal ein massives Alarmzeichen wenn der Code keine einzige Funktion enthält und einfach nur aus ca. 270 Zeilen Code auf Modulebene besteht. Das ist nicht strukturiert, es gibt viel zu viele Variablen und man kann nichts separat bearbeiten und testen ohne immer den *kompletten* Vorgang durchgehen zu müssen, statt sich auf eine Teilaufgabe fokussieren zu können.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen als Argument(e) übergeben, und liefern Ergebnisse über den Rückgabewert an den Aufrufer. So kann man den Code in kleine, unabhängig testbare Einheiten strukturieren, statt einem grossen unübersichtlichen Code-Block.

`webdriver` wird importiert aber nirgends verwendet.

Namen sollten keine kryptischen Abkürzungen enthalten. Da sind viel zu viele Katzen in dem Quelltext.

Namen sollten im gleichen Namensraum nur für den gleichen ”Duck-Typ” verwendet werden. `link_cat` fängt als Zeichenkette mit einer URL an, und wird dann wenig später für HTML-Element-Objekte verwendet. Allerdings in einer Schleife die gar keinen Sinn macht, weil die letztlich überhaupt keine Auswirkung auf das Programm hat, ausser ein bisschen Rechenzeit zu verbrauchen.

Programmierer vermeiden Redundanz, sowohl bei Code als auch bei Daten. Wenn man den gleichen Dateinamen und/oder Dateipfad oder Pfadteil mehrfach im Quelltext stehen hat, ist das eine Fehlerquelle und unnötiger Mehraufwand wenn man daran Änderungen vornehmen will oder muss. Da definiert man sich einmal eine Konstante für. Und Codewiederholungen vermeidet man durch Funktionen und/oder Schleifen.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Warum wird die Datei die gerade geschrieben wurde, sofort wieder geladen? Selbst wenn man die Daten gerne auf der Platte haben möchte, macht es doch keinen Sinn die wieder zu laden, weil man die ja bereits im Speicher hat.

HTML-Daten aus dem Netz als Textdatei mit UTF-8 als Kodierung zu speichern ist keine gute Idee. HTML kann eine Kodierungsangabe enthalten, und wenn die nicht auch UTF-8 ist, dann hat man eine kaputte Datei gespeichert. Die Daten stehen als `content`-Attribut als Bytes auf `response` zur Verfügung.

An der Stelle ist das übrigens auch etwas verwirrend den `response.text` an den Namen `html_content` zu binden, wenn es auch ein `response.content` gibt. Das weckt beim Leser falsche Erwartungen.

Statt selbst auf konkrete `status_code` zu testen gibt es ein allgemeines `ok`-Attribut. Es wird auch gar nicht überall getestet. Bei der ersten Seite die abgerufen wird beispielsweise nicht. Wenn das eine Ausnahmesituation darstellen sollte, also wenn man eigentlich gar nicht sinnvoll weitermachen kann wenn die Seite nicht geladen werden konnte, bietet es sich an `raise_for_status()` auf dem `response`-Objekt aufzurufen.

URLs am ersten "t" zu aufzuteilen und dann ein "t" mit dem Teil hinter dem "t" zu verbinden ist reichlich verwirrend ausgedrückt. Da würde man eher den Index von dem "t" suchen und den Teil ab dort ”slicen”. Bei so etwas wäre ein Kommentar gut, der zeigt wie die Daten aussehen die dort verarbeitet werden. Oder ein oder mehrere Testfälle wo man das in Aktion sehen kann ohne das Programm ablaufen lassen zu müssen.

`os` und `os.path` ist mittlerweile von `pathlib` abelöst. `Path`-Objekte haben auch eine praktische `write_bytes()`-Methode.

Erst die Seiten mit `head()` zu testen um dann am Ende doch *alle* mit `get()` zu laden um da dann noch mal zu testen ob die Seite überhaupt existiert ist ein bisschen sinnlos. Man würde da einfach gleich `get()` benutzen. Und falls es keinen Grund gibt mit ”Löchern” in der Nummerierung zu rechnen, würde man auch bei der ersten nicht existenten Seite abbrechen, statt dann noch weiter Anfragen zu stellen.

Ich würde die nummerierten URLs auch nicht erst in einer Liste erstellen, sondern erst dann wenn die einzelne URL tatsächlich gebraucht wird.

`replace()` ist nicht die richtige Methode um etwas *am Anfang* einer Zeichenkette zu entfernen. Entweder `removeprefix()`, oder falls man noch eine Python-Version hat, die diese `str`-Methode nicht hat, eine entsprechende kleine Funktion selbst schreiben.

`output_folder` wird zweimal (identisch) definiert‽

(Man muss nicht jedes kleine Zwischenergebnis an einen Namen binden.)

Listen haben eine `extend()`-Methode. Das spart eine Schleife mit einem `append()` pro Element aus einem anderen iterierbaren Objekt.

Parallele Listen macht man nicht. Das ist unübersichtlich und fehleranfällig wenn man mehrere Listen (hier 14(!) Stück) hat wo Elemente am gleichen Index eigentlich zusammengehören. Die speichert man zusammen in *einer* Liste. Beispielsweise als Tupel, allerdings bei 14 Elementen eher als Objekt, beispielsweise ein `collections.namedtuple` oder zumindest als Wörterbuch.

Grunddatentypen wie `list` haben nichts in Namen verloren. Den Typ ändert man gar nicht so selten mal im Laufe der Entwicklung und dann hat man entweder falsche, irreführende Namen im Quelltext, oder muss alle betroffenen Namen suchen und ändern.

Worte in Namen sollten in der richtigen Reihenfolge stehen (nur Yoda darf das anders machen 😉). Ein `response_recipe` ist was anderes als eine `recipe_response`, oder `soup_recipe` ist etwas anderes als eine `recipe_soup`.

In einer Schleife immer alle Namen neu binden zu müssen, damit keine Werte aus vorherigen Schleifendurchläufen ”durchrutschen” können ist fehleranfällig, und etwas das man oft nicht sofort merkt wenn es mal passiert. Es ist sicherer den Schleifenkörper in eine Funktion heraus zu ziehen, wo man dann *sicher* sein kann, das jeder Aufruf mit neuen Namen durchgeführt wird, und ein Fehler als Ausnahme deutlich auffällt.

Mir ist der Sinn solch eines DataFrames nicht so ganz klar mit so vielen redundanten Zellwerten. Das sieht eher nach einem Anwendungsfall für eine relationale Datenbank aus.

Die Programmaufteilung mit dem abfragen, speichern, und wieder laden und verarbeiten macht nicht wirklich Sinn. Das ist ja für die einzelnen Schritte ”verschränkt”, das heisst man muss immer alle Abfragen an die Webseite machen, auch wenn man nur die Verarbeitung testen will. Entweder speichert man nur und verarbeitet gleich, wenn man zusätzlich zur Verarbeitung eine lokale Kopie haben möchte. Oder man lädt erst alles herunter und speichert, und verarbeitet danach die gespeicherten Dateien ohne noch Webabfragen zu machen, damit man diese Teile getrennt hat, und für die Verarbeitung nicht immer wieder alles herunterladen muss.

Ansonsten kann für die Entwicklung bei so etwas auch das Package `requests-cache` nützlich sein, wenn man sich unnötiges herunterladen der immer gleichen Daten ersparen möchte ohne viel am Programm ändern zu müssen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dein Fehler ist hier:

Code: Alles auswählen

 quantities_list.append('\n'.join(quantities_values))
Da klatscht du einfach alle Mengen in eine Zelle. Gleiches gilt für die Zutaten,

Code: Alles auswählen

ingredients_value = '\n'.join(ingredients_values) if ingredients_values else 'N/A'
Diese Listen musst du anders verarbeiten. Dein Beispiel erzeugt einfach eine Zeile pro Zutat, und hat alle anderen Daten gleich. Das ging zb mit

Code: Alles auswählen

for ingredient, quantity in zip(ingredients_values, quantities_values):
    # jetzt immer eine Zeile mit allen Werten anlegen, wie gehabt
Sinnvoll ist das IMHO nicht, weil CSV Daten und eine solche 1:n-Beziehung sich nicht gut vertragen.
catalina_2099
User
Beiträge: 3
Registriert: Donnerstag 30. November 2023, 22:15

Hallo zusammen

Vielen lieben Dank @__blackjack__ für die wertvollen Anmerkungen :)

Danke @__deets__ für deinen Lösungsansatz. Mir ist nicht ganz klar wie ich jetzt dannn im Loop die Zeile mit den Werten anlegen soll. Diese Code Abschnitt meinst du ja nicht oder :
quantities_list.append('\n'.join(quantities_values))
ingredients_list.append(ingredients_value)

Tut mir Leid, falls die Frage trivial ist aber ich bin wie gesagt noch ganz neu in dem ganzen...
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Na du hast doch eine große Liste von append Instruktionen, um eine Zeile zu erzeugen. Laut deiner Vorgabe muss die eben pro Zutat angelegt werden, und damit muss eine for-Schleife wie von mir gezeigt genutzt werden, um eben die n Zeilen zu erzeugen.
catalina_2099
User
Beiträge: 3
Registriert: Donnerstag 30. November 2023, 22:15

@__deets__

Also ich würde jetzt so vorgehen. Eine neue Liste erstellen
list_new = []

for ingredient, quantity in zip(ingredients_values, quantities_values):
line = f"{ingredient}: {quantity}" #Zeile erzeugen für jedes Zutaten und Mengenelement

recipe_lines.append(line) # und die Zeilen dann der Liste zuordnen.
Damit alle meine Listen aber die selbe n Anzahl der Zeilen hat wie Ingredients und Quantities werde ich ja die Werte in den anderen Listen entsprechend duplizieren müssen oder wie man es in Excel macht "runterziehen" bis zur letzten Ingredients/Quantites Zeile....

Steh leider auf dem Schlauch..
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ja, dann musst du eben alle anderen appends auch IN der Schleife machen. Muss ja für jeden Wert so oft wie die Zutaten passieren.
Antworten