Auslesen einer Tabelle aus dem Web (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
eibrahi
User
Beiträge: 2
Registriert: Freitag 23. August 2019, 10:48

Hallo Leute,

habe ein kleines Problemchen! Ich möchte eine Tabelle aus dem Web auslesen um diese dann in ein Dataframe zu hinterlegen.
Ich weis das es zu diesem Thema vieles gibt habe auch jede Menge gelesen habe aber trotzdem leider den Fehler nicht finden können.

Ich möchte von dieser Seite "https://de.numbeo.com/lebenshaltungskos ... title=2018" die Tabelle mit den Lebenshaltungskosten und den dazugehörigen Indexen auslesen.

Im ersten Schritt hole ich mir die Spalten Überschriften:

Code: Alles auswählen

tables = soup.findChildren('table')
#my_table = tables[2]

rows = soup.findChildren(['tr', 'td'])

for row in rows:
    cells = row.findChildren('th')
    for cell in cells:
        value = cell.string
        print("The value in this cell is %s" % value)
Das ist das Ergebnis:
The value in this cell is Platz
The value in this cell is Stadt
The value in this cell is Lebenshaltungskosten-Index
The value in this cell is Miet-Index
The value in this cell is Index Lebenshaltungskosten + Miete
The value in this cell is Lebensmittel-Index
The value in this cell is Gaststätten-Index
The value in this cell is Kaufkraft-Index

Und das ist auch alles richtig!

Aber wenn ich jetzt versuche die zu den Überschriften der Tabelle die jeweiligen Werte auszulesen dann bringt er mir mit diesem Code nur die Städtenamen aber die darunter aufgelisteten Werte alle sammt in den "td" Tags bringt er nicht. Also er bringt mir immer nur den ersten Wert so zu sagen für die Stadt aber die restlichen nicht und ich ´komme hier nicht mehr weiter und bitte um eure Hilfe.

Code: Alles auswählen

for row in rows:
    cells = row.findAll(['td'])
    for cell in cells:
        value = cell.string
        print("The value in this cell is %s" % value)


Ich bedanke mich schon im Voraus für eure Unterstützung!
Liebe Grüße
Elvir
eibrahi
User
Beiträge: 2
Registriert: Freitag 23. August 2019, 10:48

Hallo Leute,

ich war zu voreilig habe die Lösung mir erarbeitet.

Lösung:

Code: Alles auswählen

ueberschrift = []
cells = soup.findChildren('th')
for cell in cells:
    value = cell.string
    ueberschrift.append(value)
ueberschrift

Code: Alles auswählen

werte = []
i = 0
for row in table.find_all('tr'):
    werte.append([])
    for cell in row.find_all('td'):
        value = cell.text
        werte[i].append(value)
        werte[i][0] = i
    i= i+1
llrem = werte[0]
werte.remove(llrem)
werte
Mit den beiden Listen "ueberschriften" und "werte" erstelle ich mein Dataframe!

Vielen Dank
Elvir
Benutzeravatar
snafu
User
Beiträge: 6866
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

eibrahi hat geschrieben: Freitag 23. August 2019, 23:33 Mit den beiden Listen "ueberschriften" und "werte" erstelle ich mein Dataframe!
Dann könntest du auch gleich read_html() benutzen:

Code: Alles auswählen

import pandas as pd
from pandas.api.types import is_integer_dtype

URL = 'https://de.numbeo.com/lebenshaltungskosten/rankings?title=2018'

def parse_table(url, table_index):
    df = pd.read_html(url)[table_index]
    for column in df:
        if is_integer_dtype(df[column].dtype):
            df[column] /= 100
    return df

def main():
    df = parse_table(URL, 2)
    print(df.head())

if __name__ == '__main__':
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Die Schleife mit dem Teilen kann man sich sparen wenn man `thousands` und `decimal` passend übergibt:

Code: Alles auswählen

In [168]: df = pd.read_html(URL, thousands="", decimal=",")[2]                  

In [169]: df.head()                                                             
Out[169]: 
   Platz              Stadt       ...         Gaststätten-Index  Kaufkraft-Index
0    NaN  Hamilton, Bermuda       ...                    158.75           112.26
1    NaN    Zürich, Schweiz       ...                    135.76           142.70
2    NaN      Genf, Schweiz       ...                    129.74           130.96
3    NaN     Basel, Schweiz       ...                    127.22           139.01
4    NaN      Bern, Schweiz       ...                    119.48           112.71

[5 rows x 8 columns]
Als nächsten Schritt könnte man die „Platz“-Spalte rauswerfen, die enthält nur NaN-Werte – wird offenbar durch JavaScript gefüllt.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@eibrahi: Noch mal zum `BeautifulSoup`-Code:

Wenn man nur nach einem Tag sucht, braucht man den Tagnamen nicht in einer Liste übergeben.

Die alten Namen, die nicht den Python-Namenskonventionen entsprechen, sollte man nicht mehr verwenden. Du bist da auch inkonsequent in dem Du sowohl `find_all()` als auch `findAll()` verwendest. Nach welchem Kriterium hast Du *das* denn entschieden?

Du solltest Dich mehr an der Tabellenstruktur orientieren. Die Kopfzeile steckt in einem <thead> und die normalen Zeilen in einem <tbody>. Wenn man sich daran orientiert, dann braucht man am Ende auch keine leere Liste vom Anfang des Ergebnisses entfernen, weil man die gar nicht erst erzeugt. Zumal das Löschen sehr schräg gelöst ist. Du nimmst das erste Element aus der Liste und rufst dann `remove()` auf, was das Element in der Liste *sucht* – Du weisst doch aber schon das es das erste ist, also hättest Du auch einfach ``del werte[0]`` schreiben können. `llrem` ist auch ein schlechter Name. Was soll das bedeuten?

Statt den Platz als `i` manuell mit den Tabellenzeilen mit zu zählen würde man `enumerate()` verwenden. Und ich würde das auch nicht `i` sondern `platz` beziehungsweise `rank` nennen, dann weiss der Leser was die Zahl bedeutet.

Statt erst eine leere Liste an das Ergebnis zu hängen und die dann im Ergebnis zu erweitern ist es leichter verständlich und auch leichter refaktorisierbar wenn man pro Ergebniszeile eine leere Liste erstellt, die füllt, und erst dann an das Ergebnis anhängt. Dann kommt man zudem auch ohne einen Indexzugriff aus.

Du wandelst die Zahen, die ja als Zeichenketten aus der Tabelle kommen, gar nicht in Zahlen um.

Code: Alles auswählen

#!/usr/bin/env python3
import bs4
import pandas as pd
import requests


URL = "https://de.numbeo.com/lebenshaltungskosten/rankings?title=2018"


def main():
    response = requests.get(URL)
    response.raise_for_status()

    soup = bs4.BeautifulSoup(response.text, "lxml")
    table = soup.find("table", id="t2")
    header = [cell.text.strip() for cell in table.thead.tr("th")]
    values = list()
    for rank, table_row in enumerate(table.tbody("tr"), 1):
        row_cells = table_row("td")

        if row_cells[0].text.strip() != "":
            raise ValueError(f"expected empty cell, got {row_cells[0]}")

        city = row_cells[1].text.strip()
        index_values = (
            float(cell.text.replace(",", ".")) for cell in row_cells[2:]
        )
        values.append([rank, city, *index_values])

    data = pd.DataFrame(values, columns=header)
    print(data.head())


if __name__ == "__main__":
    main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Antworten