7 Commits
14 changed files with 255 additions and 24 deletions
+2
View File
@@ -0,0 +1,2 @@
^doc$
^Meta$
+2
View File
@@ -1 +1,3 @@
*.xml *.xml
/doc/
/Meta/
+3 -1
View File
@@ -19,8 +19,10 @@ Imports:
pbapply, pbapply,
rvest, rvest,
stringr, stringr,
tibble,
xml2 xml2
Suggests: Suggests:
rmarkdown, rmarkdown,
knitr knitr,
ggplot2
VignetteBuilder: knitr VignetteBuilder: knitr
+3
View File
@@ -1,7 +1,10 @@
# Generated by roxygen2: do not edit by hand # Generated by roxygen2: do not edit by hand
export(read_all)
export(repair)
import(dplyr) import(dplyr)
import(pbapply) import(pbapply)
import(stringr) import(stringr)
import(tibble) import(tibble)
import(utils)
import(xml2) import(xml2)
+34 -1
View File
@@ -30,7 +30,31 @@ fetch_batch <- function(offset, download_dir) {
# TODO: error handling # TODO: error handling
# - what if: page not reachable # - what if: page not reachable
# - wrong format, etc. # - wrong format, etc.
fetch_all <- function(download_dir="records/") {
#' Download available records
#'
#' This fetches all available records of the 19th legislative period of the german Bundestag.
#'
#' @param download_dir character
#'
#' @export
fetch_all <- function(download_dir="records/", create=FALSE) {
# check if download_dir path is a directory path
if (str_sub(download_dir, -1) != .Platform$file.sep)
download_dir <- str_c(download_dir, .Platform$file.sep)
# check if download_dir exists
if(file.access(download_dir, mode=0) == -1) {
if (create) {
tryCatch(dir.create(download_dir),
error = stop_dir_not_creatable,
warning = stop_dir_not_creatable)
} else {
stop("Directory does not exist. Use create = TRUE if you wish to create the directory.")
}
} else if (file.access(download_dir, mode=2) == -1) {
stop("Directory exists, but is not writeable.")
}
cat("Fetching all available records from bundestag.de. This may take a while ...\n") cat("Fetching all available records from bundestag.de. This may take a while ...\n")
# create progress bar # create progress bar
@@ -38,9 +62,18 @@ fetch_all <- function(download_dir="records/") {
progress <<- 0 progress <<- 0
# close progress bar on exit (also on error) # close progress bar on exit (also on error)
on.exit({close(pb); cat("Done.\n")}) on.exit({close(pb); cat("Done.\n")})
# fetch batch by batch # fetch batch by batch
offset <- 0 offset <- 0
while(fetch_batch(offset, download_dir)) offset <- offset + 10 while(fetch_batch(offset, download_dir)) offset <- offset + 10
# if successful, set progressbar to 100% # if successful, set progressbar to 100%
setTimerProgressBar(pb, 250) setTimerProgressBar(pb, 250)
} }
stop_dir_not_creatable <- function(cond) {
# currently this has call: dir.create(download_dir)
# do we want to change this to fetch_all(...) ?
cond$message <- "Directory does not exist and can't be created. Probably because the path is not writeable."
stop(cond)
}
+1
View File
@@ -5,6 +5,7 @@
#' @import pbapply #' @import pbapply
#' @import stringr #' @import stringr
#' @import xml2 #' @import xml2
#' @import utils
#' @keywords internal #' @keywords internal
"_PACKAGE" "_PACKAGE"
+1
View File
@@ -1,4 +1,5 @@
`%$%` <- function(f, x) f(x) `%$%` <- function(f, x) f(x)
`%.%` <- function(f, g) function(...) f(g(...)) `%.%` <- function(f, g) function(...) f(g(...))
flip <- function(f) function(x, y) f(y, x)
clear_na <- function(xs) xs[!is.na(xs)] clear_na <- function(xs) xs[!is.na(xs)]
+69 -8
View File
@@ -1,5 +1,13 @@
# for usage see the example at the end # for usage see the example at the end
#' Parse xml records
#'
#' Creates a list of tibbles containing relevant information from all records
#' stored in the input directory.
#'
#' @param path character
#'
#' @export
read_all <- function(path="records/") { read_all <- function(path="records/") {
cat("Reading all records from", path, "\n") cat("Reading all records from", path, "\n")
available_protocols <- list.files(path) available_protocols <- list.files(path)
@@ -20,7 +28,14 @@ read_all <- function(path="records/") {
distinct() -> distinct() ->
talks talks
list(redner = redner, reden = reden, talks = talks) lapply(res, `[[`, "comments") %>%
bind_rows() %>%
distinct() ->
comments
if (length(available_protocols) == 0)
warning("The given directory is empty or does not exist.")
list(redner = redner, reden = reden, talks = talks, comments = comments)
} }
# this reads all currently parseable data from one xml # this reads all currently parseable data from one xml
@@ -42,7 +57,7 @@ read_one <- function(name, path) {
parse_redenliste() -> parse_redenliste() ->
res res
list(redner = redner, reden = res$reden, talks = res$talks) list(redner = redner, reden = res$reden, talks = res$talks, comments = res$comments)
} }
xml_get <- function(node, name) { xml_get <- function(node, name) {
@@ -78,23 +93,39 @@ parse_rede <- function(rede_xml) {
principal_redner <- NA_character_ principal_redner <- NA_character_
cur_content <- "" cur_content <- ""
reden <- list() reden <- list()
comments <- list()
for (node in cs) { for (node in cs) {
if (xml_name(node) == "p") { if (xml_name(node) == "p" || xml_name(node) == "name") {
klasse <- xml_attr(node, "klasse") klasse <- xml_attr(node, "klasse")
if (!is.na(klasse) && klasse == "redner") { if ((!is.na(klasse) && klasse == "redner") || xml_name(node) == "name") {
if (!is.na(cur_redner)) { if (!is.na(cur_redner)) {
rede <- c(rede_id = rede_id, rede <- c(rede_id = rede_id,
redner = cur_redner, redner = cur_redner,
content = cur_content) content = cur_content)
reden <- c(reden, list(rede)) reden <- c(reden, list(rede))
cur_content <- "" cur_content <- ""
} else { }
if (is.na(principal_redner) && xml_name(node) != "name") {
principal_redner <- xml_child(node) %>% xml_attr("id") principal_redner <- xml_child(node) %>% xml_attr("id")
} }
if (xml_name(node) == "name") {
cur_redner <- "BTP"
} else {
cur_redner <- xml_child(node) %>% xml_attr("id") cur_redner <- xml_child(node) %>% xml_attr("id")
}
} else { } else {
cur_content <- paste0(cur_content, xml_text(node), sep="\n") cur_content <- paste0(cur_content, xml_text(node), sep="\n")
} }
} else if (xml_name(node) == "kommentar") {
# comments are of the form
# <kommentar>(blabla [Fraktion] blabla liasdf bla)</kommentar>
xml_text(node) %>%
str_sub(2, -2) %>%
str_split("") %>%
`[[`(1) %>%
lapply(parse_comment, rede_id = rede_id, on_redner = cur_redner) ->
cs
comments <- c(comments, cs)
} }
} }
rede <- c(rede_id = rede_id, rede <- c(rede_id = rede_id,
@@ -102,7 +133,30 @@ parse_rede <- function(rede_xml) {
content = cur_content) content = cur_content)
reden <- c(reden, list(rede)) reden <- c(reden, list(rede))
list(rede = c(id = rede_id, redner = principal_redner), list(rede = c(id = rede_id, redner = principal_redner),
parts = reden) parts = reden,
comments = comments)
}
fraktionspattern <- "BÜNDNIS(SES)?\\W*90/DIE\\W*GRÜNEN|CDU/CSU|AfD|SPD|DIE LINKE|FDP|LINKEN"
fraktionsnames <- c("BÜNDNIS 90/DIE GRÜNEN", "CDU/CSU", "AfD", "SPD", "DIE LINKE", "FDP")
parse_comment <- function(comment, rede_id, on_redner) {
base <- c(rede_id = rede_id, on_redner = on_redner)
str_extract_all(comment, fraktionspattern) %>%
`[[`(1) %>%
sapply(partial(flip(head), 1) %.% agrep, x=fraktionsnames, max=0.2, value=T) %>%
str_c(collapse=",") ->
by
# classify comment
# TODO:
# - actually separate content properly
# - differentiate between [AfD] and AfD in by
if(str_detect(comment, "Beifall")) {
c(base, type = "applause", fraktion = by, kommentator = NA_character_, content = comment)
} else {
ps <- str_match(comment, "(.*) \\[(.*?)\\]: (.*)")[1,]
c(base, type = "comment", fraktion = ps[3], kommentator = ps[2], content = ps[4])
}
} }
# creates a tibble of reden and a tibble of talks from a list of xml nodes representing reden # creates a tibble of reden and a tibble of talks from a list of xml nodes representing reden
@@ -110,10 +164,17 @@ parse_redenliste <- function(redenliste_xml) {
d <- sapply(redenliste_xml, parse_rede) d <- sapply(redenliste_xml, parse_rede)
reden <- simplify2array(d["rede", ]) reden <- simplify2array(d["rede", ])
parts <- simplify2array %$% unlist(d["parts", ], recursive=FALSE) parts <- simplify2array %$% unlist(d["parts", ], recursive=FALSE)
comments <- simplify2array %$% unlist(d["comments", ], recursive=FALSE)
list(reden = tibble(id = reden["id",], redner = reden["redner",]), list(reden = tibble(id = reden["id",], redner = reden["redner",]),
talks = tibble(rede_id = parts["rede_id", ], talks = tibble(rede_id = parts["rede_id", ],
redner = parts["redner", ], redner = parts["redner", ],
content = parts["content", ])) content = parts["content", ]),
comments = tibble(rede_id = comments["rede_id",],
on_redner = comments["on_redner",],
type = comments["type",],
fraktion = comments["fraktion",],
kommentator = comments["kommentator",],
content = comments["content", ]))
} }
# create a tibble of redner from a list of xml nodes representing redner # create a tibble of redner from a list of xml nodes representing redner
@@ -132,7 +193,7 @@ parse_rednerliste <- function(rednerliste_xml) {
# EXAMPLE USE # EXAMPLE USE
# make sure data ist downloaded via fetch.R # make sure data ist downloaded via fetch.R
# res <- read_one("19126-data.xml") # res <- read_one("records/19126-data.xml")
# #
# res$redner # res$redner
# res$reden # res$reden
+48 -5
View File
@@ -23,25 +23,68 @@ collect_unique <- function(xs) xs %>% clear_na() %>% unique() %>% str_c(collapse
# expects a tibble of redner and repairs # expects a tibble of redner and repairs
repair_redner <- function(redner) { repair_redner <- function(redner) {
redner %>% mutate(fraktion = Vectorize(repair_fraktion)(fraktion)) %>% # fix fraktion if (nrow(redner) == 0) return(redner)
group_by(id, vorname, nachname) %>% redner %>%
summarize(fraktion = collect_unique(fraktion), filter(id != "10000") %>% # invalid id's
mutate(fraktion = Vectorize(repair_fraktion)(fraktion)) %>% # fix fraktion
group_by(id) %>%
summarize(vorname = head(vorname, 1),
nachname = head(nachname, 1),
fraktion = collect_unique(fraktion),
titel = longest_titel(titel), titel = longest_titel(titel),
rolle_kurz = collect_unique(str_squish(rolle_kurz)), rolle_kurz = collect_unique(str_squish(rolle_kurz)),
rolle_lang = collect_unique(str_squish(rolle_lang))) rolle_lang = collect_unique(str_squish(rolle_lang))) %>%
ungroup() #%>%
# arrange(id) %>%
# distinct(vorname, nachname, fraktion, titel)
} }
repair_reden <- function(reden) { repair_reden <- function(reden) {
if (nrow(reden) == 0) return(reden)
# TODO: fill with content # TODO: fill with content
reden reden
} }
repair_talks <- function(talks) { repair_talks <- function(talks) {
if (nrow(talks) == 0) return(talks)
# TODO: fill with content # TODO: fill with content
talks talks
} }
# repairs all tables # tries to find the correct redner id given a name
# this is sufficient since every prename lastname combination in the bundestag is
# unique (luckily :D)
# returns a lookup table
lookup_redner <- function(comments, redner) {
tobereplaced <- "[-–—‑­­-­­­ ]"
redner %>%
unite(name, vorname, nachname, sep=".*") %>%
mutate(name = str_replace_all(name, tobereplaced, ".*")) ->
rs
find_match <- function(komm) {
if (komm == "") return (NA_character_)
# I tried with agrep (levensthein distance) but results are better that way
matches <- str_which(komm, rs$name)
if (length(matches) == 0) return(NA_character_)
rs[head(matches, 1), ]$id
}
comments %>%
distinct(kommentator) %>%
mutate(redner = Vectorize(find_match)(str_replace_all(kommentator, tobereplaced, "")))
}
repair_comments <- function(comments, redner) {
# try to find a redner id for each actual comment
comments %>%
filter(!is.na(kommentator)) %>%
lookup_redner(redner) %>%
left_join(comments, ., by="kommentator") %>%
select(-kommentator)
}
#' Repair parsed tables
#'
#' @export
repair <- function(parse_output) { repair <- function(parse_output) {
list(redner = repair_redner(parse_output$redner), list(redner = repair_redner(parse_output$redner),
reden = repair_reden(parse_output$reden), reden = repair_reden(parse_output$reden),
+15
View File
@@ -0,0 +1,15 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/parse.R
\name{read_all}
\alias{read_all}
\title{Parse xml records}
\usage{
read_all(path = "records/")
}
\arguments{
\item{path}{character}
}
\description{
Creates a list of tibbles containing relevant information from all records
stored in the input directory.
}
+11
View File
@@ -0,0 +1,11 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/repair.R
\name{repair}
\alias{repair}
\title{Repairs parsed tables}
\usage{
repair(parse_output)
}
\description{
Repairs parsed tables
}
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
\documentclass{article}
\begin{document}
\section*{Was wurde bisher gemacht?}
\begin{itemize}
\item Website scrapen
\item Protokolle fetchen
\item Protokolle parsen
\begin{itemize}
\item Redner ermitteln
\item Redebeiträge extrahieren
\item Ordnungskommentare filtern
\item Kommentare extrahieren
\end{itemize}
\end{itemize}
\section*{Wie ist der Zustand aktuell?}
Protokolle sind heruntergeladen und geparsed in einer großen Tabelle.
Es gibt noch ein paar Issues, aber das meiste was wir haben funktioniert auch soweit.
\section*{Was muss noch gemacht werden?}
\begin{itemize}
\item Parsen verbessern (muss nicht alles gemacht werden, aber wäre vielleicht schön)
\begin{itemize}
\item Kommentare in Kategorien einteilen, z.B. Kategorie Beifall mit Liste der applaudierenden Parteien
\item Metadaten vom Anfang extrahieren
\item Fehler bereinigen
\end{itemize}
\item Daten auf bestimmte Fragestellungen untersuchen
\begin{itemize}
\item Wer redet am häufigsten / längsten?
\item Was ist typisches Vokabular für einzelne Fraktionen?
\item Welche Fraktion gibt / bekommt den meisten Beifall / die meisten Zwischenrufe?
\item Ab wann wird Pandemie-Vokabular (Virus, Corona, Maske, $\dots$ ) genutzt?
\item Hat die Pandemie andere Themen wie Klimawandel verdrängt?
\end{itemize}
\item Ergebnisse visualisieren
\end{itemize}
\section*{Wie sieht der Plan für die nächsten Wochen aus?}
\begin{itemize}
\item[-24.07.] Klausurenphase
\item[25.07.-31.07.] Datenanalyse
\item[01.08.-07.08.] Visualisierung, Bugfixes und Improvements
\item[08.08.-11.08.] Präsentation machen
\end{itemize}
\end{document}
+20 -7
View File
@@ -14,23 +14,36 @@ knitr::opts_chunk$set(
) )
``` ```
```{r setup}
library(hateimparlament)
library(dplyr)
library(ggplot2)
```
## Preparation of data
First, you need to download all records of the current legislative period.
```r ```r
read_all() %>% repair() -> res fetch_all("../records/") # path to directory where records should be stored
```
Second, those `.xml` files, need to be parsed into `R` `tibbles`. This is accomplished by:
```{r}
read_all("../records/") %>% repair() -> res
reden <- res$reden reden <- res$reden
redner <- res$redner redner <- res$redner
talks <- res$talks talks <- res$talks
```
We also used `repair` to fix a bunch of formatting issues in the records and unpacked
the result into more descriptive variables.
# first tries ## Analysis
Now we can start analysing our parsed dataset, e.g. find out which party gives the most talks:
```{r}
left_join(reden, redner, by=c("redner" = "id")) %>% left_join(reden, redner, by=c("redner" = "id")) %>%
group_by(fraktion) %>% group_by(fraktion) %>%
summarize(n = n()) %>% summarize(n = n()) %>%
ggplot(aes(x = fraktion, y = n)) + ggplot(aes(x = fraktion, y = n)) +
geom_bar(stat = "identity") geom_bar(stat = "identity")
``` ```
```{r setup}
library(hateimparlament)
```