-
Notifications
You must be signed in to change notification settings - Fork 0
/
opis projektu.Rmd
330 lines (265 loc) · 15.7 KB
/
opis projektu.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
---
title: "Scrapper ostrzeżeń pogodowych IMGW"
author: "Jakub Dakowski"
date: "11.01.2021"
output:
md_document: default
html_document: default
pdf_document:
df_print: paged
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(tidy.opts = list(width.cutoff = 60), tidy = TRUE, echo = T)
library(formatR)
library(xml2)
library(dplyr)
library(pdftools)
library(stringr)
library(rvest)
unlink("tmp", recursive = TRUE)
dir.create("tmp")
preprocess <- function(txt) {
txt <- gsub("\n+| {2,}\t+", " ", txt)
txt <- gsub("strona \\d+ z \\d+", "", txt, ignore.case=T)
txt <- gsub(" Instytut Meteorologii i Gospodarki Wodnej .+ www: www\\.imgw\\.pl", "", txt, ignore.case=T)
txt <- gsub("Zjawisko/Stopień", "\nZjawisko/Stopień", gsub("ostrzeżenia dla powiatu\\) ", "", txt))
txt <- paste(txt, collapse=" ")
txt <- gsub(" {2,}", " ", txt)
return (txt)
}
extract <- function(txt) {
START_PATTERN <- paste(
"Zasięg ostrzeżeń w województwie WOJEWÓDZTWO (?<voivodeship>[\\w\\-]+)",
"OSTRZEŻENIA METEOROLOGICZNE ZBIORCZO NR (?<id>\\d+) WYKAZ OBOWIĄZUJĄCYCH OSTRZEŻEŃ",
"o godz\\. \\d\\d:\\d\\d dnia \\d\\d\\.\\d\\d\\.\\d{4}",
"(?<text>(?:\\n|.)+)",
"Dyżurny synoptyk (?<creator>.+?(?= IMGW-PIB))",
sep=" "
)
PATTERN <- paste(
"Zjawisko/Stopień zagrożenia (?<event>[\\w ]+)/(?<lvl>\\d+)(?<messtype>| \\w+)",
"Obszar \\(w nawiasie numer powiaty: (?<regions>(?:[\\w- ]+\\(\\d+\\)(?:, )*)+)",
"(?:Ważność od godz\\. (?<starthour>\\d\\d\\:\\d\\d)",
"dnia (?<startday>\\d\\d\\.\\d\\d\\.\\d{4})",
"do godz\\. (?<endhour>\\d\\d\\:\\d\\d)",
"dnia (?<endday>\\d\\d\\.\\d\\d\\.\\d{4})",
"Prawdopodobieństwo (?<prob>\\d{1,3}\\%)",
"Przebieg (?<how>.+?(?= SMS))|Czas",
"odwołania godz\\. (?<hour>\\d\\d:\\d\\d) dnia (?<day>\\d\\d\\.\\d\\d\\.\\d{4})",
"Przyczyna (?<cancelcause>.+?(?= SMS)))",
"SMS (?<sms>.+?(?= RSO))",
"RSO (?<rso>.+?(?= Uwagi))",
"Uwagi (?<remarks>[^\\n]+)",
sep=" "
)
pat <- str_match(txt, START_PATTERN)
df <- as.data.frame(str_match_all(pat[4], PATTERN))
df$voivodeship <- str_to_lower(pat[2], locale = 'pl')
df$warn_id <- pat[3]
df$author <- pat[5]
df$infile <- rownames(df)
return (df)
}
```
## Opis teoretyczny
### Motywacja
Od ponad stu lat ludzkość może obserwować skutki swojej industrialnej działalności na klimacie. Wśród skutków pojawiają się między innymi zmiany w temperaturze globalnej. Wspomniany wzrost temperatury stanowi sam w sobie zagrożenie zdrowotne dla ludzi. Oprócz tego, powoduje on wyższe szanse na takie anomalie jak fale ciepła, czy susze. Wraz z nasileniem się tych zjawisk, skutki zdrowotne będą także się nasilać, wymuszając na wielu osobach zmiany w trybie życia. W związku z tym pojawia się potrzeba uwzględniania w działaniu różnych aplikacji informacji o ostrzeżeniach klimatycznych. Niestety jednak IMGW - organ odpowiedzialny za tworzenie tych ostrzeżeń dla Polski - publikuje je w jednej z najtrudniejszych do automatyzacji form - plikach PDF.
### Czym jest ten program?
Projekt ten jest przedsięwzięciem z zakresu inżynierii danych mając za zadanie sprowadzić dane PDF do jaśniejszego formatu JSON. Dokonywane jest to przy pomocy biblioteki *plumber*, która umożliwia utworzenie z kodu R działającego API.
API to posiada dwa endpointy. Jeden udostępnia wszystkie ostrzeżenia. Drugi natomiast pozwala na wyszukiwananie ostrzeżeń na podstawie nazw powiatu.
## Dane zbierane przez scrapper
Największym problemem tej pracy jest mała ilość dostępnej dokumentacji na temat sposobu działania ostrzeżeń. Większość informacji na ich temat autor musiał wywnioskować na własną rękę.
Opublikowane ostrzeżenia publikowane są w zbiorowych komunikatach dla określonych województw. Trudno powiedzieć, czy jedno województwo może mieć kilka plików. W jednym może znajdować się jednak kilka ostrzeżeń.
Komunikaty mogą być ogłoszeniem, aktualizacją, lub odwołaniem ostrzeżenia. Autor jednak także w tym obszarze nie ma informacji na temat sposobu działania systemu. W każdym ogłoszeniu, bądź aktualizacji znajduje się informacja o typie i stopniu zjawiska, czasie ważności, prawdopodobieństwie, przebiegu, komunikatach SMS, RSO oraz uwagach. Odwołania podają natomiast nie podają czasu ważności, prawdopodobieństwa i przebiegu, a zamiast tego dają powód odwołania oraz moment odwołania. Każdy plik dodatkowo zawiera informację o dyżurnym synoptyki, kilka informacji o numerze ostrzeżenia (dla województwa i powiatów, ale niestety nie dla kraju). Można także tam znaleźć krótką informację prawną o możliwości udostępniania. Cytując:
> Wszelkie dalsze udostępnianie, rozpowszechnianie (przedruk, kopiowanie, wiadomość sms) jest dozwolone wyłącznie w formie
dosłownej z bezwzględnym wskazaniem źródła informacji tj. IMGW-PIB.
Autor dokładał wszelkich starań, aby przekazywać ostrzeżenia w formie dosłownej, starając się jedynie przetransformować je do czytelnej dla komputera wersji.
## Rozwiązanie problemu pobierania ostrzeżeń
Czyli inaczej opis działania algorytmu.
### Pobieranie danych
Schemat działania systemu jest stosunkowo prosty. System rozpoczyna od pobrania listy aktualnych ostrzeżeń ze strony <https://danepubliczne.imgw.pl/data/current/ost_meteo/>. Dane są następnie transformowane do formy tabeli oraz czyszczone dla łatwiejszej obsługi.
```{r}
webpage_url <- "https://danepubliczne.imgw.pl/data/current/ost_meteo/"
webpage <- xml2::read_html(webpage_url)
ost_files <- rvest::html_table(webpage)[[1]] %>%
tibble::as_tibble(.name_repair = "unique") %>%
filter(Name != "" & Name != "Parent Directory") %>%
select(Name, `Last modified`) %>%
rename_with(function(x){"modified_at"}, "Last modified")
ost_files
```
Program następnie pobiera kolejne ogłoszone ostrzeżenia i kompiluje je do ramki danych. Dokonuje tego funkcja `get_warns`, która odwołuje się do `read_warns`.
```{r}
get_warns <- function(file) {
webpage_url <- "https://danepubliczne.imgw.pl/data/current/ost_meteo/"
saved.file <- paste('tmp', file, sep="/")
if (!file.exists(saved.file)) {
download.file(paste(webpage_url, file, sep=""), saved.file, mode="wb")
}
return (read_warns(saved.file))
}
read_warns <- function(saved.file) {
txt <- preprocess(pdf_text(saved.file))
extracted <- extract(txt)
extracted$file <- saved.file
extracted$messtype[extracted['messtype']==" ZMIANA"] <- "1"
extracted$messtype[extracted['messtype']==" WYCOFANIE"] <- "2"
extracted$messtype[extracted['messtype']==""] <- "0"
return (extracted)
}
```
`read_warns` odczytuje plik PDF o określonym adresie i przetwarza go do formy tekstowej z pomocą biblioteki *pdftools*. Te dane ulegają przetworzeniu przez funkcje `preprocess` oraz `extract`.
### Ekstrakcja ostrzeżeń
Ponieważ przetwarzane pliki są oryginalnie przechowywane w formacie PDF, znaczna część zawartych w nich znaków nie należy do faktycznych komunikatów, a jest jedynie nagłówkami, czy stopkami dokumentu.
Zadaniem funkcji `preprocess` jest usunięcie tych segmentów. Przeprowadza ona kolejno:
1. Usunięcie nowych linii, tabulacji oraz nadmiaorwych spacji.
2. Usunięcie numerów stron.
3. Usunięcie nagłówka z danymi adresowymi IMGW.
4. Usunięcie "ostrzeżenia dla powiatu)" znajdującego się w niepoprawnie zescrapowanym miejscu.
5. Dodanie nowej linii na początku każdego nowego komunikatu.
6. Konkatenacja oraz ponowne usunięcie nadmiarowych spacji.
```r
# Przy uruchamianiu programu funkcja jest uruchamiana wraz z setupem,
# nie trzeba jej uruchamiać drugi raz.
preprocess <- function(txt) {
txt <- gsub("\n+| {2,}\t+", " ", txt)
txt <- gsub("strona \\d+ z \\d+", "", txt, ignore.case=T)
txt <- gsub(" Instytut Meteorologii i Gospodarki Wodnej .+ www: www\\.imgw\\.pl",
"", txt, ignore.case=T)
txt <- gsub("Zjawisko/Stopień", "\nZjawisko/Stopień",
gsub("ostrzeżenia dla powiatu\\) ", "", txt))
txt <- paste(txt, collapse=" ")
txt <- gsub(" {2,}", " ", txt)
return (txt)
}
```
Funkcja `extract` ma o wiele trudniejsze zadanie. Przyjmuje ona przetworzony tekst i rozpoczyna od przeszukania go z pomocą wyrażenia regularnego `START_PATTERN`, które umożliwia wykrycie województwa, numeru ostrzeżenia zbiorczego, treści ostrzeżeń jednostkowych i autora ostrzeżenia. Treść ostrzeżeń jest następnie przeszukiwana z pomocą wyrażenia `PATTERN`, które identyfikuje pojedyncze ostrzeżenia i wykrywa w nich konkretne dane. Kolejne wiersze odpowiadają za identyfikację:
- Zjawiska i stopnia zagrożenia (a także bycia modyfikacją/usunięciem danego ostrzeżenia)
- Zagrożonych powiatów
- Czasu rozpoczęcia
- Daty rozpoczęcia
- Czasu zakończenia
- Daty zakończenia
- Prawdopodobieństwa
- Opisu przebiegu
- Jeżeli ogłoszenie jest odwołaniem ostrzeżenia:
- Czasu odwołania
- Przyczyny odwołania
- Komunikatu SMS
- Komunikatu RSO
- Uwag do ostrzeżenia
Wyniki tego przeszukiwania są następnie transformowane do ramki danych, która uzupełniana jest o pozostałe informacje pochodzące z przeszukiwania z pomocą `START_PATTERN` oraz numer ostrzeżenia w danym pliku (na podstawie tego można potem utworzyć klucz składający się z nazwy pliku i numeru ostrzeżenia w pliku).
```r
# Przy uruchamianiu programu funkcja jest uruchamiana wraz z setupem,
# nie trzeba jej uruchamiać drugi raz.
extract <- function(txt) {
START_PATTERN <- paste(
"Zasięg ostrzeżeń w województwie WOJEWÓDZTWO (?<voivodeship>[\\w\\-]+)",
"OSTRZEŻENIA METEOROLOGICZNE ZBIORCZO NR (?<id>\\d+) WYKAZ OBOWIĄZUJĄCYCH OSTRZEŻEŃ",
"o godz\\. \\d\\d:\\d\\d dnia \\d\\d\\.\\d\\d\\.\\d{4}",
"(?<text>(?:\\n|.)+)",
"Dyżurny synoptyk (?<creator>.+?(?= IMGW-PIB))",
sep=" "
)
PATTERN <- paste(
"Zjawisko/Stopień zagrożenia (?<event>[\\w ]+)/(?<lvl>\\d+)(?<messtype>| \\w+)",
"Obszar \\(w nawiasie numer powiaty: (?<regions>(?:[\\w- ]+\\(\\d+\\)(?:, )*)+)",
"(?:Ważność od godz\\. (?<starthour>\\d\\d\\:\\d\\d)",
"dnia (?<startday>\\d\\d\\.\\d\\d\\.\\d{4})",
"do godz\\. (?<endhour>\\d\\d\\:\\d\\d)",
"dnia (?<endday>\\d\\d\\.\\d\\d\\.\\d{4})",
"Prawdopodobieństwo (?<prob>\\d{1,3}\\%)",
"Przebieg (?<how>.+?(?= SMS))|Czas",
"odwołania godz\\. (?<hour>\\d\\d:\\d\\d) dnia (?<day>\\d\\d\\.\\d\\d\\.\\d{4})",
"Przyczyna (?<cancelcause>.+?(?= SMS)))",
"SMS (?<sms>.+?(?= RSO))",
"RSO (?<rso>.+?(?= Uwagi))",
"Uwagi (?<remarks>[^\\n]+)",
sep=" "
)
pat <- str_match(txt, START_PATTERN)
df <- as.data.frame(str_match_all(pat[4], PATTERN))
df$voivodeship <- str_to_lower(pat[2], locale = 'pl')
df$warn_id <- pat[3]
df$author <- pat[5]
df$infile <- rownames(df)
return (df)
}
```
Jak nietrudno sobie wyobrazić strategia od tego momentu jest dość prosta. Oprogramowanie łączy ze sobą kolejne ramki danych:
```{r}
file <- ost_files$Name[1]
df <- get_warns(file)
if (length(ost_files$Name)>1){
for (file in ost_files$Name[2:length(ost_files$Name)]) {
df <- rbind(df, get_warns(file))
}
}
df
```
### Obróbka danych
W tym momencie może zostać włączone filtrowanie tylko nowych ostrzeżeń. Program zmienia też zawartość zestawu kolumn:
- `regions` od teraz przechowuje wektor powiatów zamiast ciągu znakowego,
- `file` jest pozbawiane nazwy folderu,
- `prob` konwertowane jest na ułamek zamiart ciągu znakowego
- Daty kompilowane są do wystandaryzowanego formatu,
- Liczby całkowite konwertowane są do formatu liczb całkowitych.
Warto w tym momencie pochylić się nad `format_time` oraz `get_places`. Pierwsza skleja ze sobą dwie kolumny i przetwarza z użyciem `strptime`. Następnie pola `NA` uzupełniane są przez `NULL`. Druga natomiast jest prostym regexem, który dodatkowo konwertuje nazwy powiatów na małe litery.
```{r}
format_time <- function(date, time) {
t <- strptime(paste(date, time), "%d.%m.%Y %H:%M")
t[is.na(t)] <- NULL
return(t)
}
get_places <- function(place_list) {
p <- " ?([^,]+)\\(\\d+\\),?"
match <- str_match_all(place_list, p)
return(lapply(match, function(x) {
return(str_to_lower(x[, 2], locale = 'pl'))
}))
}
```
Poza tym program usuwa zbędne kolumny. W tym też momencie nadchodzi pora na jakże długą linię służącą do odsortowania ostrzeżeń niedotyczących danego miejsca. Taki wynik jest zwracany użytkownikowi w formacie JSON.
```{r}
fin <- df %>%
# filter(messtype == '0') %>%
mutate(
regions = get_places(df$regions),
file = gsub("tmp/", "", file),
prob = as.numeric(sub("%", "", prob)) / 100,
starttime = format_time(df$startday, df$starthour),
endtime = format_time(df$endday, df$endhour),
canceltime = format_time(df$day, df$hour),
lvl = as.numeric(lvl),
messtype = as.numeric(messtype),
infile = as.numeric(infile),
warn_id = as.numeric(warn_id),
) %>%
# mutate(include = unlist(lapply(regions, function(x) { place %in% x })))
# %>% filter(include) %>% select(-c(include)) %>%
# ^ umożliwia sortowanie po powiatach
select(-c(V1, startday, starthour, endday, endhour, day, hour))
knitr::kable(fin %>% select(event, starttime, endtime, rso),
caption = "Przykładowy wynik działania algorytmu")
```
Oczywiście, jeśli żadne ostrzeżenia nie zostaną znalezione, API zwróci pusty wektor, który w JSONie jest reprezentowany jako `[]`. Ten warunek nie jest tu jednak wprowadzony.
## Sprawdzenie założeń
Autor niestety wciąż nie jest pewien, czy opracowane przez niego wyrażenie regularne działa w pełni. Można je co najwyżej sprawdzić na zestawie wszystkich poprzednich ostrzeżeń, co jednak jest trudne, ze względu na ich inny sposób przechowywania na stronie (są one uprzednio kompresowane).
## Interpretacja wyników
We wszystkich komunikatach, które autor napotkał algorytm zadziałał poprawnie, wskazuje to na jego wysoką skuteczność. Niestety aktualne rozwiązanie działa stosunkowo wolno i jest ograniczone, jak chodzi o zmiany formatu ogłoszeń. Założenia na temat ich formy także są dość silne, co jednak wynika z bierności IMGW. Cała potrzeba produkcji takiego oprogramowania w XXI wieku wydaje się absurdalna. Zdaniem autora, dostęp do tego typu ostrzeżeń, jako informacjach umożliwiających ludziom bezpieczną egzystencję, w systemach automatycznych powinien być jak najprostszy. Program pozostawia więc wiele do życzenia, ale wciąż uzupełnia lukę ignorowaną przez organ ostrzeżenia wydający.
## Informacja o użytych pakietach
| Nazwa paczki | Opis | Miesięczne pobrania | Autorzy | Data opublikowania |
|-------------:|-------------------------------------------|---------------------|------------------|--------------------|
| `xml2` | Pobieranie dokumentów HTML | 620 159 | Hadley Wickham | 30.11.2021 |
| `tidyverse` | Zestaw bibliotek wspomagających pracę w R | 689 554 | Hadley Wickham | 15.04.2021 |
| `pdftools` | Scrapowanie tekstu z plików PDF | brak danych | Jeroen Ooms | 06.05.2021 |
| `stringr` | Przetwarzanie ciągów znakowych, regex | 806 997 | Hadley Wickham | 10.02.2019 |
| `plumber` | Generowanie API ze skryptów R | 11 776 | Barret Schloerke | 24.03.2021 |
## Polecana literatura oraz linki do wykorzystanych/przydatnych stron
- Zdecydowanie nie dokumentacja API IMGW
- <https://www.rplumber.io/>
- <https://dplyr.tidyverse.org>
- Stack Overflow
- <https://regexr.com/>
- [Regular Expression Language - Quick Reference](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference)
- [Markdown table generator](https://www.tablesgenerator.com/markdown_tables)