Acg N. .
Temat: Raport HTML (knitr + rmd) jako arkusz Excela z osobnymi...
Ponieważ na grupie zrobiło się cicho jak po śmierci organisty (nie licząc wizyt szukających chętnych do odrobienia za nich prac domowych), postanowiłem podzielić się czymś, co kiedyś bardzo mi się przydało podczas tworzenia zaawansowanego systemu raportowania w R.Tytułem wstępu.
Od czasu pojawienia się narzędzia knitr i jego rozszerzenia dla języka znaczników markdown oraz HTML (dzięki czemu można mieszać kod RMD z kodem HTMLem w jednym pliku), stałem się jego wielkim fanem. Możliwości knitr są naprawdę duże: znaczna liczba opcji kontrolujących wykonanie porcji kodu R, możliwość dynamicznego skalowania obrazów (wprost nieocenione przy ggplot2::facet_wrap/grid!), bezproblemowa współpraca z ggplot2 (także w pętli), możliwość tworzenia/wywołania elementów GUI (np. okna dialogowe dla podania parametrów, ProgressBar, z którego intensywnie korzystam do wyświetlania postępu generowania raportu), możliwość wykorzystania szablonów HTML i CSS, możliwość przekazania obiektu środowiska R (zmienne, funkcje) z głównego programu do raportu (umożliwia jego sparametryzowanie)*, możliwość "cichego" wywołania spod RScript, możliwość wywoływania sub sekcji kodu ("child")**, łatwe generowanie tabel za pomocą knitr:kable oraz współpraca z xtable, a także wiele innych zalet sprawia, że jest to świetne narzędzie do budowy aplikacji analitycznych. Zwłaszcza, gdy się zbuduje wygodny interfejs użytkownika z wykorzystaniem możliwości biblioteki gWidgets/gWidgets2.
------------------
*
knitrenv <- new.env()
assign("XYZ", XYZ, knitrenv)
knit2html("output.rmd", template="template.html", envir=knitrenv, options=c("smartypants","highlight_code"), title="Report")
**
```{r child = if (XXXXXX == TRUE) 'xxxxxxxx.rmd'}
```
```{r child = 'progress_reporter.rmd'}
```
------------------
Używam tej kombinacji do tworzenia raportów HTML (kntr2html). HTML jest dla mnie optymalnym formatem raportu, ponieważ ten sam plik mogę wyświetlić:
- w przeglądarce, co świetnie sprawdza się podczas prezentacji biznesowych / naukowych.
Dzięki HTMLowi, w oparciu o linki i kotwice, łatwo można zaimplementować elementy nawigacji (spis treści, sekcji, tabel, ilustracji). Można także skorzystać z możliwości użycia CSS i JavaScript (interaktywne prezentacje, dynamiczne formatowanie). HTML świetnie także nadaje się do publikowania za pomocą chmury, np. DropBoxa.
- w Excelu, który genialnie sobie radzi z HTML i zawartymi w nim tabelami, honorując przy tym CSSy (OpenOffice jest za nim całą wieczność). Wystarczy potem w Excelu dwoma kliknięciami ustawić autofiltr i można jechać dalej z analizą
- w Wordzie, który również bezproblemowo wczytuje HTML, zachowując formatowanie tabel i ujmując wiele dodatkowej pracy podczas tworzenia raportu
- w OpenOffice Web - jako tekst, ale z zachowaniem tabel (można je potem -żmudnie- przenosić do Calca)
- HTML nadaje się także do dalszego, automatycznego, przetwarzania
Niedawno pojawił się jednak pewien problem. O ile Excel "honoruje" linki i pozwala nawigować po sekcjach raportu w obrębie arkusza, to jeśli ma się kilka tabel po kilka tysięcy rekordów, "ciągły raport" szybko staje się niewygodny, łatwo przeskoczyć do początku/końca strony lub zwyczajnie się zgubić (ctrl+home przeniesie na początek całego wielkiego raportu, a nie tabeli). Poza tym, bez patrzenia na spis treści lub przewinięcie strony trudno od razu stwierdzić, co zawiera.
Poszukiwałem więc sposobu na umieszczenie wyników każdej z analiz w osobnej zakładce. W skrócie - chodzi o to, by ten sam raport móc wyświetlać, zależnie od potrzeb, w postaci ciągłej (przeglądarka, Word) lub "zakładkowej" (Excel). Niestety, HTML nie oferuje nam żadnego taga "łamiącego strony", więc trzeba sobie poradzić inaczej. Udało mi się znaleźć sposób na wyświetlenie kilku plików HTML na osobnych zakładkach skoroszytu, wykorzystując następujący "master template". Niestety, zadziała chyba tylko z Officem od wersji 2007 (włącznie).
<HTML xmlns:x="urn:schemas-microsoft-com:office:excel">
<HEAD>
<meta name="Excel Workbook Frameset">
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<meta content="Excel.Sheet" name="ProgId">
<meta content="Microsoft Excel 11" name="Generator">
<LINK href="Sheet1.htm">
<LINK href="Sheet2.htm">
<!--[if gte mso 9]><xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>Sheet1</x:Name>
<x:WorksheetSource HRef="Sheet1.htm"/>
</x:ExcelWorksheet>
<x:ExcelWorksheet>
<x:Name>Sheet2</x:Name>
<x:WorksheetSource HRef="Sheet2.htm"/>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml><![endif]-->
</HEAD>
<body>
</body>
</HTML>
Teraz tylko trzeba umieć podzielić nasz ciągły raport na sekcje i każdą wrzucić do osobnego pliku-zakładki, a na koniec zbudować "plik matkę".
1. Przed każdą sekcją raportu dodajemy znacznik informujący o jej początku.
Można tu zręcznie użyć komentarza HTML, który pozwoli ukryje tekst, co pozwoli na wiele dodatkowych rzeczy, np. nadanie tytułu zakładkom. Dodajemy też taki tag na końcu raportu, co uprości indeksowanie.
<!-- REPORT SECTION TabTitle="Tytul" -->
## Kod RMD
<strong>Kod HTML</strong>
```{r xxxxxxx, echo=FALSE, warnings = FALSE, comment = NA, result='asis'}
zmienna <- paste("kod", "w", "R");
# Tabela
knitr::kable(as.data.frame(cbind("Row num" = 1:nrow(result), result)))
cat(paste("Dalszy", "<span class='format'>kod</span>", "HTML"))
```
<!-- REPORT SECTION TabTitle="Kolejna sekcja" -->
.......
2. Wczytujemy wygenerowany raport do zmiennej i "grepujemy" po naszym pseudo tagu:
html <- readLines(pathToReport)
breaks <- grep("REPORT SECTION", html)
Dostajemy wektor indeksów wierszy z tagami, np. c(1, 10, 44, 81)
Para indeksów (n, n+1) wyznacza wiersze sekcji raportu, poza ostatnim wierszem, który zawiera kolejny tag i jest początkiem kolejnej sekcji raportu (lub jego końca).
3. Tworzymy początek kodu głównej strony, iterujemy po sekcjach raportu, tworzymy kod stron-zakładek, następnie znów iterujemy po sekcjach i dodajemy referencje do wygenerowanych stron (możemy to oczywiście zrobić za jednym zamachem). Podaję kod proof-of-concept, każdy go sobie sam zoptymalizuje i dopasuje do potrzeb. Funkcja przyjmuje jako parametr kompletną ścieżkę do "ciągłego" raportu HTML, a następnie wyciąga z niego sekcje, wrzuca je do plików, tworzy "plik matkę" i umieszcza w nim referencje subraportów, a następnie dodaje do nazwy wejściowego raportu sufiks "_Ex"(cel).
MakeMultiSheetExcelOutput <- function(pathToReport) {
pathToOutput <- gsub("^(.+)/([^/]+)$", replacement="\\1", pathToReport)
html <- readLines(pathToReport)
breaks <- grep("REPORT SECTION", html)
content <- "<HTML xmlns:x=\"urn:schemas-microsoft-com:office:excel\">
<HEAD>
<meta name=\"Excel Workbook Frameset\">
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=windows-1252\">
<meta content=\"Excel.Sheet\" name=\"ProgId\">
<meta content=\"Microsoft Excel 11\" name=\"Generator\">"
for(sect in seq_len(length(breaks)-1)) {
reportLine <- html[breaks[sect]];
reportTitle <- gsub(pattern = "(.*TabTitle=\")(.*)(\".*$)", replacement = "\\2", x = reportLine)
reportBeginning <-
"<!DOCTYPE html>
<html>
<head>
<title>#!title#</title>
<link rel='Stylesheet' type='text/css' href='style.css'/>
<script src='js-min.js' type='text/javascript'></script>
</script>
</head>
<div class='container'>
"
reportChunk <- paste(html[breaks[sect]:breaks[sect+1]-1], sep="", collapse="");
reportEnd <-
" </div>
</html>
"
report <- paste(reportBeginning, reportChunk, reportEnd, sep="");
writeLines(report, paste(pathToOutput, "/", gsub("\\s","",reportTitle), ".html", sep=""))
content <- paste(content, "<LINK href=\"", paste(pathToOutput, "/", gsub("\\s","",reportTitle), ".html", sep="") ,"\">", sep="")
}
content <- paste(content, "<!--[if gte mso 9]><xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>", sep="")
for(sect in seq_len(length(breaks)-1)) {
reportLine <- html[breaks[sect]];
reportTitle <- gsub(pattern = "(.*TabTitle=\")(.*)(\".*$)", replacement = "\\2", x = reportLine)
content <- paste(content, " <x:ExcelWorksheet>
<x:Name>", reportTitle, "</x:Name>
<x:WorksheetSource HRef=\"", paste(pathToOutput, "/", gsub("\\s","",reportTitle), ".html", sep=""), "\"/>
</x:ExcelWorksheet>", sep="")
}
content <- paste(content, "</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml><![endif]-->
</HEAD>
<body>
</body>
</HTML>", sep="");
writeLines(content, gsub(".html", "_Ex.html", pathToReport))
}
Efekty:
1. Przykładowy "ciągły" raport ze spisem treści w przeglądarce.
2. j.w. - w Excelu (1 zakładka)
3. Przykładowa sekcja raportu w pliku RMD
4. Plik "matka" z referencjami do plików HTML sekcji raportu
5. Przykładowa sekcja wyrenderowana do osobnego pliku HTML
6. Źródło
7. "Ciągły" raport po podzieleniu - w Excelu (2 zakładki)
PS: gratis kod do wyciągnięcia z Rejestru ścieżki do Excela i Worda. Tylko te ścieżki rejestru są poprawne na każdej maszynie.
OpenIn <- function(filename, app="Excel") {Ten post został edytowany przez Autora dnia 22.09.14 o godzinie 01:24
if(app %in% c("Word", "Excel")) {
keyValue <- system(paste("reg query \"HKEY_LOCAL_MACHINE\\Software\\Classes\\", app, ".Application\\CLSID", sep=""), intern=TRUE)[3]
clsid <- gsub(pattern = "(.*\\{)(.*)(\\}.*)", replacement = "\\2", x = keyValue)
keyValue <- system(paste("reg query \"HKEY_LOCAL_MACHINE\\Software\\Classes\\CLSID\\{", clsid, "}\\LocalServer32\"", sep=""), intern=TRUE)[3]
pathToApp <- trim(gsub(pattern = "(.*REG_SZ)(.*)(/AUTOMATION.*|^)", replacement = "\\2", x = toupper(keyValue)))
pathToApp <- paste("\"", pathToApp, "\"", sep="")
}
shell(paste(pathToApp, filename))
}