R programmēšana: kā iegūt datus no pdf faila?

Aivis Brutans
6 min readJul 1, 2023

--

Atslēgvārdi: pdftools, R programma, Saeimas deputātu atalgojums.

Šajā R programmēšanas kursā iemācīsimies iegūt datus no pdf failiem, izmantojot R pakotnes pdftools funkciju pdf_text. Kā piemēru izmantosim Saeimas mājaslapā publicētos pdf failus, kurā apkopots Saeimas deputātu atalgojums — https://www.saeima.lv/lv/saeimas-struktura/deputatu-atalgojums.

Datu apstrādei izmantotas šādas papildu R pakotnes: magrittr (versija 2.0.3), openxlsx (4.2.4), pdftools (3.3.2) un rvest (1.0.2). Risinājums veidots ar R 4.1.3 versiju uz Windows 10 operētājsistēmas.

Vispirms ielādēsim izmantotās R bibliotēkas — ielādēju tikai magrittr, bet pārējo pakotņu funkcijas ir izmantotas kodā kopā ar pašas pakotnes nosaukumu, lai ir skaidrs kādas pakotnes funkcijas konkrētajā brīdī izmantotas. Nodefinēsim pamata mainīgos — Saeimas mājaslapa (page_link_main), mājaslapas apakškategorija, kurā atrodas deputātu atalgojuma dati pdf formātā (page_subdir).

# 1. Load libraries----
library(magrittr)

# 2. Define parameters----
page_link_main <- 'https://www.saeima.lv'
page_subdir <- '/lv/saeimas-struktura/deputatu-atalgojums'
pdf_file_dir_loc <- Sys.getenv('HOME') # Main folder with subfolder pdf_file_dirname
pdf_file_dirname <- 'Saeimas_atalgojums' # Subfolder name where all *.pdfs will be downloaded
overwrite_file <- FALSE # If there already exist .pdf into pdf_file_dirname with filename what you try to download
# then overwrite_file = FALSE means that file will not be overwritten

# Create subfolder if not exist
pdf_file_path <- file.path(pdf_file_dir_loc, pdf_file_dirname)

if (!dir.exists(pdf_file_path)) {
dir.create(pdf_file_path)
}

files_in_subfolder <- list.files(path = pdf_file_path, pattern = '\\.pdf$', recursive = TRUE)

Pieņemsim, ka lietotājs vēlas automatizēt datu izgūšanu, t.i., lejuplādēt pdf failus no mājaslapas un novietot tos direktorijas (pdf_file_dir_loc) apakšmapē ‘Saeimas_atalgojums’ (pdf_file_dirname), sadalot pdf failus pēc gadiem. Tāpēc ar funkciju dir.create() vispirms tiek izveidota apakšape, bet tikai tad, ja tā norādītajā mapē (kas definēta kā Sys.getenv(‘HOME’)) neatrodas.

Pirms failu lejupielādēšanas ar šo komandu nolasa kādi faili jau atrodas mapē ‘Saeimas_atalgojums’. Tā kā pdf faili tiks sadalīti pa gadiem, tad tiek meklēti visi pdf faili (pattern = ‘\\.pdf$’) arī ‘Saeimas_atalgojums’ apakšmapēs (recursive = TRUE):

files_in_subfolder <- list.files(path = pdf_file_path, pattern = '\\.pdf$', recursive = TRUE)

Protams, pirmajā reizē, šo kodu izpildot, nekādi faili nebūs. Taču, ja nākamreiz lietotājs vēlēsies lejuplādēt pdf failus, tad lietderīgi lejuplādēt tikai jaunos, tāpēc files_in_subfolder palīdz saprast kādi faili ir jau lejuplādēti.

PDF failu ielādēšana

Saeimas mājaslapā deputātu atalgojuma dati par konkrētu mēnesi ir saglabāti atsevišķā pdf failā. Tāpēc vispirms noskaidrosim visu pdf saišu nosaukumus — to panāk ar funkcijām rvest::read_html (nolasa mājaslapas saturu), rvest::html_elements (atrod <a> birkas, kurām kā saite ir nodefinēta pdf fails) un rvest::html_attr (atgriež saišu nosaukumus):

# 3. Get all pdfs from page----
## 3.1. get all necessary link names----
link_list <- rvest::read_html(x = paste0(page_link_main, page_subdir)) %>%
rvest::html_elements(css = "a[href$='.pdf']") %>%
rvest::html_attr(name = 'href')

## 3.2. Download and save PDF files----
for (i in link_list) {
pdf_filename <- basename(i)
pdf_year <- basename(dirname(i))
pdf_exist <- file.path(pdf_year, pdf_filename) %in% files_in_subfolder ||
pdf_filename %in% files_in_subfolder

# If such pdf file exist and overwrite_file = FALSE then skip this step
if (pdf_exist && !overwrite_file) next

if (!dir.exists(file.path(pdf_file_path, pdf_year))) {
dir.create(file.path(pdf_file_path, pdf_year))
}

download.file(url = paste0(page_link_main, i),
destfile = file.path(pdf_file_path, pdf_year, pdf_filename),
mode="wb") # mandatory
}

Saišu saraksts tiek ielikts objektā link_list (3.1.punkts), lai ar for cikla palīdzību (3.2.punkts) apstrādātu katru saiti atsevišķi — lejuplādēt pdf failu, ja konkrētais pdf fails neatrodas mapē ‘Saeimas_atalgojums’ (files_in_subfolder). Tā kā var būt viena mēneša, bet dažādu gadu dati, tad pdf faili salikti apakšmapēs pēc gadiem.

Iepriekšminēto pārbaudi veic šī koda daļa:

if (pdf_exist && !overwrite_file) next

overwrite_file dod lietotājam lejuplādēt failu vēlreiz — pat ja šāds faila nosaukums mapē jau atrodas. Lai pārrakstīšana strādātu, parametru definēšanas sekcijā (2.punkts) jānomaina objekta vērtība uz TRUE, resp., overwrite_file <- TRUE.

Pēc šo komandu izpildes visi *.pdf faili, kuri atradās lapā https://www.saeima.lv/lv/saeimas-struktura/deputatu-atalgojums (paste0(page_link_main, page_subdir)) ir lejuplādēti. Un var ķerties pie nākamā soļa — datu nolasīšanas no *.pdf failiem.

PDF failu datu nolasīšana

Vispirms nolasīsim visu pdf failu datus, kuri ir ielādēti ‘Saeimas_atlagojums’ mapē (files_in_subfolder) un tās apakšmapēs (recursive = TRUE). Parametrs full.names = TRUE ir nepieciešams, lai objektā tiktu saglabāts pilnais ceļš līdz pdf failam — tas ir nepieciešams funkcijai pdftools::pdf_text. Visu mēnešu atalgojuma dati tiks saglabāti vienā objektā — all_salary_data.

Ņemiet vērā, ka objektā files_in_subfolder būs visu pdf failu saraksts, kas atrodas pdf_file_path un tās apakšmapēs. Ja šajās mapēs būs kāds pdf fails, kas satur pavisam citu informāciju, tad kods pie nākamām darbībām apstāsies dēļ datu apstrādes kļūdas. Ja mapēs atalgojuma dati dublēsies (piem., viens un tas pats fails atrodas dažādās apakšmapēs), tad objektā all_salary_data šie dati arī tiks dublēti.

# 4. Read all pdfs----
files_in_subfolder <- list.files(pdf_file_path, full.names = TRUE,
pattern = '\\.pdf$', recursive = TRUE)

# Main dataframe where all salary data will be collected
all_salary_data <- NULL

for (i in files_in_subfolder) {
print(paste0("Process file: ", i))

pdf_result <- pdftools::pdf_text(pdf = i) %>%
strsplit(split = "\n") %>%
unlist() %>%
gsub(pattern = '\\s{2,}', replacement = ' ', .)

search_text <- 'Saeimas deput\u0101tiem izmaks\u0101tais atalgojums'
title_name <- grepl(pattern = search_text,
x = pdf_result, ignore.case = TRUE)
exclude_rows <- grepl(pattern = paste0('(', search_text, '|.*V\u0101rds Uzv\u0101rds.*)'),
x = pdf_result, ignore.case = TRUE)

name_surname <- trimws(sub(pattern = ' \\d+\\.\\d{0,2}', replacement = '', x = pdf_result))
amount <- as.double(sub(pattern = '\\D+', replacement = '', x = pdf_result))

page_data <- data.frame(orig = pdf_result,
title = trimws(pdf_result[title_name][1]),
name_surname = name_surname,
salary_eur = amount,
file = i,
subfolder = basename(dirname(i)),
filename = basename(i),
stringsAsFactors = FALSE)[!exclude_rows,] # Exclude title row

page_data <- page_data[nchar(page_data$orig) > 0, ]
all_salary_data <- rbind(page_data, all_salary_data)

}

Šādi izskatās 2023.gada maija pdf faila 3.lapas informācija. Šo piemēru izmantosim, lai nākamajos piemēros parādītu kā veidojas atsevišķu funkciju rezultāts:

2023. gada Saeimas atalgojuma faila 3.lapas informācija

Ar for cikla palīdzību katrā solī tiek apstrādāts konkrēts pdf fails. Cikla darbības ir sekojošas:

  • ar pdftools::pdf_text nolasa pdf faila saturu.
    pdftools pakotnei ir vēl citas pdf nolasīšanas funkcijas (pdf_data, pdf_ocr_data, pdf_ocr_text). Vienmēr pārbaudiet kādas no pdf faila nolasīšanas funkcijām jūsu projektiem der vislabāk. Saeimas atalgojuma gadījumā der pdf_text.
    Funkcija pdftools::pdf_text atgriež teksta vektoru (t.i., class = character), kur katra vektora elements ir konkrēta pdf lapa. Piemēram, ja Saeimas deputātu atalgojuma dati ir uz trim lapām, tad pdftools::pdf_text atgriezīs teksta vektoru ar trim elementiem — katrs elements atspoguļos konkrētas pdf lapas informāciju.
    Piemēram, zemāk attēlā ir parādīts pdftools::pdf_text rezultāts, ja konkrēta pdf dati tiktu ierakstīti text_from_pdf objektā. text_from_pdf[3] ir pdf faila 3.lapas saturs. Kā redzams, tad viss lapas saturs ir ierakstīts kā teksts un katra pdf failā esošā tabulas rinda ir atdalīta ar simbolu \n (new line) — šo simbolu tad var izmantot, lai katra deputāta atalgojuma datus iegūtu atsevišķā objekta elementā.
pdftools pakotnes pdf_text funkcijas rezultāts. text_from_pdf[3] — R objekta trešā elementa vērtības.
  • Ar strsplit(split = “\n”) katru pdf_text rezultātu sadala pēc simbola \n.
    Pēc strsplit funkcijas piemērošanas rezultāts izskatās sekojoši:
Rezultāts pēc funkcijas strsplit(split = “\n”) piemērošanas
  • strsplit katra vektora elementa rezultātu iekļauj sarakstā (list), tāpēc ar unlist() atbrīvosimies no saraksta un ar komandas gsub(pattern = ‘\\s{2,}’, replacement = ‘ ‘, .) palīdzību atbrīvosimies no liekām atstarpēm. Iepriekšējo darbību rezultāts tiek ierakstīts jaunā R objektā (pdf_result):
R objekta pdf_result rezultāts (pēdējo 12 elementu vērtības)

Funkcijas gsub parametra pattern vērtība ‘\\s{2,}’ ir Regex pieraksts, kas pielāgots R programmas vajadzībām, t.i., lai tekstā atrastu 2 un vairāk atstarpes, tad standarta pieraksts ir sekojošs: ‘\s{2,}’, taču R programmas gadījumā atpakaļvērstā slīpsvītra (\) ir jādefinē divas reizes, tāpēc rezultāts ir šāds: ‘\\s{2,}’.

  • no pdf_result objekta rezultāta izgūsim vajadzīgo informāciju: pdf faila virsraksts (title_name), Saeimas deputāta vārds un uzvārds (name_surname), atlagojuma lielums (amount).
  • Visi iepriekš nosauktie elementi apkopoti objektā page_data. Papildus pievienota oriģinālā teksta vērtība (orig), faila nosaukums (filename) un faila atrašanās vieta (file un subfolder).
  • Visu ciklu dati apkopoti objektā all_salary_data.

PDF datu saglabāšana

Pēc visu pdf failu datu nolasīšanas (kas uzkrāti objektā all_salary_data), atliek rezultātu saglabāt sev vēlamajā formātā. Piemērā dati saglabāti Excel formātā. Pēc vienkāršākās metodes pietiek izpildīt funkciju openxlsx::write.xlsx, bet šajā piemērā notiek vēl saglabājamā faila nosaukuma pārbaude. Ja Excel faila nosaukums (excel_fullname) jau eksistē mapē pdf_file_path, tad faila nosaukums tiek pārsaukts — galā pievienojot faila kārtas numuru (1 vai 2, vai 3 utt. — atkarībā cik daudz failu ir saglabāti ar šo nosaukumu):

# 5. Save result as xlsx----
excel_filename <- 'Saeimas_atalgojums'
excel_fullname <- paste0(excel_filename, '.xlsx')
i <- 0
while (TRUE) {
if (excel_fullname %in% list.files(pdf_file_path, pattern = '\\.xlsx$')) {
i <- i + 1
excel_fullname <- sprintf(paste0(excel_filename, '_%d.xlsx'), i)
} else {
openxlsx::write.xlsx(x = all_salary_data,
file = file.path(pdf_file_path, excel_fullname))
break
}
}

Tas ir arī viss. Esam ieguvuši Saeimas deputātu atalgojuma sarakstu Excel formātā.

Viss kods ir pieejams arī GitHub.

--

--

No responses yet