PDF:如何将一列列表转换为多列数据框? - 组内子组中的人员列表到多列

PDF: how to convert one column lists to multiple column data frame? - lists of people in subgroups inside groups to multiple columns

我有将近 15 个 PDF,其中包含人员名单。此 PDF 只有一个列宽,因此它是一个纯列表。但在某种程度上,这些列表是嵌套的(子组内的子组内的子组......)。除了每个人在列表中的第一个数字(这对我的分析很重要)和类似的订单信息外,没有任何数字数据。

我需要从 PDF 中提取此列表并将它们转换为常规数据框。

这是一个 PDF 结构的示例:

TERRITORY ONE
1. GROUP ONE
1. Name Surname
2. Name Surname
3. Name Surname
4. Name Surname
2. GROUP TWO
1. Name Surname
2. Name Surname
3. Name Surname
4. Name Surname
TERRITORY TWO
(...)

这是第一个 PDF:http://bocyl.jcyl.es/boletines/1983/04/02/pdf/BOCYL-D-02041983-1.pdf


!!!我发现这些文件也存储在网页中,所以HTML格式:http://bocyl.jcyl.es/html/1983/04/02/html/BOCYL-D-02041983-1.do 也许从他们那里获取内容而不是从 PDF 中获取内容更容易?


如您所想(领土二、三、四...,以及随后的子组一、二、三、四...等)。每个 PDF 多达近 600 行,在最新的 PDF 中更多。

我需要创建一个遵循此示例结构的数据框:

   PERSON    |    TERRITORY  |  GROUP  | POSITION IN LIST
Name Surname | TERRITORY ONE | GROUP 1 |         1
(...)
Name Surname | TERRITORY ONE | GROUP 2 |         4
(...)
Name Surname | TERRITORY TWO | GROUP 1 |         3

一排应该是一个人

POSITION IN LIST应该是指Name Surname这个人在给定年份出现的顺序(每个PDF都是一年),在他的TERRITORY,在他的[=15] =].

把它想象成一个排名,其中很重要的是人的顺序。 PDF1(第 1 年)的人很少会再次出现在 PDF2(第 2 年),然后出现在 PDF3(第 3 年)等等。所以,所有这一切背后的 objective 就是要知道有多少谁在这个列表中年复一年地重复。

还有,知道每年重复的那个人的位置,画出这个人的进化,或者知道这个人在X年后是否消失等,对分析来说很重要。

PS: 请原谅我的英语,不是我的第一语言:(

这不是一个完整的答案,但它可以帮助您解决一些问题。一步步过一遍,看看每一行会发生什么。您将能够使用 purrr::map 将这些步骤应用于 pdf 列表等。我刚刚在此处 (text[[1]]) 获取了 first/only pdf 的文本,以便保持东西比较简单。

我想进一步整理和精简代码,并添加更多评论,但我想在准备好后尽快回复。

很遗憾,由于阅读 PDF 和创建 dput 文本块时出现问题,我无法提供 reprex,但您应该能够修改以下内容以帮助您入门:

library(here)
library(pdftools)
library(purrr)
library(stringr)

# download.file(
#   url = "http://bocyl.jcyl.es/boletines/1983/04/02/pdf/BOCYL-D-02041983-1.pdf",
#   destfile = here("pdfs", "BOCYL-D-02041983-1.pdf"),
#   mode = "wb"
# )

pdfs <- list.files(path = here("pdfs"), pattern = "pdf$")
text <- map(pdfs, ~ pdftools::pdf_text(here("pdfs", .)))

# be better to combine these into a single piped function (mappable)
# we need to combine the text into a single block before editing and splitting
text1_combined <- str_c(text[[1]], collapse = "")
text1_split <- str_split(text1_combined, "JUNTA")
# remove header text
text1_split <- text1_split[[1]] %>% tail(-1)
# repair text lost from str_split
text1_list <- map(text1_split, ~ paste0("JUNTA", .))

# extract territory name and use it to name each sub-list
text1_list <- text1_list %>% set_names(., nm = str_extract(., "^([:upper:]|\s)+(?=\r)"))
text1_trimmed <- map(text1_list, ~ str_replace(., "^([:upper:]|\s)+\r\n", ""))

text1_trim_tidy <- map(text1_trimmed, ~ str_replace_all(., "\r\n(?=[:digit:])", ","))
text1_trim_tidy <- map(text1_trim_tidy, ~ str_replace_all(., "\r\n", " "))
text1_trim_tidy <- map(text1_trim_tidy, ~ str_replace_all(., "\s+$", ""))

text1_by_party <- map(text1_trim_tidy, ~ str_split(., ",(?=[:digit:]+\.\s[:upper:]{2,})"))


# clear up intermediate objects
# rm(text1_combined)
# rm(text1_split)
# rm(text1_list)
# rm(text1_trimmed)
# rm(text1_trim_tidy)

希望我能在整理完代码后编辑它或添加另一个答案。我做了一个 github 回购 here 以供进一步参考。

这是一个更完整的答案,基于抓取网页而不是 PDF,并且仍然只使用一个来源。因此,尚未测试要抓取的网页不止一个。如果您有其他网页的源数据,请将它们添加到下面代码顶部的向量中。

我可以把它留给你@pbstckvrflw!

这是一项繁重的工作,但幸运的是,我很喜欢做这件事,并且边走边学。

但是请注意这种规模的任务通常不适合解决 SO 问题,最好自己尝试解决问题,然后非常认真地提问关于您发现的问题的具体问题。

我希望你能仔细阅读我写的代码,并尝试理解每一步发生了什么。您可能需要了解的主要内容是 map 以及它如何将函数应用于列表中的每个项目。我在这里广泛使用 map 因为我们正在使用嵌套列表。还有一些很好的正则表达式。

它远非完美的代码,可能存在错误或效率低下。如果其中一些分解成可重复的功能会更好。而且它生成了几个中间对象,有点乱,但就是这样。部分代码是为了清晰起见,部分是因为我没有更多时间将这些块集成到更流畅的工作流程中,而不会有太大的破坏风险。

library(rvest)
library(stringr)
library(purrr)
library(dplyr)
library(tibble)
library(conflicted)
conflict_prefer("pluck", "purrr")

# you should add any further URLs to this vector
urls <- c("http://bocyl.jcyl.es/html/1983/04/02/html/BOCYL-D-02041983-1.do")

# scrape text from the relevant part of the webpage
# (assume that any additional URLs have the same structure)
text <- urls %>%
  map(., ~ {xml2::read_html(.) %>%
      rvest::html_nodes("#presentDocumentos p:not([class])") %>% 
      html_text})

# extract a manageable name from the URL and use it to name each text
names(text) <- urls %>% 
  str_extract_all(., pattern = "(?<=/)BOCYL.*(?=\.do$)")

# do any manual fixes for errors in source data
text1 <- text %>% 
  map(., ~
  str_replace_all(., "PARTIDO COMUNISTA DE ESPAÑA PARTIDO COMUNISTA DE CASTILLA- LEON", "2. PARTIDO COMUNISTA DE ESPAÑA PARTIDO COMUNISTA DE CASTILLA- LEON"))

text2 <- text1 %>%
  map(., ~ 
        str_replace_all(., "(\.)*(\s)*$", "") %>% 
        str_replace_all(., "(\s)+", " ") %>% 
        str_replace_all(., "^Suplente.*", "") %>% 
        str_c(., collapse = ";") %>% 
        str_split(., pattern = "JUNTA ELECTORAL DE ") %>% 
        map(., ~ tail(., -1) %>% 
              str_split(.,
                        pattern = ";(?=\d{1,2}\.\s([:upper:]|\s){2,})") %>% 
              set_names(str_to_title(map(., 1))) %>% 
              map(., ~ tail(., -1))
        )
  )


text3 <- text2 %>% 
  map(., ~
        map(., ~ 
              map(., ~ str_split(., pattern = ";(?=\d{1,2}\.\s)") %>% 
                    set_names(map(., 1) %>% 
                                str_extract(., pattern = "(?<=\d{1,2}\.\s)[:upper:].*")) %>% 
                    map(., ~ tail(., -1) %>% 
                          enframe(., name = "list_position", value = "person_name") %>% 
                          mutate_at(
                            vars("person_name"),
                            ~ str_extract_all(.,
                                              pattern = "(?<=\d{1,2}\.\s)[:alpha:]+.*"))))))

text4 <- text3 %>% 
  map(., ~
        map(., ~ 
              map(., ~
                    map_df(., c, .id = "political_group"))))

text5 <- text4 %>% 
  map(., ~
        map(., ~ 
              map_df(., c, .id = "territory")))

# EXAMPLE
# To look at just the data frame produced from the first web page supplied
# (with columns rearranged as desired):
data_frame1 <- text5 %>% 
  pluck(1, 1) %>% 
  select(person_name, everything())
data_frame1

我已将最新代码推送到the GitHub repo I made。 如果您对问题的这个答案感到满意,请勾选此为已接受的答案。