Ga naar hoofdinhoud

Reddit scrapen met Python en BeautifulSoup 4

In deze tutorial leer je hoe je webpagina's ophaalt met requests, pagina's in de browser analyseert en informatie uit ruwe HTML extraheert met BeautifulSoup.
Bijgewerkt 2 okt 2026  · 13 min lezen

Verken met AI

ChatGPTClaudePerplexity

Illustratie van Reddit scrapen

Je vindt een uitgewerkt voorbeeld van het script dat we gaan schrijven hier.

Wat is webscraping?

Goed, wat is webscraping nu precies? Zoals de naam al aangeeft, is het een methode om data van webpagina's te 'scrapen' of te extraheren. Alles wat je met je browser op internet kunt zien, inclusief deze tutorial, kun je naar je lokale harde schijf scrapen.

Webscraping kent veel toepassingen. Voor elke data-analyse is de eerste stap het verkrijgen van data. Het internet is een enorme opslagplaats van de geschiedenis en kennis van de mensheid, en jij hebt de middelen om eruit te halen wat je wilt en met die informatie te doen wat je wilt.

In onze tutorial gebruiken we Python en het BeautifulSoup 4-pakket om informatie uit een subreddit te halen. We zijn geïnteresseerd in de datascience-subreddit. We willen de eerste 1000 posts van de subreddit ophalen en exporteren naar een CSV-bestand. We willen weten wie het geplaatst heeft, en hoeveel likes en reacties het heeft.

Wat we in de tutorial behandelen:

  • Webpagina's ophalen met requests
  • Webpagina's in de browser analyseren op informatie
  • Informatie uit ruwe HTML extraheren met BeautifulSoup

Opmerking: We gebruiken de oudere versie van Reddit's website omdat die lichter is om te laden en daardoor minder belastend is voor je machine.

Vereisten

Deze tutorial gaat ervan uit dat je het volgende weet:

  • Pythonscripts uitvoeren op je computer
  • Basiskennis van de HTML-structuur

De bovenstaande skills kun je leren in DataCamps beginnerscursus Python. Dat gezegd hebbende: de gebruikte concepten zijn minimaal en je komt met heel weinig Python-kennis al een heel eind.

Nu dat geregeld is, kunnen we door naar het eerste deel van het bouwen van onze webscraper. Eigenlijk, het eerste deel van elk Pythonscript: imports.

In onze scraper gebruiken we de volgende packages:

  • requests
  • beautifulsoup4

Je kunt deze packages natuurlijk met pip installeren, zo:

pip install package_name

Als je klaar bent met downloaden, importeer ze dan in je code.

import requests
import csv
import time
from bs4 import BeautifulSoup4

We gebruiken Pythons ingebouwde csv-module om onze resultaten naar een CSV-bestand te schrijven. Misschien viel je iets geks op in de snippet hierboven. We hebben namelijk een package gedownload met de naam beautifulsoup4, maar importeren uit een module genaamd bs4. Dit is legaal in Python, en hoewel er vaak de wenkbrauwen bij worden opgetrokken, is het niet bepaald verboden.

Belangrijke opmerking over platformbeleid

Controleer vóór je de scrapingtechnieken gebruikt die ik in de volgende stappen laat zien altijd de actuele servicevoorwaarden van Reddit of welke site je ook wilt scrapen. Websitepolicy kan in de loop van de tijd veranderen. De Reddit User Agreement van september 2024 verbiedt scrapen zonder voorafgaande schriftelijke toestemming (zie sectie 7). Zorg er altijd voor dat je voldoet aan de meest recente gebruiksrichtlijnen van de site voordat je verdergaat.

Eerste stappen om Reddit te crawlen

Onze omgeving is opgezet en klaar. Vervolgens hebben we de url nodig van de pagina die we willen scrapen. Voor deze tutorial gebruiken we Reddit's 'datascience'-subreddit. Voordat we het script gaan schrijven, is er wat veldwerk nodig. Open een webbrowser en ga naar de betreffende subreddit.

Let op: we gebruiken voor onze scraper de oudere versie van de subreddit. De nieuwere versie verbergt cruciale informatie in de krochten van de pagina. Het is ook mogelijk om die informatie uit de nieuwe site te halen, maar voor de eenvoud gebruiken we de oudere versie die alles duidelijk toont.

Als je de link opent, word je overspoeld met informatie. Wat hebben we hier precies nodig? Als je alle links naloopt, zul je zien dat Redditposts van twee typen zijn:

  • Inbound
  • Outbound

Inboundlinks wijzen naar content op Reddit zelf, en outbound is precies het tegenovergestelde. Dit is belangrijk omdat we alleen de inboundlinks willen: posts met tekst van gebruikers, niet alleen links naar andere websites.

We weten dus wat we willen. Hoe halen we het eruit? Als je naar de titel van de posts kijkt, zie je dat die gevolgd wordt door tekst tussen haakjes. De posts die we willen hebben de toevoeging ‘(self.datascience)’. Logisch gezien kunnen we aannemen dat ‘self’ verwijst naar de root van Reddit, en ‘.datascience’ naar de subreddit.

Mooi, we hebben een manier om te bepalen welke posts inbound en welke outbound zijn. Nu moeten we dat in de DOM-structuur terugvinden. Kunnen we deze kenmerken in de DOM vinden door te zoeken op tags, classes of ids? Natuurlijk!

Klik in je browser met de rechtermuisknop op een ‘self.datascience’-link en klik op ‘element inspecteren’. De meeste moderne browsers hebben een handige tool waarmee ontwikkelaars de relevante delen van de broncode van de pagina dynamisch kunnen doorlopen. Gebruik je Safari, zorg dan dat de ontwikkelaarstools zijn ingeschakeld in de voorkeuren. Je krijgt een paneel te zien dat naar het deel van de broncode is gescrold dat verantwoordelijk is voor die ‘(self.datascience)’-markering.

In het paneel zie je dat de markering eigenlijk wordt geladen door een anchor-tag.

<a href="/r/datascience">self.datascience</a>

Maar dit zit binnen een span-tag.

<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>

Deze span-tag bevat de tekst die we op de pagina zien, dus ‘(self.datascience)’. Zoals je ziet is hij gemarkeerd met de class ‘domain’. Je kunt alle andere links checken om te zien of ze hetzelfde format volgen, en jawel; dat doen ze.

De pagina ophalen met BeautifulSoup

We weten wat we op de pagina willen hebben, prima, maar hoe gebruiken we Python om de inhoud van de pagina te lezen? Dat werkt ongeveer hetzelfde als hoe een mens de inhoud in de browser zou lezen.

Eerst moeten we de webpagina opvragen met de ‘requests’-library.

url = "https://old.reddit.com/r/datascience"
# Headers to mimic a browser visit
headers = {'User-Agent': 'Mozilla/5.0'}

# Returns a requests.models.Response object
page = requests.get(url, headers=headers)

We hebben nu een Response-object dat de ruwe tekst van de webpagina bevat. Daar kunnen we nog niets mee. We hebben slechts een enorme string die de volledige broncode van het HTML-bestand bevat. Om hier chocola van te maken, gebruiken we BeautifulSoup 4.

De headers laten ons een browserbezoek nabootsen. Omdat een response naar een bot verschilt van die naar een browser, en onze referentie de browser is, is het beter de response van de browser te krijgen.

Met BeautifulSoup kunnen we specifieke tags vinden door te zoeken op combinaties van classes, ids of tagnamen. Dit gebeurt door een syntaxisboom te maken, maar de details daarvan zijn niet relevant voor ons doel (en vallen buiten het bestek van deze tutorial).

Laten we die syntaxisboom maken.

soup = BeautifulSoup(page.text, 'html.parser')

De soup is gewoon een BeautifulSoup-object dat wordt gemaakt op basis van een string met ruwe broncode. Let erop dat we de html-parser moeten opgeven. BeautifulSoup kan namelijk ook soup maken van XML.

Onze tags vinden

We weten welke tags we willen (de span-tags met class ‘domain’), en we hebben de soup. Nu gaan we de soup doorlopen en alle instanties van deze tags vinden. Je zult lachen hoe simpel dit is met BeautifulSoup.

domains = soup.find_all("span", class_="domain")

Wat deden we net? We riepen de methode ‘find_all’ aan op het soup-object, die in zichzelf zoekt naar alle anchor-tags met de parameters die als tweede argument worden doorgegeven. We hebben maar één voorwaarde meegegeven (class moet ‘domain’ zijn), maar we kunnen dit combineren met veel andere zaken, of zelfs alleen een id gebruiken.

We gebruiken ‘class_=‘ omdat 'class' een gereserveerd keyword is in Python voor het definiëren van classes en dergelijke

Als je meer dan één parameter wilt meegeven, maak je het tweede argument simpelweg een dictionary met de gewenste argumenten, bijvoorbeeld:

soup.find_all("span", {"class": "domain", "height", "100px"})

We hebben alle span-tags in onze ‘domains’-lijst, maar we willen specifiek de ‘(self.datascience)’-domains.

for domain in domains:
    if domain != "(self.datascience)":
        continue

    print(domain.text)

Goed, nu zou je een lijst moeten zien met de typen posts op de pagina, exclusief die niet ‘(self.datascience)’ zijn. We hebben in feite alles wat we willen. We hebben nu referenties naar alle inbound posts.

Onze informatie vinden

Leuk dat we een paar regels ‘(self.datascience)’ printen, maar wat nu? Denk terug aan ons initiële doel. Haal de posttitel, auteur, likes en het aantal reacties op.

Daarvoor moeten we terug naar de browser. Als je de inspector bij onze markering weer opent, zie je dat onze span-tag is genest binnen een div-tag… Die op zijn beurt weer in een andere div-tag is genest, enzovoort. Blijf omhoog gaan en je komt bij de parent-div voor de hele post.

<div class=" thing id-t3_8qccuv even  link self" ...>
    ...
    <div class="entry unvoted">
        <div class="top-matter">
            <p class="title">
                ...
                <span class="domain">
                ...
            </p>
        </div>
    </div>
</div>

Zoals je ziet is de gemeenschappelijke parent 4 niveaus hoger in de DOM-structuur. De ellipsen (…) staan voor onnodige ruis. Wat we nu nodig hebben, is een referentie naar de post-div voor elke post in onze domains-lijst. We kunnen de parent van elk element in onze soup vinden met de methoden van BeautifulSoup.

for domain in soup.find_all("span", class_="domain"):
    if domain != "(self.datascience)":
        continue

    parent_div = domain.parent.parent.parent.parent
    print(parent_div.text)

Als je het script draait, wordt alle tekst van alle inbound posts geprint. Je denkt misschien dat dit slechte code is. Je hebt gelijk. Het is nooit veilig om uitsluitend te vertrouwen op de structurele integriteit van de DOM. Het is een soort hack-en-slashoplossing die constant onderhoud vergt en de code-equivalent is van een tikkende tijdbom.

Laten we in plaats daarvan naar de parent-div van elke post kijken en zien of we inbound en outbound links al vanuit de parent kunnen scheiden. Elke parent-div heeft een attribuut ‘data-domain’, waarvan de waarde precies is wat we willen! Alle inbound posts hebben het data-domain ingesteld op ‘self.datascience’.

Zoals we eerder noemden, kunnen we met BeautifulSoup zoeken naar tags met een combinatie van attributen. Gelukkig heeft Reddit de parent-div van elke post de class ‘thing’ gegeven.

attrs = {'class': 'thing', 'data-domain': 'self.datascience'}

for post in soup.find_all('div', attrs=attrs):
    print(post.attrs['data-domain'])

De variabele ‘attrs’ is een dictionary die bevat waar we op willen zoeken: attributen en hun waarden. Dit is een veel veiligere manier om de posts te vinden omdat er minder bewegende delen zijn. Omdat we ‘self.datascience’ al als argument toevoegen bij het zoeken, hoeven we geen if-statement te gebruiken om iteraties over te slaan: we krijgen gegarandeerd alleen posts met het data-domain-attribuut ‘self.datascience’.

Nu we alle informatie hebben die we willen, rest ons alleen nog de informatie uit de children te halen. Dat is trouwens net zo simpel als je denkt.

Onze informatie uit Reddit halen

Per post hebben we 4 stukjes informatie nodig.

  • Titel
  • Auteur
  • Likes
  • Reacties

Als we naar de structuur van de post-div kijken, kunnen we al deze informatie terugvinden.

De titel

Dit is de eenvoudigste. In elke post-div staat een p-tag, genest onder een paar lagen divs. Die is makkelijk te vinden omdat hij de class ‘title’ heeft.

title = post.find('p', class_="title").text

Het post-object zit in onze for-loop van eerder. Het is ook een BeautifulSoup-object, maar bevat alleen de DOM-structuur binnen de post-div. De methode ‘find’ retourneert één object, in tegenstelling tot 'find_all', dat een lijst met objecten retourneert die aan de criteria voldoen. Nadat we de tag hebben gevonden, willen we alleen de string met de titel, dus gebruiken we de text-property om die te lezen. We kunnen ook andere attributen lezen met ‘object.attrs[‘attribute’]’.

De auteur

Deze is ook relatief simpel: open de inspector bij een auteursnaam onder de posttitel. Je ziet dat de auteur in een anchor-tag staat met de class ‘author’.

author = post.find('a', class_='author').text

De reacties

Deze vergt wat extra werk, maar blijft eenvoudig. We kunnen de reacties vinden op dezelfde manier als de titel en auteur.

comments = post.find('a', class_='comments').text

Als je dit draait, krijg je zoiets als “49 comments”. Dat is oké, maar het is mooier als we alleen het getal krijgen. Daarvoor hebben we wat extra Python nodig.

Als we Pythons ‘str.split()’-functie gebruiken, retourneert die een array van alle elementen in de string, gescheiden door spaties. In ons geval krijgen we de lijst ‘[“49", “comments”]’. Mooi! Nu hoeven we alleen nog het eerste element te pakken en op te slaan. We plakken de functies gewoon aan onze regel vast.

comments = post.find('a', class_='comments').text.split()[0]

Maar we zijn er nog niet, want soms krijgen we geen getal. We krijgen ‘comment’. Dat gebeurt als er geen reacties op een post zijn. Omdat we dit weten, hoeven we alleen te checken of we ‘comments’ als resultaat krijgen en dit vervangen door ‘0’.

if comments == "comment":
    comments = 0

De likes

Het aantal likes vinden is een peulenschil en volgt dezelfde logica als hierboven.

likes = post.find("div", attrs={"class": "score likes"}).text

Hier merk je misschien dat we een combinatie van twee classes gebruiken. Dat komt simpelweg omdat er meerdere andere divs zijn met de class ‘score’ of ‘likes’, maar slechts één met de combinatie ‘score’ en ‘likes’.

Als het aantal likes 0 is, krijgen we 0. Maar er gebeurt iets vreemds als de post zo nieuw is dat er nog geen likes zijn. We krijgen “•”. Laten we dit vervangen door “None” zodat het geen verwarring veroorzaakt in onze eindresultaten.

if likes == "•":
    likes = "None"

Onze resultaten naar CSV schrijven

Tot nu toe hebben we een loop die per post op de pagina de titel, auteur, likes en reacties extraheert. Python heeft een prima ingebouwde module voor het lezen en schrijven van CSV-bestanden, ‘csv’ — helemaal in de Python-geest: houd het simpel. Het enige wat we hoeven te doen is aan het einde van het loopblok een regel toevoegen die de details van de post aan een CSV-bestand toevoegt.

counter = 1
for post in posts:
    ...
    post_line = [counter, title, author, likes, comments]
    with open('output.csv', 'a') as f:
        writer = csv.writer(f)
        writer.writerow(post_line)

    counter += 1

Vrij rechttoe rechtaan, toch? De post_line wordt gebruikt om een array te maken met de elementen die door komma's gescheiden moeten worden. De variabele counter houdt bij hoeveel posts we hebben vastgelegd.

Naar de volgende pagina gaan

Omdat we tellen hoeveel posts we opslaan, hoeven we alleen onze volledige logica in een andere loop te plaatsen die nieuwe pagina's opvraagt totdat een bepaald aantal posts is vastgelegd.

counter = 1

while (counter <= 100):
    for post in posts:
        # Getting the title, author, ...
        # Writing to CSV and incrementing counter...

    next_button = soup.find("span", class_="next-button")
    next_page_link = next_button.find("a").attrs['href']
    time.sleep(2)
    page = requests.get(next_page_link, headers=headers)
    soup = BeautifulSoup(page.text, 'html.parser')

We deden net een aantal dingen, hè? Laten we het regel voor regel bekijken. Allereerst hebben we de counter-variabele een blok omhoog verplaatst, zodat de while-loop hem kan gebruiken.

We weten wat de for-loop doet, maar de andere regels dan. De variabele ‘next_button' wordt gebruikt om de 'volgende'-knop te vinden en op te slaan. Daarna vinden we de anchor-tag daarin en halen we het 'href'-attribuut op; dat slaan we op in ‘next_page_link’.

We kunnen deze link gebruiken om de volgende pagina op te vragen en opnieuw op te slaan in ‘page’ en een nieuwe soup te maken met BeautifulSoup.

De snippet hierboven stopt na 100, maar je kunt hem laten stoppen na elk gewenst aantal posts.

Scrapen met verantwoordelijkheid

De regel waar we het hierboven niet over hadden is ‘time.sleep(2)’. Die verdient namelijk een eigen sectie. Elke webserver heeft een eindige hoeveelheid resources, dus het is aan ons om ervoor te zorgen dat we die niet allemaal opsouperen. Het levert je niet alleen een ban op als je in een paar seconden honderden pagina's opvraagt, het is ook gewoon niet netjes.

Onthoud dat het eigenlijk best coulant is van een website dat je überhaupt mag scrapen, want als ze willen, kunnen ze bots al na 10 tot 20 requests detecteren, of je zelfs herkennen op basis van het requestobject dat Python verstuurt. Het is aardig van ze dat je kunt scrapen zonder je IP te hoeven roteren, dus doe hen een plezier en vertraag je bot.

Je vindt het crawlbeleid van een website in hun robots.txt-bestand. Dit staat meestal in de root van de site (bijv. http://www.reddit.com/robots.txt).

Wat nu?

Wat je maar wilt, eigenlijk. Zoals we net hebben laten zien, kun je alles wat je op het web ziet scrapen en lokaal opslaan. Je kunt de informatie die we zojuist hebben verkregen voor talloze doeleinden gebruiken. Met alleen al de vier stukjes informatie die we vonden, kunnen we veel conclusies trekken.

Door onze informatie verder te analyseren kunnen we achterhalen welke soorten Redditposts de meeste likes krijgen, welke woorden ze bevatten, door wie ze zijn geplaatst. Of omgekeerd: we kunnen een controversemeter maken door de verhouding likes/reacties te analyseren.

Denk aan een post die veel reacties krijgt maar geen likes. Dat betekent waarschijnlijk dat de post iets aansnijdt dat mensen raakt, maar niet per se iets dat ze leuk vinden.

Er zijn veel mogelijkheden, en het is aan jou hoe je de informatie gebruikt.

Vond je deze tutorial leuk? Bekijk dan zeker onze gids over Amazon scrapen met Python, plus onze verzameling Pythoncursussen.

Onderwerpen
Python

Leer meer over Python

Cursus

Introductie tot Python

4 uur
7.1M
Leer de basis van data-analyse met Python in 4 uur. Deze online cursus laat je kennismaken met de Python-interface en populaire pakketten.
Bekijk detailsRight Arrow
Start Cursus
Meer bekijkenRight Arrow
Gerelateerd

blog

AI vanaf nul leren in 2026: een complete gids van de experts

Ontdek alles wat je moet weten om in 2026 AI te leren, van tips om te beginnen tot handige resources en inzichten van industrie-experts.
Adel Nehme's photo

Adel Nehme

15 min

Meer BekijkenMeer Bekijken