Een website scrapen

Een website scrapen

13 februari 2024
Erwin Matijsen
Geplaatst in Tutorial


In het vorige bericht heb je geleerd hoe je met requests data kunt ophalen van een URL. In dat voorbeeld kwam de data in de vorm van een downloadbaar tekstbestand. In veel gevallen zal je ook API's aan kunnen roepen, die bijvoorbeeld json teruggeven.

Maar wat als je wilt werken met gegevens van een website zelf? In deze tutorial leer je werken met Beautiful Soup, een package om gegevens uit HTML en XML te halen.

Het project

In dit project download je weer gegevens van het KNMI, maar dit keer direct van hun website zelf. Als je naar KNMI gaat om het weerbericht te lezen, moet je eerst op "Bekijk weersverwachtingen" klikken. Vervolgens klik je op "Bekijk het volledige weerbericht" en tot slot klik je op "Lees het hele weerbericht".

Met requests en beautifulsoup sla je deze stappen over en download en verwerk je de uiteindelijke pagina direct, zodat je snel het weerbericht kunt lezen.

Voordat je begint

Maak een nieuw project aan, met een nieuwe virtual environment. Installeer daarin de benodigde packages:

shell
cd projects
mkdir scrape_knmi && cd scrape_knmi
python -m venv .venv
source .venv/bin/activate  # .venv\Scripts\activate.bat voor Windows
python -m pip install requests beautifulsoup4
python -m pip freeze > requirements.txt

De pagina downloaden

De eerste stap is het downloaden van de pagina. Maak in main.py een eenvoudige functie aan:

main.py
import requests

URL = "https://www.knmi.nl/nederland-nu/weer/verwachtingen"

def get_html(refresh=True):
    if not refresh:
        try:
            with open("knmi.html", encoding="utf-8") as f:
                text = f.read()
        except FileNotFoundError:
            print("Bestand niet gevonden, downloaden")

            # Roep deze functie aan, met `refresh=True`
            return get_html(refresh=True)
    else:
        response = requests.get(URL)

        # Als de HTTP-code 400 of hoger is, zal er een uitzondering worden opgeworpen
        response.raise_for_status()
        text = response.text

        # Sla bestand op
        with open("knmi.html", "w", encoding="utf-8") as f:
            f.write(text)

    return text

De functie zorgt ervoor dat je het bestand opslaat als knmi.html, zodat je tijdens het ontwikkelen niet steeds de URL hoeft aan te roepen. Wil je de gegevens verversen, roep dan get_html aan met refresh=True.

De soep maken

Nu je de HTML hebt, wil je ermee kunnen werken. Hier komt beautifulsoup om de hoek kijken. Met beautifulsoup 'ontleed' je de HTML en maak je het navigeerbaar en doorzoekbaar, zodat je de elementen eruit kunt halen die je nodig hebt.

main.py
def get_text(html):
    soup = BeautifulSoup(html, "html.parser")
    print(soup.prettify())

Met de eerste regel zal beautifulsoup de HTML ontleden en bruikbaar maken. Hiervoor gebruik je in dit geval de standaard meegeleverde html.parser. Met print(soup.prettify()) print je het HTML-document op een (redelijk) leesbare manier.

In deze soep ga je op zoek naar de juiste informatie!

Speuren

beautifulsoup maakt het eenvoudig gegevens op te zoeken in een groot HTML-document, maar dan moet je wel weten waar je naar op zoek bent. In het opgeslagen HTML-bestand kun je zelf zoeken naar de juiste elementen. Zoek bijvoorbeeld op "Vandaag & morgen", de kop van het bericht:

Het weerbericht met de kop

Je zult al snel zien dat hele weerbericht, inclusief de tekst die onder de 'uitklap' zit, in een div met de klasse weather__text media__body aanwezig is.

De div met het weerbericht

DIV?

Werken met HTML is werken met verschillende elementen, zoals containers (div), titels (h1, h2) en paragrafen (p). Deze elementen kunnen ook een klasse hebben, en een id. Een goed startpunt om te leren over hoe HTML werkt is MDN.

Nu je weet waar de voor jou interessante gegevens zich bevinden, kun je die uit de HTML halen. Vanuit daar kun je dan verder werken. Pas get_text aan:

main.py
def get_text(html):
    soup = BeautifulSoup(html, "html.parser")

    # Zoek de relevante tekst
    weerbericht = soup.find("div", class_="weather__text media__body")
    print(weerbericht.prettify())

    return weerbericht

Met find zoek je in de soep naar de eerste div met de klasse weather__text media__body. De parameter heet class_ (met een streepje erachter) om conflicten met het ingebouwde woord class te voorkomen.

Je kunt de tekst nu iets beter bestuderen. Wat opvalt is dat het begin eenvoudig is. De titel staat in een h2 en de samenvatting heeft een eigen paragraaf (p) met klasse intro. Daarna wordt het ingewikkelder. Alles van 'Waarschuwingen' tot 'Komende nacht' staat in één paragraaf (p).

Vervolgen staat het weerbericht voor komende nacht ook weer in een eigen paragraaf. Daarna volgt het weerbericht van morgen ook weer met een eigen paragraaf. Aan jou de taak om deze structuur te doorgronden en een parser te schrijven die precies de juiste teksten uit de soep haalt.

Ontleden

beautifulsoup maakt het navigeren en zoeken in HTML eenvoudig. Met het vorige stukje code heb je de variabele weerbericht aangemaakt. Dit bevat nu alle relevante HTML die je kunt gaan ontleden.

Er zijn verschillende manieren om door de tekst te navigeren. De eerste manier - zoeken - heb je al gebruikt bij het vinden van de relevante delen in het geheel. Je kunt zoeken ook gebruiken om de titel en de samenvatting op te halen, omdat deze vrij duidelijk afgebakend zijn.

main.py
def ontleed(weerbericht):

    # Sla hier de resultaten in op
    result = {}

    # De titel
    titel = weerbericht.find("h2")

    # De samenvatting
    samenvatting = weerbericht.find("p", class_="intro")

    result["titel"] = titel.text
    result["samenvatting"] = samenvatting.text

    print(result)

Met .find("h2") zoek je de titel op. Met .find("p") zoek je de eerste paragraaf die je tegenkomt. Alhoewel er meerdere paragrafen (p) zijn, haal je met find altijd alleen het eerste resultaat op dat beautifulsoup tegenkomt. Met class_="intro" maak je de zoekopdracht nog specifieker. In beide gevallen wil je de tekst hebben en niet de tags. Je haalt de tekst in de tags op met .text.

Het weerbericht voor de huidige dag bevindt zich in een eigen paragraaf. Om dit op te halen zoek je weer naar paragrafen, maar in plaats van find gebruik je find_all. Dit zal namelijk álle elementen teruggeven die aan het zoekcriteria voldoen, en niet alleen de eerste. Je weet dat je de tweede paragraaf nodig hebt, dus je code wordt:

main.py
def ontleed(weerbericht):
    # ...

    # Haal nu eerst de volgende paragraaf op
    # Zoek alle paragrafen in `weerbericht`, en sla de tweede op
    p_vandaag = weerbericht.find_all("p")[1]  

De volgende stap is het zoeken van de waarschuwingen. Als je in de HTML kijkt, zie je ongeveer:

  <strong>
   <a href="https://www.knmi.nl/nederland-nu/weer/waarschuwingen">Waarschuwingen</a>
  </strong>
  <br/>
  Er zijn geen waarschuwingen.

Je ziet dat de kop van de waarschuwing een link is (a), dit is eenvoudig zoeken. Als je de link vindt, kun je vervolgens find_next gebruiken om de eerste br te vinden. Je gebruikt find_next om ná het huidige element te zoeken. Met find zou je ín het huidige element zoeken.

Het element ná de br is de tekst die je nodig hebt. Met beautifulsoup gebruik je find, find_next en next_element om te zoeken en te navigeren naar wat je nodig hebt:

main.py
def ontleed(weerbericht):
    # ...

    # Haal de waarschuwing op. Laat de link achterwege
    # Vind eerst de link. Zoek dan de `br` en haal tot slot het volgende element op
    waarschuwing = p_vandaag.find("a").find_next("br").next_element.text
    result["waarschuwingen"] = waarschuwing

In de huidige paragraaf blijven nu de berichten per dagdeel over. De algemene structuur is:

<strong>Dagdeel</strong>
rest van de tekst

Je kunt daarom vanaf waarschuwing op zoek naar de eerste drie strong-tags in de HTML en vanuit daar de titel en de tekst ophalen:

main.py
def ontleed(weerbericht):
    # ...

    # Haal de dagdelen van de huidige dag op
    for dagdeel in waarschuwing.find_all_next("strong", limit=3):
        periode = dagdeel.text

        # De eerste `next_element` is de tekst binnen de `strong`-tags (titel)
        # De tweede `next_element` is de tekst die je nodig hebt. Haal de
        # non-breaking space (\xa0) weg.
        tekst = dagdeel.next_element.next_element.text.strip("\xa0")

        result[periode] = f"{periode}{tekst}"

Waarschijnlijk begin je door te krijgen hoe je beautifulsoup gebruikt om de juiste elementen te vinden door een combinatie van zoeken en navigeren. Het is soms even speuren naar wat je nodig hebt en in welk element het verstopt is. Klap de code hieronder uit om de complete ontleed-functie te bekijken. Probeer eerst zelf de rest te schrijven!

main.py
def ontleed(weerbericht):

    # Sla hier de resultaten in op
    result = {}

    # De titel
    titel = weerbericht.find("h2")

    # De samenvatting
    samenvatting = weerbericht.find("p", class_="intro")

    result["titel"] = titel.text
    result["samenvatting"] = samenvatting.text

    # Haal nu eerst de volgende paragraaf op
    # Zoek alle paragrafen in `weerbericht`, en sla de tweede op
    p_vandaag = weerbericht.find_all("p")[1]

    # Haal de waarschuwing op. Laat de link achterwege
    waarschuwing = p_vandaag.find("a").find_next("br").next_element
    result["waarschuwingen"] = waarschuwing.text

    # Haal de dagdelen van de huidige dag op
    for dagdeel in waarschuwing.find_all_next("strong", limit=3):
        periode = dagdeel.text

        # De eerste `next_element` is de tekst binnen de `strong`-tags (titel)
        # De tweede `next_element` is de tekst die je nodig hebt. Haal de
        # non-breaking space (\xa0) weg.
        tekst = dagdeel.next_element.next_element.text.strip("\xa0")

        result[periode] = f"{periode}{tekst}"

    # Haal de tweede paragraaf op
    p_komende_nacht = weerbericht.find_all("p")[2]

    # Haal de titel en de tekst voor komende nacht op
    komende_nacht_titel = p_komende_nacht.find("strong")
    komende_nacht_text = komende_nacht_titel.next_element.next_element.text.strip("\xa0")
    result[komende_nacht_titel.text] = f"{komende_nacht_titel.text} {komende_nacht_text}"

    # Haal de dagdelen voor morgen op
    # De structuur is hetzelfde als de huidige dag
    morgen = p_komende_nacht.next_element
    for dagdeel in morgen.find_all_next("strong", limit=3):
        periode = dagdeel.text
        tekst = dagdeel.next_element.next_element.text.strip("\xa0")

        result[periode] = f"{periode} {tekst}"

    return result

Met result kun je nu doen wat je wilt, maar dat is voor een volgende keer!

Conclusie

Met beautifulsoup kun je eenvoudig HTML-pagina's doorzoeken en navigeren, om daar zo de elementen uit te halen die je nodig hebt. Dit is handig als er geen API beschikbaar is, bijvoorbeeld. Let wel op: het structureel scrapen van een website is grijs gebied. Kijk dus altijd of het binnen redelijke grenzen blijft en past binnen de voorwaarden van de website waar je mee werkt.

Over de auteur


Erwin Matijsen

Erwin Matijsen

Erwin is de oprichter van python-cursus.nl. In allerlei rollen heeft hij Python ingezet, van het eenvoudiger maken van zijn werk tot het opleveren van complete (web)applicaties. Met vrouw en kinderen woont hij in Havelte (Drenthe), midden in de prachtige natuur. Daar wandelt hij graag, zeker ook omdat de beste ingevingen tijdens een wandeling - weg van de computer - lijken te komen.


Contact

Vragen, opmerkingen?

Heb je vragen, opmerkingen, suggesties of tips naar aanleiding van deze blog? Neem dan contact met ons op, of laat het weten via Mastodon of LinkedIN.