ASSISTANT
Hier ist dein korrigiertes komplettes Shiny-Projekt in den 5 Dateien (01_global.R, 02_module_ui.R, 03_module_server.R, 04_server_logic.R, 05_app.R). Ich habe die besprochenen Verbesserungen eingearbeitet, u.a.:
- `ausgewählte_pdfs_status` und weitere Outputs ergänzt.
- Bildpfad als voller Pfad im XML gespeichert.
- Bilder mit `reactiveVal` statt globalem Assign verwaltet.
- Download aller XMLs als ZIP hinzugefügt.
- `%||%` definiert, falls nicht vorhanden.
---
## 01_global.R
```r
library(shiny)
library(shinyFiles)
library(fs)
library(DT)
library(pdftools)
library(magick)
library(zip)
`%||%` <- function(a, b) if (!is.null(a)) a else b
xml_escape <- function(text) {
text <- gsub("&", "&", text, fixed = TRUE)
text <- gsub("<", "<", text, fixed = TRUE)
text <- gsub(">", ">", text, fixed = TRUE)
text <- gsub("\"", """, text, fixed = TRUE)
text <- gsub("'", "'", text, fixed = TRUE)
return(text)
}
```
---
## 02_module_ui.R
```r
mod_ordnerimport_ui <- function(id) {
ns <- NS(id)
tagList(
fluidRow(
column(6, shinyDirButton(ns("ordnerwahl"), "Ordner auswählen", "Bitte wählen Sie einen Ordner mit PDFs")),
column(6, checkboxInput(ns("unterordner"), "Unterordner einbeziehen", value = TRUE))
),
verbatimTextOutput(ns("ordner_pfad")),
hr(),
h4("PDF-Dateien zum Parsen:"),
p("Standardmäßig sind alle PDFs ausgewählt. Entfernen Sie die Häkchen bei Dateien, die nicht geparst werden sollen."),
checkboxInput(ns("alle_auswahlen"), "Alle auswählen/abwählen", value = TRUE),
div(style = "max-height: 400px; overflow-y: auto;",
uiOutput(ns("pdf_checkboxen"))
),
br(),
actionButton(ns("pdf_auswahl_bestatigen"), "Ausgewählte PDFs zu XML parsen", class = "btn-primary", icon = icon("code"))
)
}
```
---
## 03_module_server.R
```r
mod_ordnerimport_server <- function(id) {
moduleServer(id, function(input, output, session) {
ns <- session$ns
volumes <- c(Home = fs::path_home(), Projekt = getwd())
shinyDirChoose(input, "ordnerwahl", roots = volumes, session = session)
pfad_reaktiv <- reactiveVal(NULL)
pdf_dateien_reaktiv <- reactiveVal(NULL)
ausgewahlte_pdfs_reaktiv <- reactiveVal(character(0))
aktualisiere_pdf_liste <- function() {
pfad <- pfad_reaktiv()
if (is.null(pfad) || !dir.exists(pfad)) return()
dateien <- if (input$unterordner) {
list.files(pfad, pattern = "\\.pdf$", recursive = TRUE, full.names = TRUE, ignore.case = TRUE)
} else {
list.files(pfad, pattern = "\\.pdf$", recursive = FALSE, full.names = TRUE, ignore.case = TRUE)
}
pdf_dateien_reaktiv(dateien)
ausgewahlte_pdfs_reaktiv(dateien)
}
observeEvent(input$ordnerwahl, {
if (!is.null(input$ordnerwahl) && !is.integer(input$ordnerwahl)) {
pfad <- parseDirPath(volumes, input$ordnerwahl)
if (length(pfad) > 0 && nzchar(pfad) && dir.exists(pfad)) {
pfad_reaktiv(pfad)
aktualisiere_pdf_liste()
} else {
showNotification("Ungültiger Pfad oder Zugriff verweigert!", type = "error")
}
}
})
observeEvent(input$unterordner, {
if (!is.null(pfad_reaktiv())) aktualisiere_pdf_liste()
})
output$ordner_pfad <- renderText({
p <- pfad_reaktiv()
if (is.null(p) || !nzchar(p)) {
"Bitte wählen Sie einen Ordner aus"
} else {
paste("Gewählter Ordner:", p)
}
})
output$pdf_checkboxen <- renderUI({
dateien <- pdf_dateien_reaktiv()
ausgewahlt <- ausgewahlte_pdfs_reaktiv()
if (is.null(dateien) || length(dateien) == 0) return(p("Keine PDF-Dateien gefunden"))
checkboxGroupInput(ns("pdf_auswahl"), label = NULL,
choices = setNames(dateien, basename(dateien)),
selected = ausgewahlt)
})
observeEvent(input$alle_auswahlen, {
if (input$alle_auswahlen) {
updateCheckboxGroupInput(session, "pdf_auswahl", selected = pdf_dateien_reaktiv())
} else {
updateCheckboxGroupInput(session, "pdf_auswahl", selected = character(0))
}
})
observeEvent(input$pdf_auswahl, {
ausgewahlte_pdfs_reaktiv(input$pdf_auswahl %||% character(0))
alle_auswahl_checked <- length(input$pdf_auswahl %||% character(0)) == length(pdf_dateien_reaktiv() %||% character(0))
if (input$alle_auswahlen != alle_auswahl_checked) {
updateCheckboxInput(session, "alle_auswahlen", value = alle_auswahl_checked)
}
})
return(list(
selected_path = pfad_reaktiv,
all_pdf_files = pdf_dateien_reaktiv,
selected_pdf_files = ausgewahlte_pdfs_reaktiv,
confirm = reactive(input$pdf_auswahl_bestatigen)
))
})
}
```
---
## 04_server_logic.R
```r
library(xml2)
library(magick)
library(pdftools)
parse_pdf_to_xml <- function(pdf_pfad) {
tryCatch({
if (!file.exists(pdf_pfad)) {
return(paste0("<error>Fehler bei ", basename(pdf_pfad), ": Datei existiert nicht</error>"))
}
# Metadaten
meta <- tryCatch(pdf_info(pdf_pfad), error = function(e) list())
meta_text <- paste(names(meta), meta, sep=": ", collapse="\n")
# Text: Liste der Seiten als Vektor
textseiten <- tryCatch(pdf_text(pdf_pfad), error = function(e) character(0))
# Tabellen: grob erfasst mit pdf_data()
tabellen_raw <- tryCatch(pdf_data(pdf_pfad), error = function(e) list())
# Bilder: extrahiere erste Seite als PNG als Beispiel
bild_datei <- NULL
tmpfile <- NULL
try({
img <- image_read_pdf(pdf_pfad, density = 150, pages = 1)
tmpfile <- tempfile(fileext = ".png")
image_write(img, tmpfile, format = "png")
bild_datei <- tmpfile
}, silent = TRUE)
# XML-Baum aufbauen
doc <- xml_new_root("pdf",
filename = basename(pdf_pfad),
path = pdf_pfad)
# Metadaten
xml_add_child(doc, "metadata", xml_escape(meta_text))
# Textseiten
content_node <- xml_add_child(doc, "content")
for (i in seq_along(textseiten)) {
xml_add_child(content_node, "page", number = as.character(i), xml_escape(textseiten[[i]]))
}
# Tabellen (hier als Wörter grob gruppiert)
tables_node <- xml_add_child(doc, "tables")
for (i in seq_along(tabellen_raw)) {
page <- xml_add_child(tables_node, "page", number=as.character(i))
words <- tabellen_raw[[i]]$text
if(length(words) > 0){
xml_add_child(page, "words", paste(words, collapse = " "))
}
}
# Bilder
images_node <- xml_add_child(doc, "images")
if (!is.null(bild_datei)){
xml_add_child(images_node, "image", src = bild_datei, page = 1)
}
as.character(doc)
}, error = function(e) {
paste0("<error>Fehler bei ", basename(pdf_pfad), ": ", e$message, "</error>")
})
}
```
---
## 05_app.R
```r
source("01_global.R")
source("02_module_ui.R")
source("03_module_server.R")
source("04_server_logic.R")
ui <- fluidPage(
titlePanel("PDF zu XML Parser"),
sidebarLayout(
sidebarPanel(
mod_ordnerimport_ui("import")
),
mainPanel(
h4("Ausgewählte PDFs:"),
verbatimTextOutput("ausgewählte_pdfs_status"),
tableOutput("ausgewählte_pdfs"),
hr(),
h4("Parsing Status:"),
verbatimTextOutput("parsing_status"),
hr(),
DT::dataTableOutput("xml_ausgabe"),
uiOutput("xml_download"),
hr(),
h4("Extrahierte Bilder"),
uiOutput("bildanzeige")
)
)
)
server <- function(input, output, session) {
mod_import <- mod_ordnerimport_server("import")
bestätigte_pdfs <- reactiveVal(NULL)
parsing_ergebnisse <- reactiveVal(list())
bilder_tempfiles <- reactiveVal(list())
observeEvent(mod_import$confirm(), {
pdfs <- mod_import$selected_pdf_files()
bestätigte_pdfs(pdfs)
parsing_ergebnisse(list())
bilder_tempfiles(list())
if (length(pdfs) == 0) return()
progress <- shiny::Progress$new(session, min = 0, max = length(pdfs))
on.exit(progress$close())
progress$set(message = "PDFs werden verarbeitet...", value = 0)
ergebnisse <- list()
bilder_tmp <- list()
for (i in seq_along(pdfs)) {
progress$set(value = i, detail = basename(pdfs[i]))
xml <- parse_pdf_to_xml(pdfs[i])
ergebnisse[[pdfs[i]]] <- xml
# Extrahiere Bildpfad aus XML (opt.)
imgfile <- gsub(".*<image src=\"([^\"]+)\".*", "\\1", xml)
if (file.exists(imgfile)) bilder_tmp[[basename(pdfs[i])]] <- imgfile
}
parsing_ergebnisse(ergebnisse)
bilder_tempfiles(bilder_tmp)
})
output$ausgewählte_pdfs_status <- renderText({
pdfs <- bestätigte_pdfs()
if (is.null(pdfs)) {
"Noch keine PDFs ausgewählt."
} else if (length(pdfs) == 0) {
"Keine PDFs ausgewählt."
} else {
paste(length(pdfs), "PDF(s) ausgewählt.")
}
})
output$ausgewählte_pdfs <- renderTable({
pdfs <- bestätigte_pdfs()
if (is.null(pdfs) || length(pdfs) == 0) return(NULL)
data.frame(Datei = basename(pdfs))
})
output$parsing_status <- renderText({
ergebnisse <- parsing_ergebnisse()
if (length(ergebnisse) == 0) {
"Noch nicht geparst."
} else {
paste("Geparste PDFs:", length(ergebnisse))
}
})
output$xml_ausgabe <- DT::renderDataTable({
ergebnisse <- parsing_ergebnisse()
if (length(ergebnisse) == 0) return(NULL)
dt <- data.frame(
PDF = basename(names(ergebnisse)),
XMLSnippet = sapply(ergebnisse, function(xml) {
snippet <- substr(gsub("\n", " ", xml), 1, 200)
if (nchar(xml) > 200) paste0(snippet, "...")
else snippet
}),
stringsAsFactors = FALSE
)
DT::datatable(dt, options = list(pageLength = 5))
})
output$xml_download <- renderUI({
ergebnisse <- parsing_ergebnisse()
if(length(ergebnisse) == 0) return(NULL)
downloadButton("downloadXML", label = "Alle XML herunterladen")
})
output$downloadXML <- downloadHandler(
filename = function() {
paste0("parsed_pdfs_", Sys.Date(), ".zip")
},
content = function(file) {
ergebnisse <- parsing_ergebnisse()
xmlfiles <- character(length(ergebnisse))
for(i in seq_along(ergebnisse)) {
xmlfiles[i] <- tempfile(fileext = ".xml")
writeLines(ergebnisse[[i]], con = xmlfiles[i])
}
zip::zip(file, xmlfiles)
file.remove(xmlfiles)
},
contentType = "application/zip"
)
output$bildanzeige <- renderUI({
bilder <- bilder_tempfiles()
if (length(bilder) == 0) return(NULL)
tagList(
lapply(names(bilder), function(name) {
tagList(
h5(name),
img(src = bilder[[name]], style = "max-width:100%;height:auto;")
)
})
)
})
}
shinyApp(ui, server)
```
---
### Hinweise:
- PDFs müssen lesbar sein.
- Installiere evtl. nötige Pakete: `shiny`, `shinyFiles`, `fs`, `DT`, `pdftools`, `magick`, `zip`.
- `magick::image_read_pdf` benötigt ImageMagick und Ghostscript installiert.
- Beachte ggf. Sicherheitsaspekte (z.B. im Umgang mit Pfaden).
Wenn du noch Fragen hast, helfe ich gerne weiter!