# Installeren met install.packages("officer")
# laden van de packages
library(officer)
# Inlezen van het Word-document
locatie_word <- here::here("_bestanden/voorbeeldbestanden/file-sample_500kB.docx")
inhoud_word <- read_docx(locatie_word) %>%
docx_summary() %>%
as_tibble()17 Inlezen van tekstdata
De inhoud van deze pagina is nog in ontwikkeling maar kan al wel gebruikt worden.
Tekst kan in veel soorten bestanden voorkomen. Hier focussen we ons op de twee meest voorkomende: Word-bestanden en PDF-bestanden.
17.1 Word-bestanden
De belangrijkste bron voor deze paragraaf komt uit uit de paragraaf Import Word document het boek officeverse.
Binnen de overheid worden de meeste documenten geschreven in het docx-bestandsformaat (formeel Office Open XML), dat automatisch wordt gebruikt in recente versies van Microsoft Word. Dit formaat is open source, waardoor er verschillende R-packages bestaan om Word-bestanden snel en makkelijk in te lezen.
17.1.1 Inlezen met read_docx()
In dit boek maken we gebruik van de R-package officer. Hieronder staat code hoe je één Word-document kunt inlezen. Voor dit voorbeeld maken we gebruik van een bestand van file-examples.com:
Het resultaat van de code hierboven is een Tibble met de inhoud van het Word-document. Hierin vallen een aantal dingen op:
- Alléén de tekst is uit het Word-document gehaald! Alle grafieken en figuren zijn dus niet te bekijken. Sterker nog: in de Tibble is niet eens opgenomen óf en wáár er figuren in het Word-document staan. De enige indicatie die eventueel zou kunnen hebben is als een grafiek of figuur een onderschrift heeft, omdat dát wel wordt ingelezen.
- Aan elk tekst-element wordt meta-data toegekend. Zo is te zien of een tekst een paragraaf is (met eventueel aangegeven of het een koptekst is), of onderdeel van een tabel.
- Bij de tekst uit de cel van een tabel is in de meta-data opgenomen in welke rij en kolom die specifieke cel staat.
Deze extra meta-data die wordt toegekend kan handig zijn, want misschien wil je in je verdere analyse de inhoud van tabellen niet meenemen. Of misschien wil je alleen een analyse doen op de kopteksten.
17.2 PDF-bestanden
Hoewel de meeste documenten bij de overheid worden geschreven als Word-document, worden ze meestal gepubliceerd in het PDF-formaat. Misschien vraag je je af waarom? Volgens Wikipedia:
Het portable document format, of kortweg pdf, is een bestandsformaat dat sinds ongeveer 1993 een de facto standaard is voor de uitwisseling van elektronische documenten en formulieren die in hun oorspronkelijke vorm gereproduceerd moeten kunnen worden.
En dat is gelijk het grootste voordeel: ongeacht op welk apparaat of besturingssysteem een PDF-document wordt geopend, het ziet er nagenoeg altijd hetzelfde uit.
Vanuit het oogpunt van de data-werker is er trouwens weinig positiefs over het PDF-formaat te melden: het zijn moeilijk in te lezen documenten, waarbij - in tegenstelling tot bijvoorbeeld Word-documenten- er veel minder meta-data aan tekstdelen is toe te voegen. En ook tabellen zijn moeilijk uit PDF-documenten te exporteren.
17.2.1 Inlezen met pdf_text()
Het inlezen van PDF-documenten gaat met de package pdftools. De makkelijkste manier is met de functie pdf_text(). Hieronder staat de code waarmee we met deze functie een PDF-document inlezen. Voor dit voorbeeld maken we gebruik van een bestand van file-examples.com:
# Installeren met install.packages("pdftools")
# laden van de packages
library(tidyverse)
library(pdftools)
# Inlezen van het Word-document
locatie_pdf <- here::here("_bestanden/voorbeeldbestanden/file-example_PDF_500_kB.pdf")
inhoud_pdf <- pdf_text(locatie_pdf)Het resultaat van deze code is een character-vector met een lengte van het aantal pagina’s in het PDF-bestand. Op de plekken waar een regel in het PDF-bestand eindigt is een linebreak in de tekst opnemen. Deze herken je aan de \n in de tekst. De eerste pagina ziet er bijvoorbeeld als volgt uit:
inhoud_pdf[1][1] " Lorem ipsum\n\n Lorem ipsum dolor sit amet, consectetur adipiscing\nelit. Nunc ac faucibus odio.\n\nVestibulum neque massa, scelerisque sit amet ligula eu, congue molestie mi. Praesent ut\nvarius sem. Nullam at porttitor arcu, nec lacinia nisi. Ut ac dolor vitae odio interdum\ncondimentum. Vivamus dapibus sodales ex, vitae malesuada ipsum cursus\nconvallis. Maecenas sed egestas nulla, ac condimentum orci. Mauris diam felis,\nvulputate ac suscipit et, iaculis non est. Curabitur semper arcu ac ligula semper, nec luctus\nnisl blandit. Integer lacinia ante ac libero lobortis imperdiet. Nullam mollis convallis ipsum,\nac accumsan nunc vehicula vitae. Nulla eget justo in felis tristique fringilla. Morbi sit amet\ntortor quis risus auctor condimentum. Morbi in ullamcorper elit. Nulla iaculis tellus sit amet\nmauris tempus fringilla.\n\nMaecenas mauris lectus, lobortis et purus mattis, blandit dictum tellus.\n\n Maecenas non lorem quis tellus placerat varius.\n\n Nulla facilisi.\n\n Aenean congue fringilla justo ut aliquam.\n\n Mauris id ex erat. Nunc vulputate neque vitae justo facilisis, non condimentum ante\n sagittis.\n\n Morbi viverra semper lorem nec molestie.\n\n Maecenas tincidunt est efficitur ligula euismod, sit amet ornare est vulputate.\n\n\n 12\n\n 10\n\n 8\n Column 1\n 6\n Column 2\n 4 Column 3\n\n 2\n\n 0\n Row 1 Row 2 Row 3 Row 4\n"
Merk op dat los van de linebreaks er géén meta-data aan de tekst is toegevoegd.
Waarschijnlijk wil je de verschillende elementen van de character-vector toevoegen aan een Tibble, waarbij je ook opneemt om welke pagina het gaat. Dit kun je als volgt doen:
inhoud_pdf_tibble <- inhoud_pdf %>%
as_tibble_col(column_name = "text") %>%
rowid_to_column(var = "pagina")De meeste PDF-bestanden waar je mee te maken zult hebben zijn door bijvoorbeeld Microsoft Word of DTP-software geëxporteerd naar PDF. Bij het schrijven naar PDF wordt een aantal meta-data meegegeven, waaronder tekstgrootte en linebreaks.
Soms heb je echter te maken met ingescande PDF-bestanden, waarbij tijdens het inscannen de tekst al door middel van OCR-software naar tekst is omgezet. Hoewel deze ‘gewoon’ zijn in te lezen met de technieken die we hierboven hebben beschreven, wordt er geen meta-data aan de tekst toegevoegd. Hierdoor kun je onder andere niet achterhalen of er een linebreak in de tekst staat. Dit zorgt ervoor dat je paragrafen niet geautomatiseerd kunt inlezen.
In de toekomst zal uitgelegd worden hoe je door middel van de hieronder beschreven functie pdf_data()` dit tóch voor elkaar kunt krijgen.
17.2.2 Inlezen met pdf_data()
Het kan echter zijn dat je toch meer informatie wilt over de tekst in een PDF. In dat geval kun je ook gebruik maken van de functie pdf_data().
Het inlezen gaat dan als volgt:
inhoud_pdf_meta <- pdf_data(locatie_pdf)Het resultaat van deze functie is list-object met een list-element per pagina met daarin een data.frame waarin elk woord één record (regel) is. Daarbij is voor dát woord (variabele text) meta-data opgenomen: de breedte (width) en hoogte (height), de x- en y-positie én een variabele (space) waarin wordt aangegeven of er na het woord een spatie (TRUE) of een nieuwe regel (FALSE) komt. De eerste 10 woorden van de eerste pagina zien er bijvoorbeeld als volgt uit:
inhoud_pdf_meta[[1]] %>%
head(10)# A tibble: 10 × 6
width height x y space text
<int> <int> <int> <int> <lgl> <chr>
1 85 31 206 69 TRUE Lorem
2 82 31 299 69 FALSE ipsum
3 54 20 74 145 TRUE Lorem
4 52 20 134 145 TRUE ipsum
5 44 20 192 145 TRUE dolor
6 20 20 242 145 TRUE sit
7 46 20 268 145 TRUE amet,
8 101 20 320 145 TRUE consectetur
9 88 20 427 145 FALSE adipiscing
10 30 20 56 166 TRUE elit.
Wanneer je een PDF-bestand met meerdere pagina’s hebt ingelezen met de pdf_data-functie, zul je waarschijnlijk de inhoud van de verschillende pagina’s in één tibble willen samenvoegen. Dit gaat heel makkelijk met de bind_rows()-functie uit de dplyr-package. We voegen daarnaast de regel waar het woord op staat toe met de functie cur_group_id() uit de dplyr-package door een groeperen op de pagina-variabele en de y-variabele:
inhoud_pdf_meta_tibble <- inhoud_pdf_meta %>%
bind_rows(.id = "pagina") %>%
group_by(pagina, y) %>%
mutate(regel = cur_group_id())De meta-data van de eerste 10 woorden ziet er dan als volgt uit:
Afhankelijk van je doel kun je nu verschillende records weer samenvoegen tot regels, zinnen of paragrafen.
17.3 Inlezen van alle bestanden in een map
Je weet nu wel hoe je een afzonderlijk Word- of PDF-bestand moet inlezen, maar de kans is groot dat je in een keer meerdere bestanden wilt inlezen.
Je hebt hier verschillende methodes voor. Hier laten we zien hoe je dit met een for-loop én door een functie te schrijven. We gebruiken hiervoor alle stukken in de module raadsvragen van de Haagse gemeenteraad van de maand oktober 2023. en die we hebben opgeslagen in de map _bestanden/raadsvragen.1
17.3.1 for-loop
Het idee bij een for-loop is dat een stuk code de hele tijd herhaald wordt, tot er aan een bepaalde conditie voldaan wordt. Het stappenplan dat we doorlopen is als volgt:
- We maken een
Tibblemet eencharacter-variabele met de bestandsnamen; - We gebruiken de bestandsnamen in een
for-loop om eenlist-variabele met de tekst van de bestanden te maken. In elklist-item staat een ingelezen document. - We gebruiken de functie
unnest_longer()om de pagina’s uit delist-kolom om te zetten naar één record per pagina. Ook voegen we in deze stap per document een pagina-nummer toe.
# Stap 1
map_raadsvragen <- "_bestanden/raadsvragen"
haagse_raadsvragen <- tibble(bestandsmap = map_raadsvragen,
bestandsnaam = list.files(map_raadsvragen,
recursive = T))
# Stap 2
for(bestand in seq_along(haagse_raadsvragen$bestandsnaam)){
inhoud_bestand <- pdftools::pdf_text(file.path(haagse_raadsvragen$bestandsmap[bestand],
haagse_raadsvragen$bestandsnaam[bestand]))
haagse_raadsvragen$text[bestand] <- list(inhoud_bestand)
}
# Stap 3
haagse_raadsvragen <- haagse_raadsvragen %>%
unnest_longer(text) %>%
group_by(bestandsnaam) %>%
mutate(pagina = row_number()) %>%
ungroup() # groepering in data verwijderenHet resultaat is een Tibble met 4 kolommen (bestandsmapmap, bestandsnaam, text en pagina) met één record per pagina:
# A tibble: 824 × 4
bestandsmap bestandsnaam text pagina
<chr> <chr> <chr> <int>
1 _bestanden/raadsvragen 314287_RIS314287 Financieringsmachine Re… " … 1
2 _bestanden/raadsvragen 314288_RIS314288 Waar moeten bedrijven o… " … 1
3 _bestanden/raadsvragen 314288_RIS314288 Waar moeten bedrijven o… "aan… 2
4 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "RIS… 1
5 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "In … 2
6 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "PRE… 3
7 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "Oud… 4
8 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "Het… 5
9 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "‘Ni… 6
10 _bestanden/raadsvragen 314289_RIS314289 Ingegooide ruiten in bu… "Vri… 7
# ℹ 814 more rows
17.3.2 map-funtie
In R zijn for-loops echter niet gebruikelijk en wordt er vooral gebruik gemaakt van functional programming. Het idee is dat je een functie schrijft die één document inleest, en die - bijvoorbeeld - met een van de map*-functies2 uit de package purrr toepast op elk record uit een Tibble. Het stappenplan dat we doorlopen is als volgt:
- We maken een
Tibblemet eencharacter-variabele met de bestandspaden. Dit is hetzelfde als bij defor-loop; - We schrijven een functie die een bestandspad als variabele kent en één document inleest als
list-object. - We gebruiken de bestandspaden om een
list-variabele met de tekst van de bestanden te maken. Elklist-item wordt gevuld door demap*-functie en bevat vervolgens de inhoud van één ingelezen document; - We gebruiken de functie
unnest_longer()om de pagina’s uit delist-kolom om te zetten naar één record per pagina. Ook voegen we in deze stap per document een pagina-nummer toe. Dit is hetzelfde als bij defor-loop.
# Stap 1
map_raadsvragen <- "_bestanden/raadsvragen"
haagse_raadsvragen <- tibble(bestandsmap = map_raadsvragen,
bestandsnaam = list.files(map_raadsvragen,
recursive = T))
# Stap 2
inlezen_pdf_bestand <- function(bestandslocatie){
pdftools::pdf_text(bestandslocatie)
}
# Stap 3
haagse_raadsvragen <- haagse_raadsvragen %>%
mutate(text = map(file.path(bestandsmap, bestandsnaam),
inlezen_pdf_bestand))
# Stap 4
haagse_raadsvragen <- haagse_raadsvragen %>%
unnest_longer(text) %>%
group_by(bestandsnaam) %>%
mutate(pagina = row_number())Het resultaat van deze code is preciés hetzelfde als bij de inlees-variant met de for-loop. Het grootste verschil is dat door het gebruik van een functie de code veel makkelijker te lezen is.
De meta-data van de documenten en de URL’s naar de PDF’s zijn door middel van webscraping van het Haagse raadsinformatiesysteem gedownload. Hiervoor is de package
rvestgebruikt.↩︎De
purrr-package kent verschillendemap-functies, zoalsmap_lgl(),map_int(),map_dbl()enmap_chr(). Ze verschillen vooral in de output van de functie. De functiemap()geeft altijd eenlistals resultaat. Kijk voor meer informatie op deze pagina.↩︎