Plenarprotokolle 19.Legislaturperiode, XML

17.05.2019 - Valentin Gold - Reading time ~66 Minutes


Plenarprotokolle 19.Legislaturperiode

Hier ist mein zweiter Versuch, die Plenarprotokolle korrekt einzulesen, diesmal mit Rückgriff auf eines der dafür vorgesehenen xml-Paketen. Das klappt wesentlich besser, aber auch hier muss nachkorrigiert werden. Die Strukturdefinition finden Sie unter https://www.bundestag.de/services/opendata. Hier können Sie prinzipiell die Variablen nachschlagen, die hier mit diesem File extrahiert werden.

Den fertigen Datensatz können Sie hier herunterladen.

1. Pakete laden

#Pakete laden
library(tibble)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(magrittr)
library(stringr)
library(tidyr)
## 
## Attaching package: 'tidyr'
## The following object is masked from 'package:magrittr':
## 
##     extract
library(tidyr)
library(XML)

2. Dokumente aufteilen

Der erste Schritt besteht darin, die Dokumente in Vorspann und Reden aufzuteilen. Der Beginn des Vorspanns ist mit dem Tag “<vorspann>”, das Ende mit dem Tag “</vorspann>” gekennzeichnet. Ähnliches gilt für den Sitzungsverlauf mit dem Tag “<sitzungsverlauf>” bzw. "</sitzungsverlauf>.

Zusätzlich ändere ich gleich den Tag für die Kommentare von “<kommentar>” in “<p klasse=”kommentar“>” und entsprechend das Schlusstag “</kommentar>” in “</p>”. Das erleichtert mir das Einlesen der Kommentare als Redebeitrag; mit der Variable “klasse” und dem Wert “kommentar”. Ebenso manipuliere ich Beträge des Präsidiums und füge hier der Variable “klasse” den Wert “präsidium” hinzu.

#Auflistung aller Files
files <- list.files("../../../daten/bundestag19/", 
                    pattern = "\\.xml")
files

#Ordner erstellen
dir.create(file.path("../../../daten/bundestag19", "vorspann"))
dir.create(file.path("../../../daten/bundestag19", "sitzung"))

#Test
f <- files[6]

for (f in files){
  #Generiere Pfad zur Datei
  file_path <- str_c("../../../daten/bundestag19/", f)
  
  #Einlesen mit readLines
  source_file <- readLines(file_path, encoding = "UTF-8")
  
  #in Tibble konvertieren
  source_file <- tibble::enframe(source_file, name=NULL, value="original")
  
  #Row ID erstellen und Tag erkennen
  source_file <- source_file %>% 
    mutate(row_id = row_number()) %>% 
    mutate(vorspann_beginn = case_when(
      str_detect(original, '\\<vorspann\\>') ~ row_id, 
      TRUE ~ NA_integer_
    )) %>% 
    mutate(vorspann_ende = case_when(
      str_detect(original, '\\</vorspann\\>') ~ row_id, 
      TRUE ~ NA_integer_
    )) %>% 
    mutate(sitzungsverlauf_beginn = case_when(
      str_detect(original, '\\<sitzungsverlauf\\>') ~ row_id, 
      TRUE ~ NA_integer_
    )) %>% 
    mutate(sitzungsverlauf_ende = case_when(
      str_detect(original, '\\</sitzungsverlauf\\>') ~ row_id, 
      TRUE ~ NA_integer_
    )) %>% 
    fill(vorspann_beginn, vorspann_ende, sitzungsverlauf_beginn, sitzungsverlauf_ende, .direction=c("down")) %>% 
    fill(vorspann_beginn, vorspann_ende, sitzungsverlauf_beginn, sitzungsverlauf_ende, .direction=c("up"))

    source_file %>% 
      select(row_id, vorspann_beginn, vorspann_ende, sitzungsverlauf_beginn, sitzungsverlauf_ende) %>% 
      head(20)
    
    #Kommentar Tag ändern
    source_file <- source_file %>% 
      mutate(original = str_replace_all(original, '\\<kommentar\\>', '\\<p klasse=\\"kommentar\\"\\>'), 
             original = str_replace_all(original, '\\</kommentar\\>', '\\</p\\>'))
    source_file %>% 
      mutate(total_kommentar = str_count(original, '\\<kommentar\\>')) %>% 
      summarise(total_kommentar = sum(total_kommentar))
    
    #Manipuliere Beiträge des Präsidiums (markiert mit den Tags <name>...</name>)
    source_file <- source_file %>%
      mutate(original = 
        str_replace_all(original, '\\<name\\>([A-ZÄÖÜa-zäöüß].*?)\\</name\\>', '<p klasse="präsidium">\\1</p>') 
      )
    
    #Exporieren
    path_vorspann <- str_c("../../../daten/bundestag19/vorspann/", f)
    vorspann <- source_file %>% 
      filter(row_id >= vorspann_beginn & row_id <= vorspann_ende)
    writeLines(vorspann %>% pull(original), path_vorspann)
    
    path_sitzung <- str_c("../../../daten/bundestag19/sitzung/", f)
    sitzung <- source_file %>% 
      filter(row_id >= sitzungsverlauf_beginn & row_id <= sitzungsverlauf_ende)
    writeLines(sitzung %>% pull(original), path_sitzung)
    
}

3. Sitzungen mit xml-Paket einlesen

Ich lese insgesamt 4 verschiedene Typen von Daten aus den xml-Dateien aus und füge sie zu einem Datensatz zusammen:

  1. Id-Variablen des Dokuments, d.h. id, name, typ, uhrzeit
  2. Alle Reden, d.h. Reden vor Eintritt in die Tagesordnung (zumeist von der Bundestagspräsidentin oder dem Bundestagspräsident) und Reden zur Tagesordnung (zumeist von Parlamentarier*innen)
  3. Id-Variable der Sprecher*innen (dieser Datensatz wird später wieder den anderen beiden Datensätzen zugespielt).
  4. Variablen aus dem Vorspann, z.B. Datum der Sitzung (auch dieser Datensatz wird wieder den anderen Datensätzen zugespielt).
#Auflistung aller Files
files <- list.files("../../../daten/bundestag19/sitzung/", 
                              pattern = "\\.xml")
files

#leeres Objekt
lp19_xml <- NULL

#Test
f <- files[1]

for (f in files){
  #Generiere Pfad zur Datei
  path_sitzung <- str_c("../../../daten/bundestag19/sitzung/", f)
  
  #Einlesen mit xml-Paket
  doc <- xmlParse(path_sitzung, encoding = "UTF-8") 
  
  #Extrahiere Nodes
  #nodes_sitzungsbeginn <- getNodeSet(doc, "//*/sitzungsbeginn")
  #nodes_sitzungsbeginn[[1]]
  
  #nodes_tagesordnungspunkt <- getNodeSet(doc, "//*/tagesordnungspunkt")
  #length(nodes_tagesordnungspunkt)
  
  #--------a. Id-Variablen des Dokuments--------------------------------------#
  uhrzeit_beginn <- as.data.frame(xpathSApply(doc, "//sitzungsbeginn", xmlAttrs))
  dim(uhrzeit_beginn)
  if(nrow(uhrzeit_beginn)==0){uhrzeit_beginn <- tibble("NA")}
  names(uhrzeit_beginn) <- "uhrzeit_beginn"
  head(uhrzeit_beginn)
  
  uhrzeit_ende <- as.data.frame(xpathSApply(doc, "//sitzungsende", xmlAttrs))
  dim(uhrzeit_ende)
  if(nrow(uhrzeit_ende)==0){uhrzeit_ende <- tibble("NA")}
  names(uhrzeit_ende) <- "uhrzeit_ende"
  head(uhrzeit_ende)
  
  document_ids <- as.data.frame(t(xpathSApply(doc, "//sitzungsbeginn/a", xmlAttrs)))
  dim(document_ids)
  names(document_ids) <- c("doc_id", "doc_name", "doc_typ")
  document_ids <- document_ids %>% 
    distinct()
  head(document_ids)
  
  #bind data
  doc_ids <- bind_cols(document_ids, uhrzeit_beginn, uhrzeit_ende)
  dim(doc_ids)
  head(doc_ids)
  
  
  #--------b. Alle Reden---------------------------------------#
  #Extrahiere alle Reden, gekennzeichnet durch den Tag <p>...</p>
  reden <- as.data.frame(xpathSApply(doc, "//p", xmlValue))
  dim(reden)
  names(reden) <- "text"
  head(reden)
  
  klasse <- as.data.frame(xpathSApply(doc, "//p", function(x) xmlAttrs(x, 'klasse')))
  dim(klasse)
  names(klasse) <- "klasse"
  head(klasse)
  
  redeid <- as.data.frame(xpathSApply(doc, "//p", function(x) xmlAttrs(xmlParent(x))))
  dim(redeid)
  names(redeid) <- "redeid"
  head(redeid)
  
  top <- xpathSApply(doc, "//p", function(x) xmlAttrs(xmlParent(xmlParent(x))))
  length(top)
  #keep NULL values
  top <- lapply(top, function(x) if(is.null(x)) data.frame(top = NA) else x)
  top <- do.call(rbind.data.frame, top)
  dim(top)
  names(top) <- "top"
  head(top)
  
  #bind data
  reden <- bind_cols(redeid, 
                     klasse, 
                     top, 
                     reden)
  dim(reden)
  head(reden)
  
  
  #--------c. IDs der Sprecherinnen--------------------------------------#
  #xmlValue(xpathSApply(doc, "//*/tagesordnungspunkt/rede/p/redner")[[1]][[1]])
  #xmlValue(xpathSApply(doc, "//*/tagesordnungspunkt/rede/p/redner/name/nachname")[[1]][[1]])
  name <- xmlToDataFrame(xpathSApply(doc, "//tagesordnungspunkt/rede/p/redner/name"), 
                         homogeneous = FALSE)
  dim(name)
  names(name)
  head(name)
  
  redeid <- as.data.frame(xpathSApply(doc, "//tagesordnungspunkt/rede/p/redner", function(x) xmlAttrs(xmlParent(xmlParent(x), "id"))))
  dim(redeid)
  names(redeid) <- "redeid"
  head(redeid)
  
  rednerid <- as.data.frame(xpathSApply(doc, "//tagesordnungspunkt/rede/p/redner", function(x) xmlAttrs(x)))
  dim(rednerid)
  names(rednerid) <- "rednerid"
  head(rednerid)
  
  #bind data
  speaker <- bind_cols(name, rednerid, redeid) %>% 
    distinct() #delete duplicated rows
  dim(speaker)
  head(speaker)
  
  
  #--------d. Vorspann auslesen--------------------------------------#
  path_vorspann <- str_c("../../../daten/bundestag19/vorspann/", f)
  
  #Einlesen mit xml-Paket
  doc <- xmlParse(path_vorspann, encoding = "UTF-8") 
  
  #Extrahiere Nodes
  nodes_vorspann <- getNodeSet(doc, "//kopfdaten")
  #nodes_vorspann[[1]]
  
  kopfdaten <- xmlToDataFrame(xpathSApply(doc, "//kopfdaten"), 
                                  homogeneous = FALSE)
  dim(kopfdaten)
  head(kopfdaten)
  
  inhalt <- as.data.frame(xpathSApply(doc, "//*/ivz-block/ivz-eintrag/ivz-eintrag-inhalt", xmlValue))
  dim(inhalt)
  names(inhalt) <- "top_inhalt"
  head(inhalt)
  
  top <- as.data.frame(xpathSApply(doc, "//*/ivz-block/ivz-eintrag/ivz-eintrag-inhalt", function(x) xmlValue(xmlParent(xmlParent(x)))))
  dim(top)
  names(top) <- "top"
  top <- top %>% 
    mutate(top = str_replace_all(top, ":(.*)", ""))
  head(top)
  
  #bind data
  vorspann <- bind_cols(top, inhalt) %>% 
    mutate(plenarprotokoll_nummer = kopfdaten %>% pull(`plenarprotokoll-nummer`), 
           herausgeber = kopfdaten %>% pull(herausgeber), 
           berichtart = kopfdaten %>% pull(berichtart), 
           sitzungstitel = kopfdaten %>% pull(sitzungstitel), 
           veranstaltungsdaten = kopfdaten %>% pull(veranstaltungsdaten))
  dim(vorspann)
  
  #cleaning
  vorspann <- vorspann %>% 
    filter(str_detect(top_inhalt, 'Antrag|Stunde|Drucksache|Beschlussfassung|Geschäftsordnung|Wahl|Nationalhymne|Bericht|Befragung')) %>% 
    group_by(top) %>% 
    summarise(top_inhalt = str_c(top_inhalt, collapse=" UND "), 
              plenarprotokoll_nummer = first(plenarprotokoll_nummer), 
              herausgeber = first(herausgeber), 
              berichtart = first(berichtart), 
              sitzungstitel = first(sitzungstitel), 
              veranstaltungsdaten = first(veranstaltungsdaten)) %>% 
    ungroup()
  dim(vorspann)
  
  
  #--------e. Zusammenfügen der Sitzungsdaten--------------------------------------#
  dim(reden)
  dim(doc_ids)
  dim(speaker)
  dim(vorspann)
  
  reden %>% names()
  speaker %>% names()
  vorspann %>% names()
  doc_ids %>% names()
  
  temp <- left_join(reden, speaker, by="redeid") %>% 
    left_join(., vorspann, by="top") %>% 
    mutate(doc_id = doc_ids %>% pull(doc_id), 
           doc_name = doc_ids %>% pull(doc_name), 
           doc_typ = doc_ids %>% pull(doc_typ), 
           uhrzeit_beginn = doc_ids %>% pull(uhrzeit_beginn), 
           uhrzeit_ende = doc_ids %>% pull(uhrzeit_ende))
  
  #Check(s)
  temp %>%
    select(redeid, klasse, top, nachname, text) %>% 
    slice(40:55)
  
  
  #--------6. Zusammenfügen aller Dateien--------------------------------------#
  #Schleife schließen
  
  lp19_xml <- bind_rows(lp19_xml, data.frame(f, temp))
  
  print(f)
  
}


#Backup
save(lp19_xml, file="../../../daten/bundestag19/lp19_xml_temp.rda")

4. Korrekturen vornehmen

Da die xml-Dateien alles andere als sauber sind, d.h. meines Erachtens nach keine valide xml-Struktur aufweisen (bzw. eine Struktur, mit der ich nicht gut zurecht komme), müssen die Daten manuell nachbereitet werden. Das hier von mir angewandte Verfahren kann noch weitere fehlerhafte Kodierungen beinhalten. In anderen Worten: Ich garantieren keine Korrektheit der aufbereiteten Daten. Falls Ihnen ein Fehler auffällt, können Sie mir gerne Bescheid geben. Dann versuche ich, das Verfahren entsprechend anzupassen.

#Daten laden
load("../../../daten/bundestag19/lp19_xml_temp.rda")

#add rowid
lp19_xml <- lp19_xml %>% 
  mutate(rowid = row_number())


#--------------------------Korrektur(en)------------------------#

#-------------1. Beiträge des Präsidiums

#Beiträge des Präsidiums sind namentlich falsch zugeordnet -> Korrektur
lp19_xml %>% 
  filter(f == "19006-data.xml") %>% 
  select(redeid, klasse, nachname, text) %>% 
  slice(35:55)

lp19_xml <- lp19_xml %>% #Ersetze Name mit Text, wenn 
  mutate(speaker = case_when(
    klasse=="präsidium" ~ str_replace_all(text, ':', ''), #klasse gleich "präsidium"
    klasse!="kommentar" & 
      str_count(text, '\\S+') <= 5 & 
      str_detect(text, '[Pp]räsident') &
      str_detect(text, "\\bSolms\\b|\\bSchäuble\\b|\\bPau\\b|\\bRoth\\b|\\bKubicki\\b|\\bOppermann\\b") ~ str_replace_all(text, ':', ''),
    TRUE ~ NA_character_)) %>% 
  mutate(speaker = str_trim(speaker, side="both"))

lp19_xml %>% 
  #filter(klasse=="präsidium") %>% 
  count(speaker)

lp19_xml <- lp19_xml %>% 
  mutate(speaker = str_replace_all(speaker, "Wo lfgang", "Wolfgang")) %>%
  mutate(speaker = case_when(
    speaker=="Vizepräsident Thomas Opperman" ~ "Vizepräsident Thomas Oppermann",
    str_detect(speaker, 'SchäublePräsident') ~ "Präsident Dr. Wolfgang Schäuble",
    TRUE ~ speaker
  )) %>% 
  mutate(speaker = case_when(
    str_detect(speaker, 'Schäuble') ~ "Präsident Dr. Wolfgang Schäuble", 
    TRUE ~ speaker
  ))

lp19_xml_kommentare <- lp19_xml %>% 
  filter(klasse=="kommentar")
dim(lp19_xml_kommentare)

lp19_xml_reden <- lp19_xml %>% 
  filter(klasse!="kommentar")
dim(lp19_xml_reden)

for (r in 1:50){
  lp19_xml_reden <- lp19_xml_reden %>% 
    group_by(f) %>% 
    mutate(speaker = case_when(
      is.na(speaker) & !str_detect(klasse, 'kommentar') & #Zeile 0
      !is.na(lag(speaker, 1)) & #Zeile -1
      str_detect(lag(klasse, 1), 'präsidium') #Zeile -1
      ~ lag(speaker, 1), #Zeile -1
      TRUE ~ speaker
    )) %>% 
    mutate(klasse = case_when(
      !is.na(speaker) & str_detect(klasse, 'J') ~ "präsidium_ff", 
      TRUE ~ klasse
    )) %>% 
    ungroup()
  print(r)
}

lp19_xml <- bind_rows(lp19_xml_reden, lp19_xml_kommentare) %>% 
  arrange(rowid)
dim(lp19_xml)

lp19_xml %>% 
  filter(f == "19001-data.xml") %>% 
  select(redeid, klasse, speaker, nachname, text) %>% 
  slice(1:40)

lp19_xml %>% 
  count(speaker)

#-------------2. Verschiedene Beobachtungen löschen
#Beobachtung löschen, wenn klasse=="redner" oder klasse=="präsidium"
#weil kein Redebeitrag, sondern Kodierung des Namens
dim(lp19_xml)
lp19_xml <- lp19_xml %>% 
  filter(klasse!="redner" & klasse!="präsidium") 
dim(lp19_xml)

lp19_xml %>% 
  count(klasse)

# backup <- lp19_xml
# lp19_xml <- backup

#-------------3. Namen der Sprecher*innen einsetzen
lp19_xml <- lp19_xml %>% 
  mutate(speaker = case_when(
    is.na(speaker) & klasse!="kommentar" ~ 
      str_c(vorname, " ", nachname),
    TRUE ~ speaker
  ))

lp19_xml <- lp19_xml %>%
  group_by(f) %>% 
  fill(speaker, .direction = "down") %>% 
  mutate(speaker = case_when(
    klasse=="kommentar" ~ NA_character_, 
    TRUE ~ speaker
  )) %>% 
  ungroup()

lp19_xml %>% 
  filter(f == "19001-data.xml") %>% 
  select(redeid, klasse, speaker, nachname, text) %>% 
  slice(1:50)


#-------------4. Fraktionen
lp19_xml %>% 
  count(speaker, fraktion)

lp19_xml %>% 
  filter(fraktion == "SPD: Ja.") 

lp19_xml <- lp19_xml %>% 
  mutate(fraktion = case_when(
    fraktion=="Bündnis 90/Die Grünen" ~ "BÜNDNIS 90/DIE GRÜNEN", 
    fraktion=="SPD: Ja." ~ "SPD",
    TRUE ~ fraktion
  ))

#Reden der Präsident*innen sind keiner Fraktion zugeordnet
lp19_xml <- lp19_xml %>% 
  mutate(fraktion = case_when(
    str_detect(speaker, '[Pp]räsident') ~ NA_character_, 
    TRUE ~ fraktion
  ))

lp19_xml <- lp19_xml %>% 
  group_by(speaker) %>% 
  #count(speaker, fraktion) %>% 
  arrange(speaker, fraktion) %>% 
  mutate(fraktion = case_when(
    speaker!="" ~ first(fraktion),
    TRUE ~ fraktion
    )) %>% 
  ungroup() 


#-------------5. Resortieren
lp19_xml <- lp19_xml %>% 
  select(f, rowid, redeid, klasse, speaker, rednerid, text, everything())



#Speichern
save(lp19_xml, file="../../../daten/bundestag19/lp19_xml.rda")

4. Validate, Validate, Validate

Falls etwas noch nicht korrekt kodiert ist, muss man ggf. das Skript weiter anpassen. Das kann durchaus einige Zeit in Anspruch nehmen, v.a. wenn es relativ komplexe Extraktionen sind.

#Daten laden
load("../../../daten/bundestag19/lp19_xml.rda")
lp19_xml %>% 
  count(f)
## # A tibble: 101 x 2
##    f                  n
##    <chr>          <int>
##  1 19001-data.xml   510
##  2 19002-data.xml  2510
##  3 19003-data.xml  1561
##  4 19004-data.xml  6457
##  5 19005-data.xml  4940
##  6 19006-data.xml   626
##  7 19007-data.xml  3231
##  8 19008-data.xml  1572
##  9 19009-data.xml   462
## 10 19010-data.xml  1000
## # … with 91 more rows

#Blick auf einen Teil der Daten
lp19_xml %>% 
  filter(f == "19072-data.xml") %>% 
  slice(1:200) %>% 
  select(rowid, redeid, klasse, speaker, rednerid, fraktion, text) %>% 
  DT::datatable(., options = list(pageLength = 5))

#Welche Fraktionen gibt es
lp19_xml %>% 
  filter(klasse!="kommentar") %>% 
  count(speaker, fraktion) %>% 
  DT::datatable(., option = list(pageLength = 20))

lp19_xml %>% 
  filter(fraktion == "Bremen") %>% 
  select(rowid, redeid, klasse, speaker, rednerid, fraktion, text) %>% 
  DT::datatable(., options = list(pageLength = 5))