Hvordan "skrape" data i R?

Automatisert datainnsamling

Datamaskiner elsker å gjøre den samme tingen mange ganger. Det er fint, for da kan R gjøre unna det kjedelige arbeidet, mens jeg gjør andre ting.

Automatisert datainnsamling er ikke vanskelig, men det kan være en tålmodighetsprøve. Så lenge informasjonen vi er ute etter er strukturert med tekst eller i en html-liste, vil vi kunne fortelle en datamaskin hvordan dataene skal hentes ut og sorteres. Skraping er stordriftsøkonomi. Det krever en del tid å finne datastrukturen, men når dette er gjort, kan vi sette R til å jobbe for oss. Det er magisk!

Vi skal samle inn data fra EUR-Lex. EUR-Lex er nettsiden hvor alle EU sine offisielle dokumenter befinner seg for offentlig innsyn. Spesifikt, er jeg interessert i å samle inn “metainformasjon” om domsavsigelser fra EU-domstolen. Metainformasjon her betyr at jeg ikke er interessert i selve dommen, men informasjon rundt den. Hva er tittelen på dommen og når ble den publisert?

Et eksempel er Coman-saken hvor EU-domstolen gikk langt i å annerkjenne homoekteskap for alle EU-borgere (2016).

Vi kan dele arbeidet inn i flere faser:

  1. Jeg legger en strategi hvor jeg definerer målet mitt presist (strukturen til en datamatrise) og en skisse over veien dit (hvilken informasjon jeg skal bruke og hvor den befinner seg). Dette er en kartleggingsfase.

  2. Jeg arbeider fram en konkret taktikk for hvordan informasjonen skal hentes ut. Teksten jeg skraper (rådataene) kan være strukturert ulikt, derfor trenger jeg ulike taktikker for ulike variabler. Hvis informasjonen jeg er ute etter, er plassert ulikt i ulike dokumenter, vil jeg også ha alternative taktikker for ulike observasjoner i samme variabel. Hver taktikk er en egen kodesnutt som henter informasjonen jeg trenger. Det er dette arbeidet som tar mest tid, og jeg begynner alltid med ett eksempel (en observasjon på en variabel).

  3. Jeg skraper (samler inn) alle dataene (datapunktene). Nå går jeg fra ett eksempel til å samle inn informasjon fra alle dokumentene/sidene jeg er interessert i. Denne delen av arbeidsprosessen innebærer å gå fram og tilbake mellom skraping og korrigering av taktikken min. I prosessen vil jeg generalisere R-koden min slik at den er funksjonell for alle dokumenter. Ofte vil jeg møte på problemer som krever nye taktikker og/eller en justering av den gamle. Dette er når jeg luker feil.

Legg en strategi: Hva vil jeg oppnå?

Kartlegging

Første skritt i en datainnsamlingsprosess er å kartlegge hvilken informasjon jeg ønsker og hvilken som finnes. For å finne ut mer, begynner jeg med å surfe nettet på vanlig vis. Et viktig poeng er å finne ut om informasjonen jeg ønsker faktisk er tilgjengelig for alle observasjoner/nettsider.

Jeg bestemmer meg så for datastrukturen jeg ønsker for matrisen min (sluttproduktet). I vårt eksempel, ønsker jeg en matrise hvor hver observasjon (linje) er en domsavsigelse, og hver variabel gir en spesifikk type informasjon om avsigelsen. I dette tilfellet er jeg interessert i datoen for dokumentet.

Spesifiser en taktikk: Jobb med eksempler

Selv om målet med datainnsamlingen er en datamatrise med flere variabler og mange observasjoner, men jeg begynner alltid med en observasjon og en variabel. Det vil si at jeg jobber med et typisk eksempel på dokumentene jeg skal samle data fra. “Typisk” betyr her at dokumentet følger strukturen til flest mulig av dokumentene jeg skal skrape fra.

Jeg har valgt meg Coman-dommen som et typisk eksempel. Den finnes her: https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673

Vi ser at teksten på siden er strukturert. Blant annet ser vi at datoet på dokumentet (“Date of document”) er annsert i tykk blå skrift etterfulgt av selve datoen i svart skrift. For nettsider er denne strukturen definert av html-koder. Html blir brukt for å definere layouten i en tekst.

For å se html-strukturen, kan du høyreklikke på siden og velge “View page source”. Et nytt vindu åpner seg, og vi kan se hvordan dokumentet ser ut bak kulissene. Nå blir det klart for oss at teksten vi ser på selve nettsiden egentlig er en liste hvor all informasjon er lagt inn som egne punkter. Html-lista er ikke egentlig laget for å strukturere data, men vi kan bruke html-strukturen til vår fordel.

Nå kan vi finne fram til informasjonen vi ønsker og identifisere hvor i lista den ligger. Punkter i denne lista heter “noder”. Noder kan ha ulike navn avhengig av hvordan man ønsker at teksten ser ut. Dette er det vi bruker for å finne fram til våre data.

For å finne nodenavnet til datoen til dommsavsigelsen, gjør jeg et tekstsøk. Tast “Ctrl+F” og skriv inn “Date of document”. Vi ser at “Date of document” ligger i en node som er kalt dt og at selve datoen ligger i en node kalt “dd”. Begge hører til en node kalt “dl”. Disse er “søsken” fordi de har samme forelder. Vi ser også at det finnes andre søsken i flokken (“Date lodged”).

Vi ønsker bare å samle inn datoen for selve dokumentet, så første utfordring er å gjøre samme søk i R. La oss åpne R studio.

Jeg begynner med å hente inn `rvest``-pakken for skraping i R fra biblioteket. Om du ikke har installert den enda, må du gjøre dette først.

library(rvest)

Det først jeg må gjøre er å definere nettadressen for siden jeg ønsker å skrape.

url <- "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673"
library(rvest)

Nå kan jeg la R lese inn html-filen fra denne adressen. Jeg lagrer resultatet i et objekt kalt doc. Vi kan ta en titt på doc, men R-studio vil ikke vise oss hele dokumentet. Da ville korttidsminnet i RStudio bli “spist opp” og programmet “henger”.

doc <- read_html(url)
doc
## {html_document}
## <html lang="en" class="no-js" xml:lang="en">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
## [2] <body> \n        <script type="application/json">{\n            "utility" ...

Objektet er av typen “xml_document”; en liste med mange noder.

class(doc)
## [1] "xml_document" "xml_node"

Jeg kan hente alle noder fram ved å bruke nodenavnet.

Enkelt, men litt for spesifikt

html_nodes() henter fram alle noder i et xml_document-objekt ved hjelp av navnet deres. Vi noterte oss at datoen er lagret i “dd”.

node <- 
  doc %>%
  html_nodes("dd")
node
## {xml_nodeset (18)}
##  [1] <dd xmlns="http://www.w3.org/1999/xhtml">Romanian</dd>
##  [2] <dd xmlns="http://www.w3.org/1999/xhtml">05/06/2018</dd>
##  [3] <dd xmlns="http://www.w3.org/1999/xhtml">30/12/2016</dd>
##  [4] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <ul>\n<li>  ...
##  [5] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
##  [6] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
##  [7] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
##  [8] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
##  [9] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
## [10] <dd xmlns="http://www.w3.org/1999/xhtml">Ilešič</dd>
## [11] <dd xmlns="http://www.w3.org/1999/xhtml">Wathelet</dd>
## [12] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <span lang= ...
## [13] <dd xmlns="http://www.w3.org/1999/xhtml">\n                  <ul>\n<li>3 ...
## [14] <dd>\n               <span lang="en">Treaty establishing the European Ec ...
## [15] <dd>\n               <a href="./../../../search.html?type=advanced&amp;D ...
## [16] <dd>\n               <ul>\n<li>Interprets <a href="./../../../legal-cont ...
## [17] <dd>\n               <ul>\n<li>Related judicial information <a href="./. ...
## [18] <dd>\n               <ul>\n<li>\n                     <a href="./../../. ...

Nå kan jeg bruke indekseringsverktøyet for å hente riktig “dd”. Jeg ser at denne ligger i observasjon 2.

node[2]
## {xml_nodeset (1)}
## [1] <dd xmlns="http://www.w3.org/1999/xhtml">05/06/2018</dd>

Det eneste som gjenstår, er å fjerne html-koden for kun å beholde teksten/informasjonen. Det gjør vi med html_text()

node[2] %>%
  html_text()
## [1] "05/06/2018"

Er vi fornøyd med resultatet, kan i ta vare på det.

dato <- 
  node[2] %>%
  html_text()

Nå har jeg én observasjon på én variabel. Hurra! Nå kan jeg legge dette inn i et datasett. Det er lurt å finne en “nøkkelvariabel” som identifiserer hver observasjon. Da kan jeg lett koble resultatet fra ulike skrapeprosesser med hverandre slik at jeg til sist får mange variabler i datasettet mitt. Foreløpig kan jeg bruke nettadressen (url) som en slik nøkkel.

Til dette bruker jeg cbind(): Jeg binder de to variablene sammen kolonnevis.

#Opprett et datasett

tmp <- cbind(dato, url) tmp

##      dato        
## [1,] "05/06/2018"
##      url                                                                    
## [1,] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673"

Nå har jeg en matrise med en linje og to variabler. Senere skal vi lage et datasett (en datamatrise) av denne og alle andre observasjoner vi samler inn (fra andre sider). Men foreløpig har vi et potensielt problem. Hva om informasjonen vi ønsker ikke ligger i node 2 hver gang?

Generalisere med indeksering

Det er en fordel å bruke mer generelt språk når vi skraper. Da kan vi bruke html-strukturen.

Jeg begynner med å hente ut alle familier som har en “forelder” med navnet “dl”. Dette lagrer jeg i et nytt objekt jeg kaller familie.

familier <- doc %>%
  html_nodes("dl")
familier
## {xml_nodeset (7)}
## [1] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Authenti ...
## [2] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Date of  ...
## [3] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Subject  ...
## [4] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Author:  ...
## [5] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Type of  ...
## [6] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Notes re ...
## [7] <dl class="NMetadata">\n<dt>Treaty: </dt>\n            <dd>\n             ...

Det er flere slike “familier”. Derfor “tar jeg tak” i familien jeg er interessert i. Da bruker jeg grep() -funksjonen for å finne nodenummeret til familien med et barn som inneholder info om “Date of document”.

grep("Date of document", familier)
## [1] 2

Jeg får til svar at det er node (“forelder”) 2 som har et slikt barn. Nå kan jeg indeksere. Det kan jeg gjøre på ulikt vis.

#Dette er synonymer
familier[2]
## {xml_nodeset (1)}
## [1] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Date of  ...
familier[grep("Date of document", familier)]
## {xml_nodeset (1)}
## [1] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Date of  ...
id <- grep("Date of document", familier)
familier[id]
## {xml_nodeset (1)}
## [1] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Date of  ...

Ved å lagre indekseringen i id kan jeg bruke koden selv for tilfeller hvor det ikke er andre familie-node som er den riktige.

Jeg lagrer riktig familie i en node.

familie <- familier[id]
familie
## {xml_nodeset (1)}
## [1] <dl class="NMetadata">\n<dt xmlns="http://www.w3.org/1999/xhtml">Date of  ...

Nå inneholder familie barna i den riktige familien, men jeg ønsker bare ett av barna. Det betyr en ny omgang med indeksering.

“dd” og “dt” kommer i par. “dt” inneholder datotittelen, mens “dt” inneholder selve datoen. Jeg bruker “dt” til å finne indeksnummeret…

#Trekk ut barn med dato
tittel.barn <- familie %>% 
  html_nodes("dt")
tittel.barn
## {xml_nodeset (2)}
## [1] <dt xmlns="http://www.w3.org/1999/xhtml">Date of document: </dt>
## [2] <dt xmlns="http://www.w3.org/1999/xhtml">Date lodged: </dt>
#Indeks for dato
id2 <- grep("Date of document", tittel.barn)
id2
## [1] 1

… men jeg henter ut informasjon fra “dd”. Det gjør jeg ved å gå ett skritt tilbake, så et fram, denne gangen i retning av “dd”.

#Trekk ut dato fra familien
dato.barn <- familie %>% 
  html_nodes("dd")

Til sist, trekker jeg ut teksten vekk fra html-koden ved hjelp av html_text()

#Trekk ut teksten
dato <- dato.barn[id2] %>%
  html_text()

dato

## [1] "05/06/2018"

Voilà!

Nå kan jeg lage den samme matrisen som i stad.

tmp <- cbind(url, dato)

Hva har jeg vunnet? Jeg har generalisert koden min, slik at jeg skal kunne skrape flere sider uten å behøve å endre koden for hver gang. Etter å ha generalisert koden min, vil jeg mangfoldiggjøre den.

Mangfoldiggjør: skriv en løkke

Å skrape én enkelt nettside/dokument tar mye tid. Stordriftsfordelen kommer av at jeg kan bruke den samme koden på neste side. Det betyr at jeg må oppgi adressen til neste nettside som skal skrapes og kjøre den samme koden igjen.

Definer et R-objekt for datamatrisen

Før jeg skraper første gang, oppretter jeg et tomt R-objekt som jeg har kalt df.

df <- NULL

Dette er objektet jeg kommer til å lagre dataene mine i.

Fra én til to observasjoner

Nå kommer første test. Vil koden min fungere på neste domsavsigelse? For å teste dette, samler jeg hele kodesnutten. Det eneste jeg endrer er url-adressen.

url <- "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"

#Les inn nettsiden doc <- read_html(url)

#Finn alle familier familier <- doc %>% html_nodes("dl")

#Finn familien med riktig barn id <- grep("Date of document", familier) familie <- familier[id]

#To parallelle operasjoner

#1. finn noden med riktig datotittel tittel.barn <- familie %>% html_nodes("dt")

id2 <- grep("Date of document", tittel.barn)

#2. finn noden med datoinformasjon dato.barn <- familie %>% html_nodes("dd")

#Trekk ut teksten dato <- dato.barn[id2] %>% html_text()

#Lag en matrise med en linje for observasjonen tmp <- cbind(url, dato)

tmp

##      url                                                                    
## [1,] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"
##      dato        
## [1,] "02/06/2016"

Det ser ut til å fungere! I en vanlig arbeisflyt, vil jeg ofte gå fram og tilbake på dette stadiet for å tilpasse koden min slik at den kan håndere stadig flere typer eksempler. Dette blir tema for siste seksjon.

Nå må jeg ta vare på informasjonen i df-objektet. For hvert eksempel/nettside jeg har skrapet, har jeg opprettet en minimatrise som jeg har kalt tmp. tmp-objektet blir overskrevet hver gang jeg skraper en ny side. Derfor skriver jeg en ny linje i kodesnutten min, hvor jeg overfører all informasjonen fra det nylig skrapte siden til en matrise med alle tidligere skrapte sider. Det gjør jeg ved å binde de gamle dataene sammen med de nye hjelp av rbind().

df <- rbind(df, tmp)
df
##      url                                                                    
## [1,] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"
##      dato        
## [1,] "02/06/2016"

Foreløpig er df ganske stusselig, men det vil snart endre seg.

Skriv en løkke

Nå kan jeg mangfoldiggjøre. Måten man får R til å gjøre den samme tingen flere ganger, er å skrive en løkke (en “loop”). Det gjør jeg i tre skritt.

1. Sett opp en universliste

Jeg må ha en vektor med alle nettsidene jeg skal skrape. Her har jeg en vektor med to observasjoner.

url <- c("https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438",
         "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673")

2. Jeg indekserer

Nå kan jeg indeksere url-objektet mitt. Her trekker jeg ut første observasjon i det som skal bli datasettet mitt.

url[1]
## [1] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"

Jeg kan selvsagt gjøre indekseringen ved hjelp av et objekt. Det har vi jo allerede gjort.

i = 1
url[i]
## [1] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"

Her har jeg definert i som 1, slik at vi ser den første filadressen. Men jeg kan også endre verdien til 2 uten å endre resten av koden min.

i = 2
url[i]
## [1] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673"

… og det er nettopp det løkker gjør.

3. Jeg skriver en løkke

Jeg kan be R om å endre verdien til i for meg. Her forteller jeg R at i skal suksessivt ha alle verdier i vektoren 1:10. I krølle-parentesen befinner R-koden som du ønsker å kjøre flere ganger.

for(i in 1:10){
  cat(i)
}
## 12345678910

Vanligvis vil du ikke se hva som foregår i løkken (alt går veldig fort). Derfor bruker jeg cat()-funksjonen i dette eksempelet for printe ut hva som skjer.

Nå kan jeg samle alt jeg har gjort og pakke kodesnutten inn i løkka. Jeg klipper og limer fra tidligere. Merk deg hvordan jeg a) indekserer url[i] (på to steder i koden) og b) limer tmp sammen med df for hver gang jeg skraper en ny observasjon.

#Et tomt R-objekt før løkken
df = NULL

#… så en løkke for(i in 1:2){

#Les inn nettsiden doc <- read_html(url[i])

#Finn alle familier familier <- doc %>% html_nodes("dl")

#Finn familien med riktig barn id <- grep("Date of document", familier) familie <- familier[id]

#To parallelle operasjoner

#1. finn noden med riktig datotittel tittel.barn <- familie %>% html_nodes("dt")

id2 <- grep("Date of document", tittel.barn)

#2. finn noden med datoinformasjon dato.barn <- familie %>% html_nodes("dd")

#Trekk ut teksten dato <- dato.barn[id2] %>% html_text()

#Lag en matrise med en linje for observasjonen tmp <- cbind(url[i], dato)

df <- rbind(df, tmp) }

load("df.rda")

Tadaaa! Jeg har samlet in TO observasjoner. Verden ligger for mine føtter.

df
##                                                                             
## [1,] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62014CJ0438"
## [2,] "https://eur-lex.europa.eu/legal-content/EN/ALL/?uri=CELEX:62016CJ0673"
##      dato        
## [1,] "02/06/2016"
## [2,] "05/06/2018"
Teaching material Quantitative methods Tutorials
Avatar
Silje Synnøve Lyder Hermansen
Assistant Professor

Silje’s research concerns democratic representation in courts and parliaments. She also teaches various courses in research methods and comparative politics.

Related