Tekst classificatie 1: webscraping in R
Tegenwoordig word je van alle kanten om reviews gevraagd. Was je Thuisbezorgd maaltijd er op tijd, ben je tevreden met je wasmachine van Coolblue, zitten je schoenen van Zalando lekker? Die data is ontzettend waardevolle feedback voor bedrijven, spreek ik als ex-marketeer, maar wist je dat je die data ook kan gebruiken als input voor een machine learning model? Dat wil ik in dit project gaan doen: een algoritme trainen op basis van reviews, dat vervolgens andere teksten kan classificeren in positief of negatief. Handig, want doordat de reviews door middel van sterren al een classificatie hebben, hoef je deze niet eerst zelf te classificeren. In deze post ga ik beginnen met review-data binnen te halen door middel van webscraping.
Waarom webscraping?
Veel websites gebruiken een API waarmee je vanuit R (of andere software) direct toegang kan krijgen tot de data van de site. Zo heb ik in mijn vorige post dankbaar gebruik gemaakt van de API van Twitter. Andere sites hebben geen API of stellen deze alleen beschikbaar voor betalende klanten. Dan kun je óf handmatig alle data gaan kopiëren/plakken, of je kan de site geautomatiseerd ‘scrapen’, wat ik in deze post ga laten zien. In alle eerlijkheid ben ik waarschijnlijk net zo lang bezig geweest met het maken van een code die dit doet als dat ik alles handmatig gekopieerd en geplakt had, maar dit was beter voor mijn geestelijke gezondheid. De pakketten die ik hiervoor gebruikt heb, zijn:
library(dplyr) library(stringr) library(lubridate) library(rvest) library(rjson) library(jsonlite) library(tm)
Ik gebruik voor dit project reviews van een website die beoordelingen over bedrijven verzamelt, voornamelijk webwinkels. Ik gebruik deze site zelf ook wel eens om te checken of een website legit is of niet. Ik koos voor deze site omdat er én veel Nederlandstalige reviews opstaan én ik er een goede tutorial over kon vinden. Echter bleek dat ondanks dat deze tutorial slechts een jaar oud is, deze alweer compleet achterhaald is. Dat is het nadeel van webscraping – de code schrijven moet specifiek per website gedaan worden en kan behoorlijk veel tijd kosten, maar een kleine update en er klopt niks meer van. Toch besloot ik met deze site door te gaan en dan maar zelf mijn code te maken. Ik wil echter niet mijn hele code in een keer online zetten om onverantwoord server-verbruik te voorkomen, dus doe ik het in twee stappen: als eerste laat ik zien hoe je de reviews van 1 pagina binnenhaalt en vervolgens leg ik uit hoe je achter het pagina-aantal kan komen om alle reviews van een bedrijf binnen te halen.
Alle reviews van een pagina scrapen
Als je een website wil scrapen, moet je eerst bedenken wat je precies wilt hebben. In mijn geval is dat een dataframe met op elke regel de naam van het bedrijf, de inhoud van de review, een cijfer en een datum. Vervolgens kun je in de html code gaan zoeken hoe deze informatie te herkennen is. Wellicht heeft het een bepaalde CSS-class of staat het onder een bepaalde titel. Je kunt daar achter komen door op je rechtermuisknop te klikken en inspecteren te selecteren, waardoor een nieuw venster met de html code opent.
Vervolgens kun je met crtl + f zoeken naar bijvoorbeeld een stuk tekst uit een review, om zo te weten te komen waar de reviews zich bevinden.
Op deze site staat de code die ik wil hebben onder een kopje ‘script’. Dat wil ik dus hebben in R. Met de functies uit het pakket rvest kun je HTML data interpreteren, zoals de functie read_html.
#url kan elke pagina van een bedrijf op de reviewsite zijn url <- "***" #leest de html van de site html <- read_html(url) # zoekt naar html node pages_review_data <- html %>% html_nodes('script')
Als je naar pages_review_data (hierboven) kijkt in R, zie je dat dit een list is met maar liefst 81 nodes met de naam ‘script’ zijn. Je kunt identificeren welke de juiste is door naar de attributes van de nodes te kijken. Achter het stukje script dat ik zoek staat in de HTML code het attribute ‘data-business-unit-json-ld’, dit lijkt specifiek te zijn voor deze node. Als dit attribute dus aanwezig is, wil ik dit stukje script selecteren. Dit doe ik met een for-loop.
#selecteer de node en lees de inhoud for(node in pages_review_data) { if (!is.na(html_attr(node, "data-business-unit-json-ld"))) { text <- html_text(node) } }
Als je dit uitvoert in R, zie je dat de inhoud van deze node – het bestand text – één lange value is geworden. Dit komt doordat het formaat eigenlijk een JSON is, en dus ook als zodanig gelezen moet worden. Dit kan met de functie fromJSON (uit het rjson pakket, om de een of andere reden werkte dezelfde functie uit het jsonlite pakket niet).
for(node in pages_review_data) { if (!is.na(html_attr(node, "data-business-unit-json-ld"))) { json_data <- rjson::fromJSON(html_text(node)) } }
Als je naar json_data kijkt, zie je dat deze node ook een uitgebreide lijst met sublijsten is. De relevante informatie staat onder het eerste onderdeel, verdeeld over 20 sublijsten die allemaal reviews op een pagina representeren. Door middel van subsetting kun je de inhoud eruit halen, maar eerst moet je een plek hebben om ze op te slaan.
#maak een leeg dataframe df <- data.frame(naam=character(), datum=character(), rating=character(), review=character(), stringsAsFactors=FALSE) #loop door de 20 reviews en sla relevante onderdelen op for(i in 1:20) { df[i,1] <- json_data[[1]][["name"]] df[i,2] <- json_data[[1]][["review"]][[i]][["datePublished"]] df[i,3] <- json_data[[1]][["review"]][[i]][["reviewRating"]][["ratingValue"]] df[i,4] <- json_data[[1]][["review"]][[i]][["reviewBody"]] }
Als ik alle bovenstaande stappen samenvoeg, ziet mijn code om alle reviews van een pagina binnen te halen er uiteindelijk zo uit.
url <- "***" #leeg dataframe maken df <- data.frame(naam=character(), datum=character(), rating=character(), review=character(), stringsAsFactors=FALSE) #url lezen en script node vinden html <- read_html(url) pages_review_data <- html %>% html_nodes('script') #voor elke node checken of hij het juiste attribute heeft for(node in pages_review_data) { if (!is.na(html_attr(node, "data-business-unit-json-ld"))) { json_data <- rjson::fromJSON(html_text(node)) #extraheer de 20 reviews for(i in 1:20) { df[i,1] <- json_data[[1]][["name"]] df[i,2] <- json_data[[1]][["review"]][[i]][["datePublished"]] df[i,3] <- json_data[[1]][["review"]][[i]][["reviewRating"]][["ratingValue"]] df[i,4] <- json_data[[1]][["review"]][[i]][["reviewBody"]] } #stoppen zodra hij de juiste node gevonden heeft break } }
Alle pagina’s
Hiermee importeer je dus alle reviews van één pagina naar R. Maar misschien wil je dit voor meerdere of zelfs alle pagina’s van een bedrijf doen. Dan kun je het best een for-loop maken die hetzelfde proces uitvoert voor alle paginanummers van een bedrijf. Soms kun je het totale pagina-aantal makkelijk vinden in de HTML-code (omdat het vaak in het pagina-navigator element staat), maar in dit geval niet. Dit kun je natuurlijk wel zelf berekenen door het totaal aantal reviews door het aantal reviews per pagina te delen. Hiervoor moet je eerst het aantal Nederlandstalige reviews zoeken in de HTML code.
Zoals je ziet staat ook deze onder een script, dit keer met het attribute data-initial-state = review-filter. Op een vergelijkbare manier als voorheen kun je hieruit het totaal aantal reviews halen en dus het totaal aantal pagina’s maken.
#url lezen en script node vinden html <- read_html(url) pages_data <- html %>% html_nodes('script') #als attribute niet aanwezig is, overslaan for(node in pages_data) { if (is.na(html_attr(node, "data-initial-state"))) { next } else if (html_attr(node, "data-initial-state") == "review-filter") { jason_data <- jsonlite::fromJSON(html_text(node)) nl_review_count <- jason_data[[1]][2,3] page_count <- ceiling(as.numeric(removePunctuation(nl_review_count)) / 20) break } }
Je kunt vervolgens de bovenstaande code voor het binnenhalen van het pagina-aantal en de code voor het binnenhalen van de data samenvoegen in een for-loop, waarbij je voor elk paginanummer een url bouwt waar je vervolgens de data uithaalt. Ik heb zelf twee dataframes gebouwd om het in op te slaan: één tijdelijke dataframe per loop, en één permanente waarin alle tijdelijke frames worden opgeslagen met een rbind(). Om te voorkomen dat ik de server overbelast, heb ik bovendien met een Sys.sleep(3) een wachttijd van 3 seconden per pagina ingebouwd.
Dat was mijn post over datascraping, ik hoop dat je er iets aan gehad hebt! In de volgende post ga ik een aantal reviews gebruiken als input voor een classifier.