Probleme mit BeautifulSoup

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
mathiasellpunkt
User
Beiträge: 2
Registriert: Freitag 22. Januar 2021, 14:36

Hallo in die Runde,

ich taste mit DIY an das Thema WebScraping per Python ran. Als einfacher Einstieg lese ich von einer öffentlichen Seite Ist-Belegungen von Parkhäusern aus. Die Seite (Link unten) stellt die Belegungen von fünf Städten in gleicher Struktur zusammen. Das Auslasen klappt bei vier von fünf Städten auch gut - nur eine macht Probleme. Und das, obwohl ich im html eigentlich keinen wirklichen Unterschied erkennen kann.

Klappt: https://www.harmonyfm.de/service/parkha ... baden.html
Klappt nicht: https://www.harmonyfm.de/service/parkha ... nheim.html

Code, der erfolgreich ausgelesen wird (bpsw Wiesbaden):

Code: Alles auswählen

<tr class="even" data-facilityid="1">
<td>
<a title="mehr Infos"> RMCC</a>
<div class="trafficParkingInfoBox" data-facilityid="1">
<table class="trafficParkingDetailTable">
<tr>
<td>Plätze insgesamt:</td>
<td>700</td>
</tr>
<tr>
<td>Öffnungszeiten:</td>
<td>
Durchgehend geöffnet
</td>
</tr>
</table>
</div>
</td>
<td>
<span style="color: #7aa200; font-weight: bold;">663</span>
</td>
</tr>
Code, der Fehler schmeißt (Mannheim)

Code: Alles auswählen

<tr class="even" data-facilityid="CC">
<td>
<a title="mehr Infos">Collini Center</a>
<div class="trafficParkingInfoBox" data-facilityid="CC">
<table class="trafficParkingDetailTable">
<tr>
<td>Ein-&nbsp;und&nbsp;Ausfahrt:</td>
<td>Von der B 38 kommend, nach der Friedrich-Ebert-Brücke rechts ab, aber auf die linke Spur, am Neckar entlang und links in die 2. Einfahrt. Von der Innenstadt kommend auf dem Friedrichsring in die Collinistraße abbiegen, dann die erste Einfahrt links</td>
</tr>
<tr>
<td>Plätze&nbsp;insgesamt:</td>
<td>638</td>
</tr>
<tr>
<td>Öffnungszeiten:</td>
<td>24 Std. täglich (Tiefgarage Mo-Fr 5:30 bis 21:00 Uhr)</td>
</tr>
<tr>
<td>Einfahrtshöhen:</td>
<td>Tiefgarage: 2,00 m, Freigelände: 3,00 m</td>
</tr>
</table>
</div>
</td>
<td>
<span style="color: #7aa200; font-weight: bold;">317</span>
</td>
</tr>
Fehler, der geschmissen wird:

Code: Alles auswählen

'NoneType' object has no attribute 'find_next_sibling'
Den einzigen Unterschied, den ich in den beiden html-Codes finden kann, ist der Zellentext "Plätze insgesamt:" bzw. "Plätze&nbsp;insgesamt:", nachdem die Python-Funktion ja sucht. Hier fehlt mir allerdings die Idee, wie ich mit zweiterem umgehen kann.

Mein pythonCode ist aktuell dieser hier. Ich habe schon einen ErrorHandler eingebaut, damit der Code nicht abbricht, wenn dieses Problem auftaucht.

Code: Alles auswählen

response = requests.get(strURL)
  soup = BeautifulSoup(response.text, 'html.parser')
  results = soup.find_all('tr', {'class':['even', 'odd']})

  for result in results:
        # Each result is a new BeautifulSoup object.
        # You can use the same methods on it as you did before.
        title_elem = result.find('a', title='mehr Infos')
        facilityid_elem = result.find('div', class_='trafficParkingInfoBox')
        facilityid = facilityid_elem['data-facilityid'] 
    
        #Tabelle da drin finden
        tPD_table = result.find('table', class_='trafficParkingDetailTable')
        ph_error = "" 
        try:
            ph_plaetze_gesamt = tPD_table.find('td', text="Plätze insgesamt:").find_next_sibling('td').text
        except AttributeError:
            ph_error = 'Fehler: Kapazität nicht abrufbar.'
            ph_plaetze_frei = result.find('span').text
            ph_bel_p = ''
            ph_frei_p = ''
            ph_plaetze_gesamt = ''
            ph_plaetze_belegt = ''
            if ph_plaetze_frei.isnumeric():
                ph_status = 'offen.'
            else:
                ph_status = ph_plaetze_frei
                ph_plaetze_frei = ''
        else:
            ph_plaetze_gesamt = tPD_table.find('td', text="Plätze insgesamt:").find_next_sibling('td').text
            ph_plaetze_frei = result.find('span').text
            ph_bel_p = 0
            ph_frei_p = 0
            if ph_plaetze_frei.isnumeric():
                ph_frei_p = round(float(ph_plaetze_frei) / float(ph_plaetze_gesamt),3)
                ph_bel_p = round(1 - ph_frei_p,3)
                ph_plaetze_belegt = float(ph_plaetze_gesamt) - float(ph_plaetze_frei)
                ph_status = 'offen'
            else:
                ph_status = ph_plaetze_frei
                ph_plaetze_frei = ''
                ph_frei_p = ''
                ph_bel_p = ''
                ph_plaetze_belegt = ''
        
        print(now.strftime('%Y-%m-%d %H:%M:%S') + '\t' + str(strcity) + '\t' + str(facilityid) + '\t' + str(ph_plaetze_gesamt) + '\t' + str(ph_plaetze_frei) + '\t' + str(ph_plaetze_belegt) + '\t' + str(ph_frei_p) + '\t' + str(ph_bel_p) + '\t' + title_elem.text.strip() + '\t' + str(ph_status) + '\t' + str(ph_error))
Benutzeravatar
__blackjack__
User
Beiträge: 14053
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mathiasellpunkt: Ein "&nbsp;" ist ein anderes Leerzeichen als ein normales Leerzeichen.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

Grunddatentypen haben nichts in Namen verloren. Den Typen ändert man gar nicht so selten mal während der Programmentwicklung und dann muss man überall im Programm die betroffenen Namen ändern, oder man hat falsche, irreführende Namen im Quelltext.

Namen sollten auch keine kryptischen Abkürzungen oder Prä- oder Suffixe enthalten. Was sollen diese ganzen `ph_` an den Namen?

Bei der Antwort vom Server sollte man prüfen ob die nicht eine Fehlermeldung ist.

Sich einfach alle <tr>-Elemente geben zu lassen egal aus welcher Tabelle auf der ganzen Seite die kommen ist keine gute Idee. Selbst die Einschränkung auf CSS-Klassen "odd" und "even" kann ins Auge gehen wenn da irgendwann einmal noch eine Tabelle irgendwo gesetzt wird. Dabei hat die Tabelle mit den gewünschten Informationen sogar eine ID!

`AttributeError` zu behandeln heisst in der Regel einen selbstverschuldeten Programmierfehler irgendwie zu veartzten. Statt den Fehler zu beheben!

Was die Mannheim-Seite von der anderen Unterscheidet ist das nicht alle Tabellenzeilen ein Parkhaus beschreiben. Es gibt da Zeilen mit ”Zwischenüberschriften”. Die Zeilen die ein Parkhaus beschreiben haben ein "data-facilityid"-Attribut. Danach könnte man die selektieren.

Zahlen sollten gleich in Zahlen umgewandelt werden und nicht solange Texte bleiben bis damit gerechnet wird. Der selbe Name sollte nicht mal an eine Zahl und mal an eine Zeichenkette gebunden werden.

`round()` sollte man nur verwenden mit den gerundeten Werten weiterrechnen muss, nicht um die Anzahl der Nachkommastellen für die Anzeige zu begrenzen.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale. Oder in diesem Fall die `join()`-Methode auf Zeichenketten.

Code: Alles auswählen

#!/usr/bin/env python3
import re
from datetime import datetime as DateTime

import requests
from bs4 import BeautifulSoup


def main():
    now = DateTime.now()

    # city_name = "wiesbaden"
    city_name = "mannheim"
    url = f"https://www.harmonyfm.de/service/parkhaeuser/{city_name}.html"
    response = requests.get(url)
    response.raise_for_status()
    soup = BeautifulSoup(response.content, "html.parser")
    for row_node in soup.find(id="trafficParkingList")(
        "tr", {"data-facilityid": True}
    ):
        facility_id = row_node.get("data-facilityid").strip()
        facility_name = row_node.find("a", title="mehr Infos").text.strip()

        detail_table_node = row_node.find("table", "trafficParkingDetailTable")
        plaetze_gesamt = int(
            detail_table_node.find(
                "td", text=re.compile(r"Plätze\s+insgesamt:")
            )
            .find_next_sibling("td")
            .text.strip()
        )

        plaetze_frei = int(row_node.find("td").find_next_sibling("td").text)

        plaetze_belegt = plaetze_gesamt - plaetze_frei
        print(
            "\t".join(
                [
                    now.strftime("%Y-%m-%d %H:%M:%S"),
                    city_name,
                    facility_id,
                    str(plaetze_gesamt),
                    str(plaetze_frei),
                    str(plaetze_belegt),
                    f"{plaetze_frei / plaetze_gesamt:.3%}",
                    f"{plaetze_belegt / plaetze_gesamt:.3%}",
                    facility_name,
                ]
            )
        )


if __name__ == "__main__":
    main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
mathiasellpunkt
User
Beiträge: 2
Registriert: Freitag 22. Januar 2021, 14:36

__blackjack__ hat geschrieben: Freitag 22. Januar 2021, 18:50 @mathiasellpunkt: Ein "&nbsp;" ist ein anderes Leerzeichen als ein normales Leerzeichen.
Danke dir für die ausführliche Antwort! Das &nbsp; als nicht-Umbruch-Leerzeichen kannte ich; ich habe es nur nicht gelöst bekommen. Auf dem Pfad mit dem "re.compile(r"Plätze\s+insgesamt:")" war ich auch, konnte es aber irgendwie nicht zum Erfolg führen.

Das Präfix "ph_" steht schlicht für Parkhaus. Ansonsten werde ich mir deine Tipps zu Herzen nehmen und künftig berücksichtigen - ich steh, wie gesagt, bei python noch ganz am Anfang :)

Schönes Wochenende!
Antworten