Seite 1 von 1

Scrapen mit Beautiful Soup

Verfasst: Montag 18. Mai 2020, 20:17
von hansjürgen
Hallo liebe Community,

für ein Projekt habe ich mir vorgenommen die Daten zu den Häusern aus Immoscout24 mithilfe von BeautifulSoup zu scrapen und dann in einer CSV-Datei zu speichern, um die Daten später benutzen zu können. Ich habe bereits ein Skript geschrieben mit dem man die Dateien zu den Häusern bekommt. Das Programm funktioniert zwar einwandfrei, aber es dauert sehr lange und IDLE stürzt dabei immer ab. Wahrscheinlich liegt es daran, dass zuviel Speicher verbraucht wird. Um dieses Problem zu lösen habe ich versucht die Daten nicht alle auf einmal zu scrapen, sondern Bundesland für Bundesland, aber auch das hat nicht geklappt. Eine andere Idee wäre das Programm bezüglich des Speichers und der Laufzeit zu optimieren, doch dafür habe ich keine Möglichkeit gefunden. Für Lösungsansätze und Hinweise bezüglich der Lösung meines Problems wäre ich sehr dankbar.

Code: Alles auswählen

import bs4 as bs
import urllib.request
import pandas as pd
from functools import reduce

df = []
bundesländer = ['baden-wuerttemberg','bayern','berlin/berlin','brandenburg','bremen','hamburg/hamburg','hessen','mecklenburg-vorpommern',
                'niedersachsen','nordrhein-westfalen','rheinland-pfalz','saarland','sachsen','sachsen-anhalt','schleswig-holstein','thueringen'] 

def get_links(link):
    # Links der einzelnen Häuser
    soup = bs.BeautifulSoup(urllib.request.urlopen(link).read(),'html.parser')
    a_tags = soup.find_all('a',class_='result-list-entry__brand-title-container')
    return ['https://www.immobilienscout24.de'+a_tag['href']+'#/' for a_tag in a_tags]
    

def get_data(link):
    # extrahiert die Daten eines Hauses
    soup =  bs.BeautifulSoup(urllib.request.urlopen(link).read(),'html.parser')
    data = {}
    house_data = soup.find('div',class_ = 'criteriagroup criteria-group--two-columns')
    house_data_dt = house_data.find_all('dt')
    house_data_dd = house_data.find_all('dd')
    a_tags = soup.find_all('a',class_='breadcrumb__link')[1:]
    keys = ['Bundesland','Stadt','Ort']
    data['Kaufpreis'] = soup.find('div',class_ = 'is24qa-kaufpreis is24-value font-semibold is24-preis-value').text
    data['Ort'] = soup.find('span',class_ = 'zip-region-and-country').text
    for a_tag,key in zip(a_tags,keys):
        data[key] = a_tag.text
    for dt_tag_house,dd_tag_house in zip(house_data_dt,house_data_dd):
        try:
            data[dt_tag_house.text].append(dd_tag_house.text)
        except KeyError:
                data[dt_tag_house.text] = dd_tag_house.text
    try:
        building_data = soup.find('div',class_ = 'criteriagroup criteria-group--border criteria-group--two-columns criteria-group--spacing')
        building_data_dt = building_data.find_all('dt')
        building_data_dd = building_data.find_all('dd')
        for dt_tag_building,dd_tag_building in zip(building_data_dt,building_data_dt):
            try:
                data[dt_tag_building.text].append(dd_tag_building.text)
            except KeyError:       
                data[dt_tag_building.text] = dd_tag_building.text
    except AttributeError:
        pass
    
    return data

def add_dictionary(dict1,dict2):
    # fügt die Daten von zwei Häusern zusammen
    missing_values = len(dict1['Kaufpreis'])*[None]
    for key in dict2.keys():
        try:
            dict1[key].append(dict2[key])
        except KeyError:
            dict1[key] = missing_values+[dict2[key]]
    for key in [i for i in dict1.keys() if i not in dict2.keys()]:
        dict1[key].append(None)
    return dict1

for bundesland in bundesländer:
    main_link = 'https://www.immobilienscout24.de/Suche/de/'+bundesland+'/haus-kaufen'
    number_pages = int(bs.BeautifulSoup(urllib.request.urlopen(main_link).read(),'html.parser').find_all('option')[-1].text)
    sources = [main_link+'?pagenumber='+str(i) for i in range(1,number_pages+1)]
    data = []
    print(bundesland)
    for source in sources:
        print(source)
        for sub_link in get_links(source):
            print(sub_link)
            # Print Statements dienen als Überprüfung, ob der COde funktioniert
            try:
                print(sub_link)
                data.append(get_data(sub_link))
            except urllib.error.URLError:
                continue
            print(data)
    df.append(reduce(add_dictionary,data))


df = reduce(add_dictionary,df)
df = pd.DataFrame(df)

Re: Scrapen mit Beautiful Soup

Verfasst: Montag 18. Mai 2020, 20:45
von nezzcarth
Es gibt eine Reihe von Verbesserungspunkten (z.B. bei deinem Umgang mit Exceptions), aber ich würde als Erstes mal damit beginnen, Pandas rauszuwerfen. Das ist ein ziemlicher Overhead, nur um ein bisschen CSV zu erzeugen. Nimm normale, integrierte Datenstrukturen und speicher sie mit dem CSV-Modul aus der Standardbibliothek. Und wenn du mit Generatoren arbeitest, geht das sogar lazy, ohne dass du dir groß Sorgen um Speicherprobleme machen musst.

Die Absturzursache sollte man am Traceback ablesen können.

Re: Scrapen mit Beautiful Soup

Verfasst: Montag 18. Mai 2020, 22:51
von __blackjack__
@hansjürgen: `bs4` beim Import auf `bs` zu kürzen ist nicht wirklich sinnvoll. `urllib.error` wurde nicht importiert.

Der nichtssagende Name `df` wird im gleichen Namensraum nacheinander für drei recht unterschiedliche Dinge verwendet.

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

Konstanten werden KOMPLETT_GROSS geschrieben.

Literale Zeichenketten und Werte mit ``+`` und `str()` zusammenstückeln ist eher BASIC denn Python. Python hat dafür die `format()`-Methode auf Zeichenketten und ab Python 3.6 f-Zeichenkettenliterale.

``continue`` würde ich meiden wo es nur geht, und es geht fast immer. Das ist ein Sprung der nicht an der Quelltextstruktur erkennbar ist, und der macht es schwieriger und fehleranfälliger das Programm zu warten und zu refaktorisieren.

Die Antworten von Webanfrangen sollte man, genau wie Dateien, auch wieder schliessen.

In `get_data()` ist das definieren von Namen und die anschliessende Verwendung etwas unübersichtlich. Es werden teilweise Namen definiert die erst später verwendet werden.

`building_data_dd` wird nicht verwendet. Dafür wird `building_data_dt` zweimal verwendet, was glaube ich so falsch sein dürfte.

Das was da mit ``except KeyError:`` gemacht wird, dürfte so nicht funktionieren. Denn einmal wird eine Zeichenkette hinter dem Schlüssel hinterlegt, und wenn dann der nächste Wert mit dem gleichen Schlüssel kommt, wird versucht etwas an diese Zeichenkette mit `append()` anzugängen. Zeichenketten haben aber keine solche Methode. Das wird zumindest im zweiten Fall dann von dem `AttributeError` geschluckt und die Daten in der „definition list“ werden nicht vollständig ausgewertet. Das sieht sehr kaputt/falsch aus!

Bei `add_dictionary()` verlierst Du mich dann komplett als Leser weil ich so gar nicht verstehe warum da eine Liste mit so vielen `None`-Werten erstellt wird, wie der "Kaufpreis" — eine Zeichenkette — Zeichen hat. Das macht irgendwie so überhaupt keinen Sinn. Warum da überhaupt die ganzen Datensätze letztlich zu einem zusammengemanscht werden, verstehe ich nicht wirklich.

Das die Funktion kein neues Wörterbuch zurückgibt sondern das erste Argument verändert *und* zurückgibt ist unschön. Das ist keine saubere funktionale Schnittstelle.

„Schlüssel die in Wörterbuch A vorkommen aber nicht in Wörterbuch B“ lässt sich kürzer und effizienter mit ``-`` ausdrücken.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import urllib.error
import urllib.request
from functools import reduce

import bs4
import pandas as pd

BASE_URL = "https://www.immobilienscout24.de"
BUNDESLAENDER = [
    "baden-wuerttemberg",
    "bayern",
    "berlin/berlin",
    "brandenburg",
    "bremen",
    "hamburg/hamburg",
    "hessen",
    "mecklenburg-vorpommern",
    "niedersachsen",
    "nordrhein-westfalen",
    "rheinland-pfalz",
    "saarland",
    "sachsen",
    "sachsen-anhalt",
    "schleswig-holstein",
    "thueringen",
]


def get_soup(url):
    with urllib.request.urlopen(url) as response:
        return bs4.BeautifulSoup(response.read(), "html.parser")


def get_links(link):
    # Links der einzelnen Häuser
    return (
        f"{BASE_URL}{a_tag['href']}#/"
        for a_tag in get_soup(link)(
            "a", "result-list-entry__brand-title-container"
        )
    )


def get_data(link):
    # extrahiert die Daten eines Hauses
    soup = get_soup(link)
    data = {
        "Kaufpreis": soup.find(
            "div", "is24qa-kaufpreis is24-value font-semibold is24-preis-value"
        ).text,
        "Ort": soup.find("span", "zip-region-and-country").text,
    }
    data.update(
        (key, a_tag.text)
        for key, a_tag in zip(
            ["Bundesland", "Stadt", "Ort"], soup("a", "breadcrumb__link")[1:]
        )
    )

    house_data = soup.find("div", "criteriagroup criteria-group--two-columns")
    data.update(
        (dt_tag.text, dd_tag.text)
        for dt_tag, dd_tag in zip(house_data("dt"), house_data("dd"))
    )

    building_data = soup.find(
        "div",
        (
            "criteriagroup"
            " criteria-group--border"
            " criteria-group--two-columns"
            " criteria-group--spacing"
        ),
    )
    data.update(
        (dt_tag.text, dd_tag.text)
        for dt_tag, dd_tag in zip(building_data("dt"), building_data("dd"))
    )
    return data


def add_dictionary(dict_a, dict_b):
    # fügt die Daten von zwei Häusern zusammen
    missing_values = len(dict_a["Kaufpreis"]) * [None]
    for key, value in dict_b.items():
        try:
            dict_a[key].append(value)
        except KeyError:
            dict_a[key] = missing_values + [value]
    for key in dict_a.keys() - dict_b.keys():
        dict_a[key].append(None)
    return dict_a


def main():
    df = []
    for bundesland in BUNDESLAENDER:
        print(bundesland)
        url = f"{BASE_URL}/Suche/de/{bundesland}/haus-kaufen"
        page_count = int(get_soup(url)("option")[-1].text)
        data = []
        for page_url in (
            f"{url}?pagenumber={i}" for i in range(1, page_count + 1)
        ):
            print(page_url)
            for sub_link in get_links(page_url):
                print(sub_link)
                try:
                    data.append(get_data(sub_link))
                except urllib.error.URLError:
                    pass
                else:
                    print(data)
        df.append(reduce(add_dictionary, data))

    df = reduce(add_dictionary, df)
    df = pd.DataFrame(df)


if __name__ == "__main__":
    main()
Ein Grund für zu hohen Speicherverbrauch könnte sein, dass das `text`-Attribut von den BeautifulSoup-Objekten keine normale Zeichenkette ist, sondern ein `bs4.NavigableString` an dem jeweils immer noch der gesamte Objektbaum von dem HTML-Dokument dran hängt.

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 19. Mai 2020, 15:43
von hansjürgen
Erstmal vielen dank für die Verbesserungsvorschläge. Darauf wäre ich nicht gekommen, denn vieles wusste ich nicht.
``continue`` würde ich meiden wo es nur geht, und es geht fast immer. Das ist ein Sprung der nicht an der Quelltextstruktur erkennbar ist, und der macht es schwieriger und fehleranfälliger das Programm zu warten und zu refaktorisieren.
Der Sinn von continue lag darin, dass Häuser übersprungen werden, die noch nicht direkt zum Verkauf stehen. In einer vorherigen Version wo ich bloß Kaufpreis, Grundstücksfläche, Wohnfläche und Zimmeranzahl gescrapet habe, brauchte ich das nicht bzw. habe ich eine andere Methode verwendet, um die Häuser zu überspringen. Ich dachte, dass die Methode mit continue eine einfachere Lösung darstellt.
Die Antworten von Webanfrangen sollte man, genau wie Dateien, auch wieder schliessen.
Das verstehe ich leider nicht ganz. Zum einen was ist mit Webanfrage genau gemeint (ich vermute das der Teil, der in der getsoup Funktion vorhanden ist) und zum anderen wie schließt man dann die Webanfrage und wann, weil wenn ich sie zu früh schließe dann kann nicht alles gescrapet werden.
Das was da mit ``except KeyError:`` gemacht wird, dürfte so nicht funktionieren. Denn einmal wird eine Zeichenkette hinter dem Schlüssel hinterlegt, und wenn dann der nächste Wert mit dem gleichen Schlüssel kommt, wird versucht etwas an diese Zeichenkette mit `append()` anzugängen. Zeichenketten haben aber keine solche Methode. Das wird zumindest im zweiten Fall dann von dem `AttributeError` geschluckt und die Daten in der „definition list“ werden nicht vollständig ausgewertet. Das sieht sehr kaputt/falsch aus!
Das Problem lässt sich dadurch lösen, indem das erste Dictionary bei data und df den Key `Kaufpreis` und als Value eine Liste hat. Denn falls ein Key in dict1 nicht existiert wird der Wert innerhalb einer Liste hinzugefügt.
Bei `add_dictionary()` verlierst Du mich dann komplett als Leser weil ich so gar nicht verstehe warum da eine Liste mit so vielen `None`-Werten erstellt wird, wie der "Kaufpreis" — eine Zeichenkette — Zeichen hat. Das macht irgendwie so überhaupt keinen Sinn. Warum da überhaupt die ganzen Datensätze letztlich zu einem zusammengemanscht werden, verstehe ich nicht wirklich.
Das Problem war, dass nicht jedes Haus die gleichen Daten hat. Zwar hat jedes Haus einen Kaufpreis, eine Wohnfläche etc. aber es gibt Häuser die z.B ein Attribut haben was das andere Haus nicht hat. Daher wird dann das neue Attribut dem Datensatz hinzugefügt und bei allen anderen Häusern, die dieses Attribut zuvor nicht hatten wird ein None hinzugefügt, was für `dieses Attribut ist bei diesem Haus nicht vorhanden` steht.
Die einzelnen Datensätze werden zu einem ganzen zusammengemanscht, um sie später für eine Datenanalyse verwenden zu können.
Das ist ein ziemlicher Overhead, nur um ein bisschen CSV zu erzeugen. Nimm normale, integrierte Datenstrukturen und speicher sie mit dem CSV-Modul aus der Standardbibliothek. Und wenn du mit Generatoren arbeitest, geht das sogar lazy, ohne dass du dir groß Sorgen um Speicherprobleme machen musst.
Ich kannte bisjetzt nur Pandas und fand das intuitiv. Aber ok ich werde die Standardbibliothek verwenden. Das mit den Generatoren habe ich nicht ganz verstanden. Ist damit gemeint, dass ich statt dem for loop eine yield Anweisung verwenden soll, weil dort die Daten on the fly, also nur kurz für den Verwendungszweck gespeichert werden?

Den Rest habe ich verstanden.

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 19. Mai 2020, 16:42
von __blackjack__
@hansjürgen: Mit der Webanfrage ist das Ergebnis von `urlopen()` gemeint. Da das `HTTPResponse`-Objekt das man da bekommt ein Kontextmanager ist, kann man das mit ``with`` verwenden, was dann dafür sorgt, dass wenn der Programmfluss den ``with``-Block verlässt, die Verbindung geschlossen wird.

Re: Scrapen mit Beautiful Soup

Verfasst: Mittwoch 20. Mai 2020, 17:39
von nezzcarth
``continue`` würde ich meiden wo es nur geht, und es geht fast immer. Das ist ein Sprung der nicht an der Quelltextstruktur erkennbar ist, und der macht es schwieriger und fehleranfälliger das Programm zu warten und zu refaktorisieren.
Ich verstehe die Argumentation dahinter. Aber stückweise ist es, finde ich, auch eine Abwägungssache, ob man dafür dann tiefere Verschachtelungen in Kauf nehmen möchte, weil der Code-Block ja dann häufig in einen If-Block gesteckt wird. Das finde ich auch nicht immer schön zu lesen, wenn es sich aufsummiert. Mit continue wird der Code tendenziell eher etwas flacher.

Re: Scrapen mit Beautiful Soup

Verfasst: Mittwoch 20. Mai 2020, 18:52
von __blackjack__
@nezzcarth: Man spart Tiefe, aber man verbaut sich damit die Möglichkeit noch etwas ans Ende jedes Schleifendurchlaufs zu schreiben, und wenn man was gegen zu viel Tiefe machen möchte in dem man etwas aus der Schleife in eine Funktion heraus zieht, kann man Teile mit ``continue`` nicht einfach mitnehmen, weil man das ja nicht aus der Schleife nehmen kann.

Und der Code wird nur optisch flacher, denn der Sprung ist ja trotzdem noch da, nur eben nicht an der Struktur der Quelltextes so leicht zu erkennen. Ich würde ``continue`` wenn dann nur ganz am Anfang der Schleife verwenden, quasi als „early exit“ wie man manchmal bei Funktion/Methoden ein bedingtes ``return`` an den Anfang schreibt.

Re: Scrapen mit Beautiful Soup

Verfasst: Mittwoch 20. Mai 2020, 20:20
von nezzcarth
@blackjack: Hm. Ich glaube, das muss ich mal ausprobieren. ;) Danke für die Argumente.

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 26. Mai 2020, 12:21
von hansjürgen
Ich habe jetzt einige Änderungen vorgenommen, die von blackjack übernommen und das Programm verbessert. Die Dictionaries werden jetzt nicht mehr aufwendig zusammengemanscht, da ich eine bessere Möglichkeit gefunden habe, um die Daten von den Webseiten zu scrapen. Das kritisierte continue habe ich durch ein pass ersetzt. Den NavigableString habe ich mithilfe der str Funktion in einen normalen String umgewandelt, sodass nicht mehr der Objektbaum dranhängt, was den Speichervebrauch reduziert. Zudem werden nur die Häuser betrachtet, die bereits fertiggestellt sind. Meine letzte Frage wäre: Warum dauert das Programm so lange? Denn selbst wenn ich das Skript ein paar Stunden laufen ließ kam ich nicht bis zur "Fertig" Nachricht.

Code: Alles auswählen

import urllib.error
import urllib.request
import bs4
import csv
import os

BASE_URL = "https://www.immobilienscout24.de"
BUNDESLAENDER = [
    "baden-wuerttemberg",
    "bayern",
    "berlin/berlin",
    "brandenburg",
    "bremen",
    "hamburg/hamburg",
    "hessen",
    "mecklenburg-vorpommern",
    "niedersachsen",
    "nordrhein-westfalen",
    "rheinland-pfalz",
    "saarland",
    "sachsen",
    "sachsen-anhalt",
    "schleswig-holstein",
    "thueringen",
]


def get_soup(url):
    with urllib.request.urlopen(url) as response:
        return bs4.BeautifulSoup(response.read(), "html.parser")


def get_links(link):
    # Links der einzelnen Häuser
    return (
        f"{BASE_URL}{a_tag['href']}#/"
        for a_tag in get_soup(link)(
            "a", "result-list-entry__brand-title-container"
        )
    )


def get_data(link):
    # extrahiert die Daten eines Hauses
    soup = get_soup(link)
    data = {
        "kaufpreis": "-",
        "typ": "-",
        "wohnflaeche-ca": "-",
        "grundstueck-ca": "-",
        "nutzflaeche-ca": "-",
        "bezugsfrei-ab": "-",
        "zimmer": "-",
        "schlafzimmer": "-",
        "badezimmer": "-",
        "etagenanzahl": "-",
        "garage-stellplatz": "-",
        "baujahr": "-",
        "qualitaet-der-ausstattung": "-",
        "modernisierung-sanierung": "-",
        "objektzustand": "-",
        "heizungsart": "-",
        "wesentliche-energietraeger": "-",
        "energieausweis": "-",
        "energieausweistyp": "-",
        "endenergieverbrauch": "-",
        "energieeffizienzklasse": "-"
        }
    for key in data.keys():
        try:
            data.update(
                {key: str(soup.find("dd",class_=f"is24qa-{key} grid-item three-fifths").string)}
                )
        except (KeyError,AttributeError):
            pass
    data.update(
        Bundesland = "-",
        Stadt = "-",
        Ort = "-"
        )
    for key, a_tag in zip(["Bundesland", "Stadt", "Ort"], soup("a","breadcrumb__link")[1:]): 
        data.update(
            {key: str(a_tag.string)}
            )
    return data

def main():
    dict_data = []
    csv_columns = [
        "kaufpreis",
        "typ",
        "wohnflaeche-ca",
        "grundstueck-ca",
        "nutzflaeche-ca",
        "bezugsfrei-ab",
        "zimmer",
        "schlafzimmer",
        "badezimmer",
        "etagenanzahl",
        "garage-stellplatz",
        "baujahr",
        "qualitaet-der-ausstattung",
        "modernisierung-sanierung",
        "objektzustand",
        "heizungsart",
        "wesentliche-energietraeger",
        "energieausweis",
        "energieausweistyp",
        "endenergieverbrauch",
        "energieeffizienzklasse",
        "Bundesland",
        "Stadt",
        "Ort"
        ]
    csv_file = r"C:\Users\erdogan seref\Documents\Desktop\ML Projects\germany_housing_data.csv"
    for bundesland in BUNDESLAENDER:
        print(bundesland)
        url = f"{BASE_URL}/Suche/de/{bundesland}/haus-kaufen?constructionphasetypes=completed"
        page_count = int(str(get_soup(url)("option")[-1].string))
        for page_url in (
            f"{url}&pagenumber={i}" for i in range(1, page_count + 1)
        ):
            print(page_url)
            for sub_link in get_links(page_url):
                print(sub_link)
                try:
                    dict_data.append(get_data(sub_link))
                except urllib.error.URLError:
                    pass
    # Speichern der Dateien in einer CSV Datei
    with open(csv_file, "w") as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=csv_columns)
        writer.writeheader()
        for data in dict_data:
            writer.writerow(data)
    print("Fertig")

    
if __name__ == "__main__":
    main()

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 26. Mai 2020, 13:27
von __blackjack__
@hansjürgen: Es könnte sein, dass das Programm effizienter sein könnte, beispielsweise weil die gesamte Seite immer wieder nach Dingen durchsucht wird statt das Suchgebiet vorher einzuschränken. Oder das über den View in die Schlüssel eines Wörterbuchs iterieren und in der Schleife das selbe Wörterbuch verändern kann zu komischem/falschen Verhalten führen. Oder der Webseitenbetreiber bremst Dich aus weil er Scrapen nicht mag. Oder was anderes, oder eine Kombination davon.

Wenn man etwas beschleunigen will, ist der erste Schritt üblicherweise zu messen wo die Zeit verbraucht wird, also wo es sich am meisten lohnt zu schauen ob man das schneller bekommt.

Falls der Anbieter drosselt, kann es Sinn machen künstlich langsamer zu sein, und damit versuchen unter der Schwelle zu bleiben an der man vom Anbieter ausgebremst wird.

Insgesamt kann es bei lang laufenden Programmen auch sinnvoll sein eine Fortschrittsanzeige einzubauen. Hier würde sich beispielsweise das `tqdm`-Modul anbieten und Fortschrittsanzeigen zu schachteln: die Länder und die einzelnen Detailseiten beispielsweise.

`os` wird importiert, aber nirgends verwendet.

In einer Funktion die Namen `csv_file` und `csvfile` zu verwenden ist verwirrend. Eins davon ist auch gar keine Datei sondern ein Datei*name*.

Bei Textdateien sollte man beim öffnen immer eine Kodierung angeben. Und bei CSV-Dateien muss man ``newline=""`` als Argument übergeben, damit keine kaputten CSV-Dateien erzeugt werden können.

`dict.update()` ist der falsche Weg wenn man da grundsätzlich ein Wörterbuch mit genau einem Schlüssel/Wert-Paar übergibt.

Datenwiederholungen sollte man vermeiden. Die Spaltennamen kommen alle mindestens zweimal im Quelltext vor.

Einige Namen könnten besser sein. Statt dem generischen `data` beispielsweise `house_details`.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import csv
import os
import urllib.error
import urllib.request

import bs4

CSV_FILENAME = (
    r"C:\Users\erdogan seref\Documents\Desktop\ML Projects"
    r"\germany_housing_data.csv"
)
BASE_URL = "https://www.immobilienscout24.de"
BUNDESLAENDER = [
    "baden-wuerttemberg",
    "bayern",
    "berlin/berlin",
    "brandenburg",
    "bremen",
    "hamburg/hamburg",
    "hessen",
    "mecklenburg-vorpommern",
    "niedersachsen",
    "nordrhein-westfalen",
    "rheinland-pfalz",
    "saarland",
    "sachsen",
    "sachsen-anhalt",
    "schleswig-holstein",
    "thueringen",
]
HOUSE_DETAIL_NAMES = [
    "kaufpreis",
    "typ",
    "wohnflaeche-ca",
    "grundstueck-ca",
    "nutzflaeche-ca",
    "bezugsfrei-ab",
    "zimmer",
    "schlafzimmer",
    "badezimmer",
    "etagenanzahl",
    "garage-stellplatz",
    "baujahr",
    "qualitaet-der-ausstattung",
    "modernisierung-sanierung",
    "objektzustand",
    "heizungsart",
    "wesentliche-energietraeger",
    "energieausweis",
    "energieausweistyp",
    "endenergieverbrauch",
    "energieeffizienzklasse",
]
HOUSE_LOCATION_NAMES = ["Bundesland", "Stadt", "Ort"]
COLUMN_NAMES = HOUSE_DETAIL_NAMES + HOUSE_LOCATION_NAMES


def get_soup(url):
    with urllib.request.urlopen(url) as response:
        return bs4.BeautifulSoup(response.read(), "html.parser")


def get_houses_links(link):
    # Links der einzelnen Häuser
    return (
        f"{BASE_URL}{a_tag['href']}#/"
        for a_tag in get_soup(link)(
            "a", "result-list-entry__brand-title-container"
        )
    )


def get_house_details(link):
    # extrahiert die Daten eines Hauses
    soup = get_soup(link)
    data = dict.fromkeys(COLUMN_NAMES, "-")
    for key in HOUSE_DETAIL_NAMES:
        try:
            data[key] = str(
                soup.find(
                    "dd", class_=f"is24qa-{key} grid-item three-fifths"
                ).string
            )
        except (KeyError, AttributeError):
            pass

    data.update(
        (key, str(a_tag.string))
        for key, a_tag in zip(
            HOUSE_LOCATION_NAMES, soup("a", "breadcrumb__link")[1:]
        )
    )
    return data


def main():
    houses = []
    for bundesland in BUNDESLAENDER:
        print(bundesland)
        url = (
            f"{BASE_URL}/Suche/de/{bundesland}/haus-kaufen"
            f"?constructionphasetypes=completed"
        )
        page_count = int(str(get_soup(url)("option")[-1].string))
        for page_url in (
            f"{url}&pagenumber={i}" for i in range(1, page_count + 1)
        ):
            print(page_url)
            for house_link in get_houses_links(page_url):
                print(house_link)
                try:
                    houses.append(get_house_details(house_link))
                except urllib.error.URLError:
                    pass

    with open(CSV_FILENAME, "w", encoding="utf-8", newline="") as csv_file:
        writer = csv.DictWriter(csv_file, fieldnames=COLUMN_NAMES)
        writer.writeheader()
        writer.writerows(houses)

    print("Fertig")


if __name__ == "__main__":
    main()

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 26. Mai 2020, 13:37
von Sirius3
Die Funktion `get_links` ist fehlerhaft, und tut, wenn überhaupt, nur zufällig. Um URLs zusammenzubauen kennt urllib eine Funktion. Da sollte dann auch die richtige Basis-URL benutzt werden und nicht eine fixe.

Wörterbücher, über die man gerade iteriert, dürfen nicht geändert werden, das tut also auch nur in diesem Spezielfall, `update` für einen einzelnen Key ist ein Fehler. Statt Wörterbücher zu ändern erzeugt man einfach ein neues. Hier das Wörterbuch mit Dummy-Werten zu befüllen, ist eh nicht richtig. Man nimmt eine Liste von Keys, und fügt, falls nicht vorhanden einfach enien "-" als Wert ein. `string` sollte doch schon ein String sein?
Dass `csv_columns` die selben Keys nochmal im Code stehen hat, sollte nicht sein, das wäre eine Konstante am Anfang des Skripts.

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 26. Mai 2020, 13:54
von __blackjack__
@Sirius3: `string` liefert einen `bs4.NavigableString` wo das gesamte Dokument noch dran hängt. Das heisst selbst wenn man nur eine einzelne solche Zeichenkette aus einem geparsten HTML-Dokument weiterverwendet, hängt da immer noch der gesamte BeautifulSoup-Objektbaum dran und verbraucht Arbeitsspeicher:

Code: Alles auswählen

In [263]: soup = bs4.BeautifulSoup("<p><span>some text</span></p>", "html.parser")                                                                    

In [264]: soup.span.string                                                      
Out[264]: 'some text'

In [265]: soup.span.string.parent                                               
Out[265]: <span>some text</span>

In [266]: soup.span.string.parent.parent                                        
Out[266]: <p><span>some text</span></p>

In [267]: type(soup.span.string)                                                
Out[267]: bs4.element.NavigableString

In [268]: isinstance(soup.span.string, str)                                     
Out[268]: True
Ein `str()`-Aufruf liefert dann nur die Zeichenkette und kappt die Verbindung zum Dokument.

Re: Scrapen mit Beautiful Soup

Verfasst: Dienstag 4. August 2020, 10:42
von hansjürgen
Sirius3 hat geschrieben: Dienstag 26. Mai 2020, 13:37 Die Funktion `get_links` ist fehlerhaft, und tut, wenn überhaupt, nur zufällig. Um URLs zusammenzubauen kennt urllib eine Funktion. Da sollte dann auch die richtige Basis-URL benutzt werden und nicht eine fixe.

Wörterbücher, über die man gerade iteriert, dürfen nicht geändert werden, das tut also auch nur in diesem Spezielfall, `update` für einen einzelnen Key ist ein Fehler. Statt Wörterbücher zu ändern erzeugt man einfach ein neues. Hier das Wörterbuch mit Dummy-Werten zu befüllen, ist eh nicht richtig. Man nimmt eine Liste von Keys, und fügt, falls nicht vorhanden einfach enien "-" als Wert ein. `string` sollte doch schon ein String sein?
Dass `csv_columns` die selben Keys nochmal im Code stehen hat, sollte nicht sein, das wäre eine Konstante am Anfang des Skripts.
Und wie heißt diese Funktion? Warum ist die Funktion fehlerhaft und wie lässt sich das beheben?