Seite 1 von 2

Scrapping einer website mit Beautiful soup

Verfasst: Sonntag 3. September 2023, 20:44
von m0$tw4nt3d
Hi und zwar bin ich gerade dabei einer Website mit Hilfe von bs4 Daten zu entnehmen, der betreffende html5 Code der Website ist folgender:

Code: Alles auswählen

<div class="table-container">
    
        <table class="table table-bordered table-condensed table-striped" id="track-table">
            
            
                <thead >
                <tr>
                
                    <th class="artist orderable">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=artist">Artist</a>
                        
                    </th>
                
                    <th class="orderable title">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=title">Title</a>
                        
                    </th>
                
                    <th class="mix orderable">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=mix">Mix</a>
                        
                    </th>
                
                    <th class="bpm orderable">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=bpm">BPM</a>
                        
                    </th>
                
                    <th class="genre orderable">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=genre">Genre</a>
                        
                    </th>
                
                    <th class="orderable rlabel">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=rlabel">Label</a>
                        
                    </th>
                
                    <th class="orderable year">
                        
                            <a href="?artist=&amp;title=&amp;bpm=&amp;genre=31&amp;sort=year">Year</a>
                        
                    </th>
                
                </tr>
                </thead>
            
            
            
                <tbody >
                     <tr class="odd">
                                
                                    <td class="artist"><a href="/music/by/aaliyah">Aaliyah</a></td>
                                
                                    <td class="title">One In A Million</td>
                                
                                    <td class="mix">—</td>
                                
                                    <td class="bpm">122</td>
                                
                                    <td class="genre">Pop</td>
                                
                                    <td class="rlabel">Atlantic</td>
                                
                                    <td class="year">2017</td>
                                
                            </tr>
                        
                    
                
                    
                        
                            <tr class="even">
                                
                                    <td class="artist"><a href="/music/by/aaron-carter">Aaron Carter</a></td>
                                
                                    <td class="title">Aaron&#x27;s Party (come Get It)</td>
                                
                                    <td class="mix">—</td>
                                
                                    <td class="bpm">117</td>
                                
                                    <td class="genre">Pop</td>
                                
                                    <td class="rlabel">Jive Records</td>
                                
                                    <td class="year">2000</td>
                                
                            </tr>
                        
                    
                ...
                    
                        
                            <tr class="odd">
                                
                                    <td class="artist"><a href="/music/by/aaron-carter">Aaron Carter</a></td>
                                
                                    <td class="title">Don&#x27;t Say Goodbye</td>
                                
                                    <td class="mix">PO PopRoXxX Intro Edit</td>
                                
                                    <td class="bpm">60</td>
                                
                                    <td class="genre">Pop</td>
                                
                                    <td class="rlabel">PO PopRoXxX Intro Edit</td>
                                
                                    <td class="year">2018</td>
                                
                            </tr>
                        
                    
                
                    
                        
                            <tr class="even">
                                
                                    <td class="artist"><a href="/music/by/aaron-carter">Aaron Carter</a></td>
                                
                                    <td class="title">Don&#x27;t Say Goodbye</td>
                                
                                    <td class="mix">PO PopRoXxX Quick Edit</td>
                                
                                    <td class="bpm">60</td>
                                
                                    <td class="genre">Pop</td>
                                
                                    <td class="rlabel">PO PopRoXxX Quick Edit</td>
                                
                                    <td class="year">2018</td>
                                
                            </tr>
                        
                    
                
                </tbody>
            
            
            
            
        </table>
Ich möchte eigentlich alle Daten für jedes even bzw odd raus ziehen um damit dann weiter arbeiten zu können, allerdings funktioniert irgendetwas in meinem Code nicht. Ich schätze ich verwende bs4 noch falsch, da ich ein wenig verwirrt bin wie genau ich in dem html Code navigieren soll...
Hier der entsprechende Python Code:

Code: Alles auswählen

def extractData(url, file_name):
    page = requests.get(url)
    soup = bs(page.content, "html.parser")

    odds = soup.find_All("tr", class_="odd")
    evens = soup.find_All("tr", class_="even")
    oddsANDevens = {}
    
    for even in evens:
        artist = even.find("td", class_="artist")
        title = even.find("td", class_="title")
        mix = even.find("td", class_="mix")
        bpm = even.find("td", class_="bpm")
        year = even.find("td", class_="year")

        oddsANDevens.update({title: {'artist': artist, 'mix': mix, 'bpm': bpm, 'year': year}})

    for odd in odds:
        artist = odd.find("td", class_="artist")
        title = odd.find("td", class_="title")
        mix = odd.find("td", class_="mix")
        bpm = odd.find("td", class_="bpm")
        year = odd.find("td", class_="year")

        oddsANDevens.update({title: {'artist': artist, 'mix': mix, 'bpm': bpm, 'year': year}})

    with open(file_name, "w") as file:
        json.dump(oddsANDevens, file)
        
Ich bekomme folgende Error Meldung:

Code: Alles auswählen

    odds = soup.find_All("tr", class_="odd")
TypeError: 'NoneType' object is not callable
Allerdings verstehe ich nicht ganz wo hier ein NoneType ist.

Meine Frage ist also, navigiere ich falsch mit bs4? Oder kann mir irgendjemand einen Tip geben, wie ich mit der Error Meldung umzugehen habe?
Vielen Dank für jede Hilfe zu bs4 Anwendung und Lösung meines Problems.

Ps: Hier die entsprechende url zur verwendeten Website: https://www.bpmdatabase.com/music/sear ... &genre=31

Re: Scrapping einer website mit Beautiful soup

Verfasst: Sonntag 3. September 2023, 21:20
von __blackjack__
`find_All` ist None, weil es das nicht gibt (und bs4 etwas zu ”magisch” ist und dafür keinen `AttributeError` liefert).

Letztlich willst Du aber gar nicht "even" und "odd" sondern *alle* <tr> im <tbody> von der <table> mit der ID "track-table". Dann muss man da auch nicht zweimal den gleichen Code schreiben.

Also (ungetestet) ``soup.find("table", id="track-table").find("tbody").find_all("tr")``.

Namen werden in Python klein_mit_unterstrichen geschrieben. Und bitte keine kryptischen Abkürzungen wie `bs`. Das hat ja eine Bedeutung. Die zu Abkürzungen passt. 😉

Edit: `page` ist kein guter Name für etwas das keine Seite ist sondern eine HTTP-Antwort.

Man sollte prüfen ob die Antwort kein HTTP-Fehler war.

`update()` ist die falsche Methode um *ein* Schlüssel/Wert-Paar zu einem Wörterbuch hinzuzufügen. Da nimmt man ``mappint[key] = value``.

Beim öffnen von Textdateien sollte man immer die Kodierung angeben. Im Fall von JSON ist das wirklich wichtig, weil die UTF-8 kodiert sind nach Spezifikation.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import json

import bs4
import requests


def extract_data(url, filename):
    response = requests.get(url, timeout=None)
    response.raise_for_status()

    keys = ["artist", "mix", "bpm", "year"]
    result = {
        row.find("td", "title"): {key: row.find("td", key) for key in keys}
        for row in (
            bs4.BeautifulSoup(response.content, "html.parser")
            .find("table", id="track-table")
            .find("tbody")
            .find_all("tr")
        )
    }
    with open(filename, "w", encoding="utf-8") as file:
        json.dump(result, file)

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 09:52
von m0$tw4nt3d
Vielen Dank! Jetzt funktioniert es zumindest soweit, dass ich die entsprechenden Infos bekomme, allerdings immer noch mit Tags drum rum. Gibt es da vielleicht eine Methode um die zu entfernen? Ich werde da jetzt aufjedenfall mal nach suchen.

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 09:58
von grubenfox
irgendwas mit "inner_html" oder so ähnlich?

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 10:07
von Sirius3
@grubenfox: statt zu raten, könnte man auch einfach in die Dokumentation schauen.

Code: Alles auswählen

row.find("td", key).string

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 10:46
von m0$tw4nt3d
@Sirius3

das funktioniert nicht, das habe ich schon ausprobiert, da bekkomme ich wieder nur

Code: Alles auswählen

'NoneType' object has no attrubute 'string'.
Allerdings kann ich mir ja aus geben lassen, was

Code: Alles auswählen

 row.find("td", key)
ergibt und das ist nicht None sondern wie folgt:

Code: Alles auswählen

{'mix': <td class="mix">—</td>, 'artist': <td class="artist"><a href="/music/by/13-crowns-ft-poo-bear">13 Crowns ft. Poo Bear</a></td>, 'bpm': <td class="bpm">95</td>, 'year': <td class="year">2018</td>}
Ich habe es auch schon mit get_text() versucht, allerdings erhalte ich jedes mal den selben Error.

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 11:04
von Sirius3
Du wirst wohl irgendwo eine Tabellenzeile haben, in der nicht alle Spalten definiert sind. Diese Fälle mußt Du halt explizit behandeln.

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 11:22
von m0$tw4nt3d
Ja soweit habe ich gedacht und es deswegen nur auf der ersten spalte getestet und da sind alle Tabellen zeilen ausgefüllt und es funktinoiert leider trotzdem nicht...

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 15:12
von x2wr0
Holá,
nur mal ein kleiner Wink auf die Schnelle: wenn du dir die Tabelle auf besagter Seite und den zugehörigen Quelltext mal genauer anschaust, gibt es da etwas ominöses zu entdecken..:

Code: Alles auswählen

 <tr class="odd" style="height:101px;">
   <td colspan="12">
      <div class="container-fluid m-auto" style="max-width: 1000px;">
         <div class="row">
            <input type="hidden" name="IL_IN_ARTICLE">
         </div>
      </div>
   </td>
</tr>
Dieser Schnippsel taucht da immer wieder mal im <tbody> auf. Vielleicht stolpert deine Schleife ja darüber (hat ja schließlich auch das class="odd" Attribut)?

saludos ~

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 16:39
von m0$tw4nt3d
@x2wr0

hmm, daran könnte es liegen. Ich schau mir das aufjedenfall mal an. Danke für den Hinweis!

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 19:53
von m0$tw4nt3d
Okay, also die "hidden" attribute sollten eigentlich kein Problem sein, da ich ja nur durch die tds loope, die in der Liste keys spezifiziert wurden. Das bedeutet zwar, dass ich mich um die getrennt kümmern müsste, aber den NoneType Fehler, beim ersten Element, welches wie folgt aussieht ergibt immer noch keinen Sinn:

Code: Alles auswählen

<td class="title">Grateful</td>: {'mix': <td class="mix">—</td>, 'artist': <td class="artist"><a href="/music/by/13-crowns-ft-poo-bear">13 Crowns ft. Poo Bear</a></td>, 'bpm': <td class="bpm">95</td>, 'year': <td class="year">2018</td>}
folgenden Error erhalte ich wenn ich

Code: Alles auswählen

row.find("td", key).string
bzw

Code: Alles auswählen

row.find("td", key).get_text()
eingebe:

Code: Alles auswählen

row.find("td", "title"): {key: row.find("td", key).get_text() for key in keys}
AttributeError: 'NoneType' object has no attribute 'get_text'

Re: Scrapping einer website mit Beautiful soup

Verfasst: Montag 4. September 2023, 20:04
von Sirius3
Die `row` hat kein <td>-Element mit der Klasse "title". Was gibt denn `print(row)` aus?

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 10:31
von m0$tw4nt3d
Also ich finde <td> mit der Klasse "title":
z.b. hier:

<td class="title">One In A Million</td>

Und row kann ja gar nicht None sein, sonst würde es mir die Elemente ja nicht mit Tags zurückgeben. Ich möchte ja nur die Tags entfernen also, dass das hier:

Code: Alles auswählen

<td class="title">Grateful</td>: {'mix': <td class="mix">—</td>, 'artist': <td class="artist"><a href="/music/by/13-crowns-ft-poo-bear">13 Crowns ft. Poo Bear</a></td>, 'bpm': <td class="bpm">95</td>, 'year': <td class="year">2018</td>}
Zu dem hier wird:

Code: Alles auswählen

"Grateful": {"mix": "---", "artist": "13-crowns-ft-poo-bear", "bpm": "95", "year": "2018"}

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 10:49
von Sirius3
Warum gibst Du nicht einfach mal `row` aus, dann siehst Du, was tatsächlich darin steht.

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 10:51
von m0$tw4nt3d
Das ist row:

Code: Alles auswählen

<td class="title">Grateful</td>: {'mix': <td class="mix">—</td>, 'artist': <td class="artist"><a href="/music/by/13-crowns-ft-poo-bear">13 Crowns ft. Poo Bear</a></td>, 'bpm': <td class="bpm">95</td>, 'year': <td class="year">2018</td>}

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 11:18
von Sirius3
Das ist das, was in result steht, und dort kommt kein None vor, also kann das nicht die Zeile sein, die den Fehler verursacht. Also mußt Du erst die Zeile finden, die den Fehler verursacht und dann die auch ausgeben.

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 11:37
von m0$tw4nt3d
Also ich habe jetzt eine Lösung gefunden in dem ich einfach später noch mal durch das dictonary iteriere mit Hilfe von parsel.
Falls jemand später einmal das selbe Problem hat, hier der Code der jetzt für mich funktioniert:

Code: Alles auswählen

def extractData(url, file_name):
    response = requests.get(url, timeout=None)
    response.raise_for_status()

    keys = ["mix", "artist", "bpm", "year"]
    result = {
        row.find("td", "title"): {key: row.find("td", key) for key in keys}
        for row in (
            BeautifulSoup(response.content, "html.parser")
            .find("table", id="track-table")
            .find("tbody")
            .find_all("tr")
        )
    }
    res_keys = list(result.keys())
    for i in range(len(result)):
        key = res_keys[i]
        selector = parsel.Selector(text=str(key))
        text_key = selector.css('td::text').get()
        for part in result[key]:
            selector_part = parsel.Selector(text=str(result[key][part]))
            if part == 'artist':
                text_part = selector_part.css('a::text').get()
            else:
                text_part = selector_part.css('td::text').get()
            result[key][part] = text_part


        result[text_key] = result[key]
        del result[key]

Vielen Dank an alle die mir mit ihren Tipps hier geholfen haben!

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 11:54
von __deets__
Woher hast du denn diese for-Schleife? Das ist ja wirklich von hinten durch die Brust ins Auge, einmal um den Mond rum, und dann durch die Tiefsee.

Ein simlpes

Code: Alles auswählen

for key, parts in result.items():
    ...
   for part in parts:
        ...
sollte vollkommen ausreichen. Und das del result[key] ist bestenfalls unoetig, schlimmstenfalls ein harter Bug, weil man *NIEMALS* die Datenstruktur, auf der man gerade rumiteriert, waehrenddessen veraendern darf. Du setzt da ja sogar noch extra result[key]]part], nur um das dann 3 Zeilen spaeter wegzuwerfen....

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 12:45
von Sirius3
Über einen Index zu iterieren macht man in Python nicht. Und man ändert auch kein Wörterbuch, sondern erzeugt bei Bedarf einfach ein neues.
Funktionsnamen schreibt man wie Variabelnnamen komplett klein:

Code: Alles auswählen

def extract_data(url, file_name):
    response = requests.get(url, timeout=None)
    response.raise_for_status()

    keys = ["mix", "artist", "bpm", "year"]
    intermediate_result = {
        row.find("td", "title"): {key: row.find("td", key) for key in keys}
        for row in (
            BeautifulSoup(response.content, "html.parser")
            .find("table", id="track-table")
            .find("tbody")
            .find_all("tr")
        )
    }
    result = {}
    for key, values in intermediate_result.items():
        selector = parsel.Selector(text=str(key))
        text_key = selector.css('td::text').get()
        texts = {}
        for part, value in values.items():
            selector_part = parsel.Selector(text=str(value))
            if part == 'artist':
                text_part = selector_part.css('a::text').get()
            else:
                text_part = selector_part.css('td::text').get()
            texts[part] = text_part
        result[text_key] = texts
    return result
Natürlich ist das völliger Aberwitz, einen weiteren Parser einzubinden, weil man mit dem ersten Parser nicht zurecht kommt:

Code: Alles auswählen

def extract_text(row, class_):
    element = row.find("td", class_)
    return element.text if element is not None else None

def extract_data(url, file_name):
    response = requests.get(url, timeout=None)
    response.raise_for_status()

    keys = ["mix", "artist", "bpm", "year"]
    return {
        extract_text(row, "title"): {key: extract_text(row, key) for key in keys}
        for row in (
            BeautifulSoup(response.content, "html.parser")
            .find("table", id="track-table")
            .find("tbody")
            .find_all("tr")
        )
    }

Re: Scrapping einer website mit Beautiful soup

Verfasst: Dienstag 5. September 2023, 13:17
von x2wr0
mit

Code: Alles auswählen

response = requests.get("<url>")
if response:
    soup = BeautifulSoup(response.text, 'html.parser')
    for row in soup.find('tbody').find_all('tr'):
        print(row.find_all('td'))
lassen sich doch schon mal die einzelnen "Zeilen" der Tabelle ausgeben; da siehst du dann auch, dass die Liste/"Zeile" mit dem ominösen Ding (s.o.) nur ein Element hat und sich somit solide herausfiltern ließe..
und mit

Code: Alles auswählen

        print([r.string for r in row.find_all('td')])
wirds noch deutlicher, woher das None kommen könnte.