Seite 1 von 1

CSV writer Umlaute richtig darstellen

Verfasst: Freitag 12. Februar 2021, 20:58
von laraxxmara
Hallo zusammen, ich bin eine totale Python Anfängerin.
Für die Uni muss ich ein Webscraping durchführen. Der Code funktioniert und ich bekomme die Anforderungen als Bewerber für einen neuen Job in eine CSV geschrieben.
Allerdings werden die Umlaute hier nicht richtig dargestellt. Ich habe mich zwar schon ein wenig eingelesen, bekomme es aber nicht hin.
Ich hoffe mir kann hier jemand helfen :)

Danke schon vorab!!

import requests
from bs4 import BeautifulSoup
import csv

url = 'https://www.stepstone.de/jobs/Junior-Consultant.html'

headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0)'}
res = requests.get(url, headers=headers)
soup = BeautifulSoup(res.text, 'html.parser')

angebote = soup.select('.job-element__url')
firma = soup.select('.job-element__body__company')

filename = 'Stepstone_Kompetenzen_Juiorberater.csv'


# Liste mit Links definieren

links = []
kompetenz_liste =[]

# Liste mit Links füllen

for angebot in angebote:
link = (angebot.get('href'))
links.append(link)

sep = ';'

with open(filename, 'w') as f:
w=csv.writer(f, delimiter = sep)
w.writerow(('Jobangebot', 'Firma', 'Kompetenzen'))

for i in range(len(links)):
url_1 = links
res_1 = requests.get(url_1, headers=headers)
soup = BeautifulSoup(res_1.text, "html.parser")
profil = soup.select('.at-section-text-profile-content')
angebotsbezeichnung = angebote.text.strip('\n').strip(' \n\n')
firmenbezeichnung = firma.text.strip('\n').strip(' \n\n').strip(',')

for p in profil:
kompetenz_1 = p.get_text(strip=True, separator=sep).strip('"')
w.writerow((angebotsbezeichnung, firmenbezeichnung, kompetenz_1))


print('Datei siehe Download Menuleiste als: ' + filename)

Re: CSV writer Umlaute richtig darstellen

Verfasst: Freitag 12. Februar 2021, 22:15
von __blackjack__
@laraxxmara: Beim `open()` für die CSV-Datei fehlen zwei Argumente. Einmal das `newline`-Argument. Siehe dazu die Dokumentation vom `csv`-Modul. Und dann die Kodierung der Textdaten. Ich würde da UTF-8 verwenden, weil das a) alle Unicode-Zeichen kodieren kann und b) das eine Kodierung ist, die zumindest für ”westliche” Sprachen platzeffizient ist, weil ASCII eine Untermenge davon ist, dass heisst sehr viele Zeichen mit nur einem Byte kodiert werden.

Alternativ müsstest Du da die Kodierung verwenden die vom Auftraggeber erwartet wird. Und falls die nicht den gesamten Unicode-Bereich abdeckt, eventuell auch Gedanken darüber machen wie das Programm damit umgehen soll wenn in den Daten Zeichen vorkommen die mit der gewählten Kodierung nicht kodiert werden können.

Re: CSV writer Umlaute richtig darstellen

Verfasst: Freitag 12. Februar 2021, 22:26
von laraxxmara
Hey, Danke für deinen Tipp..
Leider klappt es so immer noch nicht, vllt liegt es aber auch daran dass ich einen fehler gemacht habe..

with open(filename, 'w', newline='', encoding='utf-8') as f:

Re: CSV writer Umlaute richtig darstellen

Verfasst: Freitag 12. Februar 2021, 23:03
von __blackjack__
@laraxxmara: Also bei mir klappt das. Was heisst denn „klappt nicht“ genau? Wie überprüfst Du ob es geklappt hat? Ist die Software mit der Du das machst auch darauf eingestellt, dass die Daten als UTF-8 kodiert sind?

Re: CSV writer Umlaute richtig darstellen

Verfasst: Samstag 13. Februar 2021, 12:56
von __blackjack__
@laraxxmara: Noch ein paar Anmerkungen zum Quelltext: Die Einrückung ist nicht konsequent. Üblich sind vier Leerzeichen pro Ebene.

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

Namen sollten nicht kryptisch abgekürzt und/oder nummeriert werden. `p`, `f`, und `w` sind keine guten Namen, weil sie dem Leser nichts darüber verraten was der Wert dahinter im Kontext des Programms bedeutet. Wenn man `response` meint, sollte man nicht nur `res` schreiben.

`kompetenz_liste` wird definiert, aber nirgends verwendet. Grunddatentypen haben auch nichts in Namen verloren, weil man den Typ während der Entwicklung nicht selten mal ändert, und dann hat man entweder falsche, irreführende Namen im Quelltext oder muss alle betroffenen Namen anpassen.

Die Antwort vom Webserver sollte man vor dem Verarbeiten auf den Status prüfen, also ob das eine erfolgreiche Antwort mit den angeforderten Daten war oder ob der Körper der Antwort eine Fehlerseite enthält.

Beim abfragen und parsen der Webseiten wiederholt sich ein bisschen Code den man IMHO schon sinnvoll in eine eigene Funktion auslagern kann.

”Parallele” Datenstrukturen, also beispielsweise Listen bei denen Elemente mit dem gleichen Index zusammengehören sind keine gute Idee, weil das in der Regel zu mehr Code bei der Verarbeitung führt und fehleranfällig ist. Das fängt beim Scrapen damit an, dass man hofft wenn man die gesamte Seite separat nach zwei zusammengehörenden Kriterien durchsucht, die Listen am Ende a) gleich viele Elemente enthalten, und b) dass die dann auch tatsächlich paarweise zusammengehören. So etwas kann leicht ins Auge gehen. Statt auf der Einstiegsseite Links und Firmennamen getrennt zu suchen, würde man besser die Elemente suchen in denen diese beiden Informationen zusammengefasst sind, und im weiteren dann in jedem dieser Elemente die beiden zusammengehörenden Daten raussuchen.

Mit `links` wird dann ganz ohne Not noch eine dritte Liste parallel zu `angebote` und `firma` erstellt, die man so nicht erstellen würde. `firma` ist als Namen falsch, weil irreführend, weil der Wert gar nicht für *eine* Firma steht, sondern für mehrere Firmen.

Falls man `links` denn erstellen *würde*, könnte man das deutlich kompakter mit einer „list comprehension“ machen:

Code: Alles auswählen

    links = []
    for angebot in angebote:
        link = (angebot.get("href"))
        links.append(link)

    # =>

    links = [angebot.get("href") for angebot in angebote]
``for i in range(len(sequence)):`` um dann mit `i` auf die Elemente von `sequence` zuzugreifen ist in Python ein „anti pattern“, weil man direkt über die Elemente von `sequence` iterieren kann, ohne den Umweg über einen Laufindex. Wenn man über mehrere Objekte ”parallel” iterieren möchte, gibt es die `zip()`-Funktion. Wobei man die hier nur braucht, weil unschönerweise vorher drei ”parallele” Listen erstellt werden, was wie gesagt keine so gute Idee ist.

Die Wirkung von `strip()` ist offensichtlich nicht beziehungsweise falsch verstanden worden. Die beiden folgenden Zeilen haben genau den gleichen Effekt:

Code: Alles auswählen

firmenbezeichnung = firma[i].text.strip("\n").strip(" \n\n").strip(",")

# <=>

firmenbezeichnung = firma[i].text.strip("\n ,")
Falls das überraschend sein sollte, bitte noch mal in der Dokumentation nachlesen was `strip()` genau macht.

Wobei das Komma hier aktuell auch gar keinen Unterschied macht. Hast Du ein Beispiel wo Du tatsächlich vorne oder hinten am Text für den Firmennamen ein Komma hast/hattest?

Das entfernen eines " bei den Profiltextabschnitten macht keinen Sinn. Ich denke Du hast da irgendwie versucht ein Missverständnis zu bereinigen das dadurch entsteht, dass in der CSV der gleiche Trenner für Felder verwendet wird, den Du hier zum Trennen der Abschnitte innerhalb eines einzelnen Feldes verwendest. Die " in der CSV kommen nicht aus den Daten der Webseite, sondern sorgen dafür, dass das Feld mit den Feldtrennern im Inhalt eindeutig lesbar bleibt und da nicht zusätzliche Felder entstehen.

Ich frage mich ob da tatsächlich für jedes Jobangebot mehrere Datensätze erstellt werden sollen? Eines pro Abschnitt auf der Detailseite. Und dann auch noch ohne jegliche Informationen was das jeweils für ein Abschnitt ist.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import csv

import requests
from bs4 import BeautifulSoup


URL = "https://www.stepstone.de/jobs/Junior-Consultant.html"
CSV_FILENAME = "Stepstone_Kompetenzen_Juniorberater.csv"
SEPARATOR = ";"

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0)"
}


def get_soup(url):
    response = requests.get(url, headers=HEADERS)
    response.raise_for_status()
    return BeautifulSoup(response.text, "html.parser")


def main():
    with open(CSV_FILENAME, "w", newline="", encoding="utf-8") as csv_file:
        writer = csv.writer(csv_file, delimiter=SEPARATOR)
        writer.writerow(["Jobangebot", "Firma", "Kompetenzen"])

        for job_element in get_soup(URL).select(".job-element"):
            link_element = job_element.select_one(".job-element__url")
            angebotsbezeichnung = link_element.text.strip()
            firmenname = job_element.select_one(
                ".job-element__body__company"
            ).text.strip()
            #
            # TODO Sollen hier wirklich mehrere Datensätze pro Jobangebot
            #   erstellt werden?
            #
            for profil_abschnittselement in get_soup(
                link_element.get("href")
            ).select(".at-section-text-profile-content"):
                writer.writerow(
                    [
                        angebotsbezeichnung,
                        firmenname,
                        profil_abschnittselement.get_text(
                            strip=True, separator=SEPARATOR
                        ),
                    ]
                )

    print("Datei siehe Download Menuleiste als:", CSV_FILENAME)


if __name__ == "__main__":
    main()
Von der Länge her ist die `main()`-Funktion noch vertretbar, aber es könnte trotzdem Sinn machen die Eingabe, also in diesem Fall das auslesen der Informationen aus der/den Webseite(n) von der Ausgabe, also das speichern als CSV-Datei, zu trennen. So liessen sich die beiden Teile einfacher separat testen und auch austauschen. Falls man beispielsweise auch Daten von einem anderen Portal scrapen will und/oder die Daten (auch) als JSON oder in einer Datenbank speichern möchte.

Re: CSV writer Umlaute richtig darstellen

Verfasst: Montag 15. Februar 2021, 08:51
von Jankie
#edit: verlesen.