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:
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.
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.