W “serii” wpisów dotyczącej automatyzacji pracy, chciałbym skupić się na omówieniu przykładów dotyczących usprawniania powtarzających się zadań. Postaram się zaprezentować rozwiązania tych samych problemów za pomocą różnych narzędzi (m.in. R, Python, VBA). Dzisiejszy wpis dotyczy automatyzacji prostego procesu za pomocą R.

Jeżeli często masz do czynienia z cyklicznymi zadaniami typu: pobierz dane > policz > wklej do Worda > wyślij maila i nie robisz tego w sposób zautomatyzowany, to ten wpis jest dla Ciebie!


Przykładowy kod do tego wpisu dostępny jest na moim GitHubie.

# Lista pakietów z których korzystam:
  
require(magrittr) # pipe
require(rmarkdown) # generowanie dokumentów
require(jsonlite) # obsługa plików JSON
require(ggplot2) # rysowanie wykresów
require(readr) # operacje na plikach
require(mailR) # wysyłanie maili

Hipotetyczna sytuacja. Szef lub klient chciałby otrzymywać codziennie mini analizę dotyczącą zmian cen (np. złota). Chce podstawowe obliczenia takie jak zmiana procentowa względem dnia poprzedniego oraz względem pierwszego dnia roku. Dodatkowo potrzebuje wykresu punktowego data ~ cena. Czy czas poświęcony na to zadanie można skrócić do “jednego kliknięcia”? Przekonajcie się…

Zadanie można sprowadzić do prostego schematu przedstawionego na poniższym rysunku:

KROK 1. POBIERZ DANE

Pobranie danych jest w tym przypadku proste, ponieważ NBP udostępnia API.

# plik o nazwie fn będzie załącznikiem do maila
# plik o nazwie fn zapisuję do folderu tmp
fn <- paste0("tmp/prices_", format(Sys.Date(), '%Y_%m_%d'), ".csv")
dir.create("tmp", showWarnings = FALSE)

# interesują mnie dane od początku roku
prices <- paste0('http://api.nbp.pl/api/cenyzlota/',
                 format(Sys.Date(), '%Y'), '-01-01/', 
                 format(Sys.Date(), '%Y-%m-%d'),
                 '?format=json') %>%
  fromJSON %>%
  set_colnames(c('date', 'price')) %T>%
  write.csv2(fn, row.names = FALSE)

KROK 2. PRZYGOTUJ RAPORT

Do wygenerowania raportu i treści maila wykorzystuję pakiet rmarkdown. Do tego celu przygotowałem sparametryzowany szablon w formacie Rmd, który dostępny jest pod tym adresem.

Na początku dokumentu definiuję nazwy parametrów przekazywane z zewnątrz. W tym przypadku prices jest tabelą pobraną za pomocą API, natomiast is_html informuje, czy generowana jest treść maila (html), czy raport w formacie docx (jest to istotne z punktu widzenia generowania obrazków, ale o tym później).

---
...
params:
  prices: "prices"
  is_html: "is_html"
---

W dalszej części dokumentu znajduje się fragment odpowiedzialny za formatowanie tekstu opisującego zmiany cen złota. Gdy zmiana procentowa będzie dodatnia odpowiedni fragment zostanie pokolorowany na zielono. W przypadku wartości ujemnej odpowiedni fragment tekstu zostanie pokolorowany na czerwono.

n = nrow(params$prices)

dn = (params$prices$price[n] - params$prices$price[n-1])/params$prices$price[n-1]

if (dn > 0) {
  dn_desc = '**<font color="green">increased by</font>**'
} else if (dn < 0) {
  dn_desc = '**<font color="red">decreased by</font>**'
}
dn %<>% formatC(digits = 4, format = "f")
dn_desc %<>% paste(dn, "% compared to")

if (params$prices$price[n] == params$prices$price[n-1]) {
  dn_desc = "has not changed in relation to"
}

d1 = (params$prices$price[n] - params$prices$price[1])/params$prices$price[1]

if (d1 > 0) {
  d1_desc = '**<font color="green">increased by</font>**'
} else if (d1 < 0) {
  d1_desc = '**<font color="red">decreased by</font>**'
}
d1 %<>% formatC(digits = 4, format = "f")
d1_desc %<>% paste(d1, "% compared to")

if (params$prices$price[n] == params$prices$price[1]) {
  d1_desc = "has not changed in relation to"
}

Kod R można wplatać także w tekst.

The current gold price is **```r params$prices$price[n]```** PLN (```r params$prices$date[n]```).

Czasami wplatanie kodu R w tekst jest bardzo uciążliwe (w szczególności, gdy chcemy użyć instrukcji warunkowych czy pętli). Z tego powodu warto skorzystać z tej opcji chunka {r, results='asis'} oraz funkcji cat. W ten sposób wygenerowana zawartość będzie formatowana jako zwykły tekst.

cat("The price ", dn_desc, " the price from ", 
     params$prices$date[n-1], " (", params$prices$price[n-1], " PLN) ",
     "and ", d1_desc, " the price from the beginning of the year ",
     "(", params$prices$price[1],  " PLN).", sep = "")

Pozostało wstawienie obrazka do dokumentu. Fragment ten został obłożony instrukcją warunkową, z uwagi na sposób działania poczty. Standardowo pakiet rmarkdown renderuje wszystkie obrazki do postaci base64 (niezależnie od tego, w jaki sposób je zapiszemy w dokumencie). Problem w tym, że klient poczty nie pozwala na przesyłanie takiego formatu - obrazki musimy przesyłać, jako “załączniki” (względy bezpieczeństwa). Z tego powodu dopiero po wyrenderowaniu dokumentu html podmieniam fragment %%plot%% na znacznik <img>.

if (params$is_html) {
  cat('%%plot%%')
} else {
  ggplot(prices, aes(as.Date(date), price)) + 
    geom_point() + 
    xlab("date") + 
    ggtitle(paste0("Gold prices in ", format(Sys.Date(), "%Y")))
}

Zostało już tylko renderowanie dokumentów.

# html
render(input         = "template.Rmd",
       output_file   = "email.html",
       output_format = "html_document",
       params        = list(prices = prices,
                            is_html = TRUE),
       encoding      = "utf-8")

# podmiana %%plot%%
read_file("email.html") %>%
  gsub("%%plot%%", '<img src="tmp/plot.png">', ., fixed = TRUE) %>%
  readr::write_file("email.html")

# docx
render(input         = "template.Rmd",
       output_file   = "report.docx",
       output_format = "word_document",
       params        = list(prices = prices,
                            is_html = FALSE),
       encoding      = "utf-8")

KROK 3. WYŚLIJ E-MAIL

Wysyłanie wiadomości opiera się na pakiecie mailR. Jego konfiguracja może być kłopotliwa (chyba, że ktoś korzysta z takich rozwiązań jak docker), ponieważ działa on w oparciu o Java. Prawdopodobnie wymaga on wersji nie nowszej niż wersja 8.

email <- send.mail(from         = email_from, # patrz plik config
                   to           = email_to, # patrz plik config
                   subject      = paste0("NBP > GOLD PRICES > ", format(Sys.Date(), '%Y-%m-%d')),
                   body         = "email.html",
                   encoding     = "utf-8",
                   html         = TRUE,
                   smtp         = smtp_config, # patrz plik config
                   inline       = TRUE,
                   attach.files = c(fn, "report.docx"),
                   authenticate = TRUE,
                   send         = FALSE,
                   debug        = TRUE)

email$send()

KROK 4. AUTOMATYZACJA

Wystarczy teraz uruchomić skrypt i można powiedzieć, że “jednym kliknięciem” wykonałem cały proces przedstawiony w początkowym schemacie. W efekcie dostaję taką wiadomość:

Ostatecznie można skorzystać z Linuxowego CRON’a lub Windowsowego menadżera zadań i nie potrzebne będzie nawet “jedno kliknięcie”.

BONUS

Dla osób zaznajomionych z dockerem przygotowałem Dockerfile ze zdefiniowanym środowiskiem pracy. W ten sposób można pominąć część konfiguracyjną pakietów i odpalić skrypt za pomocą komendy docker run nazwa_obrazu. Obraz może się długo kompilować i nie jest optymalnej wielkości, ale nie o to chodziło w tym wpisie.

FROM r-base:3.5.0

RUN apt-get update
RUN apt-get install -y --fix-missing openjdk-8-jdk libcurl4-openssl-dev
RUN rm /var/lib/apt/lists/* -R

RUN R CMD javareconf

RUN Rscript -e "install.packages('jsonlite')"
RUN Rscript -e "install.packages('rmarkdown')"
RUN Rscript -e "install.packages('readr')"
RUN Rscript -e "install.packages('curl')"
RUN Rscript -e "install.packages('ggplot2')"
RUN Rscript -e "install.packages('rJava')"
RUN Rscript -e "install.packages('mailR')"

RUN wget https://github.com/jgm/pandoc/releases/download/1.19.2.1/pandoc-1.19.2.1-1-amd64.deb
RUN dpkg -i pandoc-1.19.2.1-1-amd64.deb

COPY scripts /scripts

WORKDIR /scripts

CMD Rscript download_prepare_send.R