Bouw je eigen Cultuurscraper

800px-John_Linnell_-_The_Harvest_Cradle

Afbeelding: The Harvest Cradle door John Linnell uit de collectie van de York Art Gallery (Wikimedia Commons).

Om de gloednieuwe Cultuur Data API te vullen met content moeten we een aantal scrapers schrijven die relevante data op gaat halen bij de participerende culturele instellingen. Dit zullen we gaan doen tijdens de Open Cultuur Data Harvests, die de komende maand plaats gaan vinden. Om jullie snel op gang te helpen, laat ik zien hoe de scrapers in elkaar zitten, en hoe je er zelf een kan maken.

ETL

De scraper is gebouwd op het ETL pattern. Dat staat voor Extract-Transform-Load, wat een soort van drietrapsraket is om data van externe bronnen in te laden voor je applicatie. Precies wat we nodig hebben dus!

De eerste stap in het ETL proces is het extraheren van de data. Dit is het gedeelte van het pattern waar API calls gedaan worden om informatie van externe bronnen te verkijgen.

De tweede stap is het transformeren van de data. Dit is een vertaalslag van de externe data naar je interne informatie structuur. In deze fase kan je bijvoorbeeld het externe veld ‘creator’ omzetten naar het interne veld ‘schilder’.

In de laatste stap wordt de getransformeerde data opgeslagen in de datastore die je gebruikt. In een hoop gevallen is dit een insert statement als je een database gebruikt, maar je kan hier ook checken of je bestaande records moet updaten. En misschien wil je wel een versie controle schema hanteren.

De Open Cultuur Data API

In de Open Cultuur Data API is de belangrijkste entiteit een ‘item’. Het beschrijft een object van een culturele instelling. De API zal Elasticsearch gebruiken om deze items op te slaan. Een van de ingewikkelde dingen bij het opslaan van objecten van meerdere culturele instellingen is dat sommige instellingen een uitgebreidere beschrijving hebben van het object dan andere. Om op een adequate manier met dit probleem om te gaan hebben we besloten om een gecombineerde (Elasticsearch) index te vullen, maar ook per culturele instelling een aparte index aan te maken. Op deze manier kan je in een keer meerdere instellingen queryen, maar toch ook gebruik maken van rijke metadata, mocht dat nodig zijn.

In de API hebben we voor elk van de stappen van het ETL proces klasses gedefinieerd, die het desbetreffende onderdeel van dit pattern kunnen uitvoeren. Om het wat overzichtelijker te houden hebben we de transform stap gesplitst in een Transformer en een Item object. De Transformer zorgt ervoor dat de externe data geparsed wordt (bijvoorbeeld JSON parsen, of XML), terwijl het item object zorgt voor de transformatie op item nivo. Er is ook een uitgebreide technische documentatie voor de API.

Zo, nu kunnen we aan de slag!

Stap 1: Definieer je bron

In de codebase staat een bestand ‘sources.json’ (in de ocd_backend directory) — hier staan de verschillende bronnen in, die de scraper kan harvesten. Om een nieuwe bron toe te voegen zul je dit bestand moeten gaan bewerken. Het bestand is in JSON formaat en bestaat uit een array van objecten, waar elk object een bron is. Bronnen dienen unieke namen te hebben en in de configuratie staat onder meer welke Extractor gebruikt wordt, welke Transformer, welk Item object er aangemaakt moet worden, en welke loader er gebruikt wordt (Dit zijn referenties naar Python classes). De structuur van een bron object ziet er zo uit:


{
"id": "rijksmuseum",
"extractor": "ocd_backend.extractors.rijksmuseum.RijksmuseumExtractor",
"transformer": "ocd_backend.transformers.BaseTransformer",
"item": "ocd_backend.items.rijksmuseum.RijksmuseumItem",
"loader": "ocd_backend.loaders.ElasticsearchLoader",
"rijksmuseum_api_key": ""
},

In dit voorbeeld zie je ook dat er aanvullende velden gedeclareerd worden, die je later kan opvragen. Handig voor dingen zoals API keys!

Stap 2: Een extractor bouwen

Je hoeft niet altijd zelf een extractor te bouwen. Er is onder meer al een extractor voor een API die volgens de OAI-PMH standaard werkt, dus dat scheelt tijd.

Het start punt voor een extractor is de run methode. In het geval van de OAI extractor is die vrij simpel, en ziet er als volgt uit:


def run(self):
    for record in self.get_all_records():
        yield record

In dit geval regelt get_all_records ook dingen zoals paginering voor de API calls die gemaakt worden. Het is een generator. In het algemeen zal een extractor die met paginering werkt er globaal als volgt uit zien:


import requests

from ocd_backend.extractors import BaseExtractor
from ocd_backend.extractors import log
from ocd_backend.utils.misc import parse_oai_response

class PaginatedExtractor(BaseExtractor):
    def __init__(self, *args, **kwargs):
        super(PaginatedExtractor, self).__init__(*args, **kwargs)

    def get_api_page(self, page=1):
        return NotImplementedError # does not really matter

    def get_all_records(self):
        for page in xrange(1, 10): # for example
            data = self.get_api_page(page)
            for record in data[‘records’]:
                yield ‘application/json’, record # for example

    def run(self):
        for record in self.get_all_records():
            yield record

Uiteraard zullen niet alle exatractors zo simpel zijn. Denk vooral aan eventuele HTML scrapers.

Stap 2: Een Transformer schrijven

Je zou eventueel een Transformer kunnen schrijven, maar in de meeste gevallen is de BaseTransformer al voldoende. Voor de volldigheid is hieronder een voorbeeldje dat CSV data deserialiseert:


class CSVTransformer(BaseTransformer):
    def deserialize_item(self, raw_item_content_type, raw_item):
        if raw_item_content_type == 'text/csv':
            return csv.reader([raw_item]) # misschien ook iets met headers doen

Stap 3: Een item lezer schrijven

Dit is het tweede deel van de Transform stap in het ETL proces, en voor de scrapers is dit de belangrijkste stap. Hier zorg je dat de goede velden op de goede plaats komen te staan.

Om een goed werkende Item transformer te schrijven maak je een subclass van BaseItem en moet je tenminste de volgende methoden implementeren:


def get_original_object_id(self):

def get_rights(self):

def get_original_object_urls(self):

def get_combined_index_data(self):

def get_index_data(self):

def get_all_text(self):

Een voorbeeld implementatie voor het Rijksmuseum kan je hier bekijken.

Hopelijk geeft dit een goed beeld van wat er moeten gebeuren om een scraper te schijven voor de Open Cultuur Data API. Als je zondag langskomst om mee te sleutelen aan de Open Cultuur Data API, heel veel plezier alvast. Alles over de Culture Harvests en de andere dagen waarop we cultuur gaan harvesten kan je hier vinden.

Geef een reactie