Pobieraniem treści ze stron internetowych (web scrapingiem) zajmuję się komercyjnie od kilku lat. Wykorzystuję do tego m.in. Bash, VBA, Google Sheets, R oraz Python. W ostatnim czasie, podczas konferencji WhyR? 2017 oraz DATA SCIENCE? AGHree! 2018, miałem przyjemność prowadzić warsztaty z web scrapingu w R. W trakcie przygotowywania warsztatów natrafiłem na ciekawe zabezpieczenie przed automatycznym pobieraniem danych. W “serii” wpisów dotyczących web scrapingu chciałbym podzielić się wybranymi problemami z którymi przyszło mi się zmierzyć.
Przykładowy kod do tego wpisu dostępny jest na moim GitHubie.
# Lista pakietów z których korzystam:
require(magrittr) # pipe
require(rvest) # pobieranie danych ze stron internetowych
require(stringi) # obróbka tekstu
require(readr) # wczytywanie plików
require(magick) # obróbka obrazu
require(tesseract) # OCR
require(googledrive) # Google Drive
W dzisiejszym wpisie zajmę się zabezpieczeniem zastosowanym na tej stronie. Na pierwszy rzut oka problem pobrania danych wygląda banalnie. Prosta tabela podzielona na kilka podstron. Liczbę wyników w tabeli można zwiększyć do 100. Do nawigacji pomiędzy kolejnymi podstronami wystarczy odpowiednia parametryzacja linku.
# pobieram zawartość strony
s <- paste0(
'https://www.analizy.pl/',
'fundusze/fundusze-inwestycyjne/notowania') %>%
html_session
# ekstrahuję dane z tabeli
notowania <- s %>%
# tabela z danymi jest klasy noteTable
# to jedyny taki element na stronie
html_node("#noteTable") %>%
# dwupoziomowy nagłówek
# wyrzucam pierwszy poziom
# drugi poziom staje się nagłówkiem
html_table(header = TRUE) %>%
.[,-1] %>%
set_colnames(.[1,]) %>%
# pierwsza kolumna to obrazek
# może być interesujący atrybut title
# aktualnie pomijam
.[-1,] %T>%
# podsumowanie
str
fundusz | data | j.u..netto | X1d | X1m | X3m | X12m | X36m | X60m | ytd | grupa | srri |
---|---|---|---|---|---|---|---|---|---|---|---|
AGIO Agresywny Spółek Wzrostowych (AGIO SFIO) | 3.03 | NA | 0,00% | -4,78% | -0,6% | -3,6% | -19,7% | 0,3% | -2,9% | AKP_UN | 5/7 |
AGIO Akcji Małych i Średnich Spółek (AGIO SFIO) | 3.03 | NA | 0,00% | -3,35% | 4,7% | 2,5% | 0,7% | AKP_MS | 4/7 | ||
AGIO Akcji PLUS (AGIO PLUS FIO) | 2.03 | NA | 2,02% | -9,40% | -8,8% | -14,0% | -28,7% | -10,2% | AKP_UN | 5/7 | |
AGIO Dochodowy PLUS (AGIO PLUS FIO) | 2.03 | NA | -0,01% | 0,18% | 0,6% | 2,1% | 0,4% | PDP_UN | 2/7 | ||
AGIO Kapitał (AGIO SFIO) | 3.03 | NA | 0,00% | 0,12% | 0,6% | -0,3% | 6,4% | 12,3% | 0,3% | PDP_CO | 2/7 |
Udało się pobrać prawie wszystkie dane, za wyjątkiem kolumny j.u. netto
. Dlaczego?!
…
Tak! Wartości w kolumnie j.u. netto
reprezentowane są przez obrazki.
Na początku wydawało mi się, że sprawa jest beznadziejna - tzn. będzie potrzebne użycie jakiegoś zaawansowanego OCR’a albo wykorzystanie uczenia maszynowego do rozpoznawania obrazu. Po małym researchu na temat możliwości R okazało się, że rozwiązania dające dobre wyniki są dostępne bez większego zagłębiania się w tematykę konwersji obrazu do tekstu.
Wypróbowałem dwa popularne rozwiązania: open source’owy tesseract oraz dostępny dla każdego posiadacza konta na Gmailu, Google Drive. Dla każdego z tych rozwiązań dostępne są odpowiednie biblioteki w R, których instalacja (nawet pod Windowsem) jest szybka i przyjemna.
Zanim jednak przejdę dalej uzupełniam kolumnę j.u. netto
o linki do obrazków.
notowania$`j.u. netto` <- s %>%
html_nodes("tbody img[alt*='kurs']") %>%
html_attr("src") %>%
paste0("https://www.analizy.pl", .)
TESSERACT
Pakiet tesseract dostępny jest na GitHubie i aby go zainstalować należy użyć komendy install.packages("tesseract")
(pod linuxem trzeba doinstalować dodatkowe biblioteki, ale wszystko jest objaśnione w źródle).
# ustawienia OCR - skupiam się na poszukiwaniu liczb
engine <-
tesseract(options =
list(tessedit_char_whitelist = " 0123456789,.",
tessedit_char_blacklist = "!?@#$%&*()<>_-+=/:;'\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\t\r",
classify_bln_numeric_mode = "1"))
# oczyszczanie tekstu
text_clearing <- function(x) {
x %>%
stri_replace_all_regex("[ \n]", "") %>%
stri_replace_all_regex("[,]+", ".") %>%
stri_extract_first_regex('[0-9]+[//.]{0,1}[0-9]{0,2}') %>%
as.numeric %>%
format(2)
}
# przykładowa konwersja obrazka
notowania[['j.u. netto']][1] %>%
image_read %>%
ocr(engine) %>%
text_clearing
(obraz) vs 1254.97 (OCR)
ZADZIAŁAŁO!!!
[AKTUALIZACJA - marzec 2020]
Gdy publikowałem ten post w 2018 roku, biblioteka tesseract
nie zadziałała od razu tak dobrze jak teraz. Aby uzyskać dobre wyniki potrzebna była zmiana rozmiaru obrazka (o 30%) za pomocą funkcji image_resize
z biblioteki magick. Jak widać narzędzie to jest ciągle udoskonalane (ale nadal nie jest idealne).
Poniżej wyniki dla pierwszych dziesięciu obrazków:
(obraz) vs 1254.97 (OCR)
(obraz) vs 959.51 (OCR)
(obraz) vs 126.72 (OCR)
(obraz) vs 104.24 (OCR)
(obraz) vs 1481.4 (OCR)
(obraz) vs 129.41 (OCR)
(obraz) vs 101.11 (OCR)
(obraz) vs 107.41 (OCR)
(obraz) vs 104.46 (OCR)
(obraz) vs 83.55 (OCR)
Po przeanalizowaniu kolejnych obrazków okazuje się, że to rozwiązanie ma skuteczność na poziomie 95%. Całkiem nieźle.
GOOGLE DRIVE
Do skorzystania z pakietu googledrive wystarczy posiadanie konta na Gmailu (jest ono niezbędne do autoryzacji połączenia z dyskiem Google). Zasada działania OCRa w oparciu o dysk Google jest prosta: wysyłam plik na dysk Google i konwertuję go do dokumentu Google. Następnie pobieram przekonwertowany plik w dowolnej formie (np. jako plik tekstowy).
OCRbyGoogleDrive <- function(urls) {
# pobieram obrazki do plików tymczasowych
imgs <- list()
for (url in urls) {
imgs %<>%
append(list(list(url = url,
fn = tempfile())))
}
lapply(imgs, function(x)
{image_read(x$url) %>%
image_write(x$fn, format = "png")})
# wysyłam obrazki na dysk Google
gd_imgs <-
lapply(imgs, function(x)
{drive_upload(x$fn, type = "png")})
# konwertuję obrazki na dokumenty Google
gd_imgs_cp <-
lapply(gd_imgs, function(x)
{drive_cp(as_id(x$id),
mime_type = drive_mime_type("document"))})
# zapisuję na dysku jako pliki txt
txt_files <-
lapply(gd_imgs_cp, function(x)
{drive_download(as_id(x$id), type = "txt")})
# oczyszczam wyniki
res <-
sapply(txt_files, function(x)
{read_file(x$local_path)}) %>%
stri_replace_all_regex("[ ,\\.]", "") %>%
stri_extract_last_regex("[0-9]+") %>%
as.numeric %>%
divide_by(100)
# usuwam pliki tymczasowe
imgs %>% lapply(function(x)
unlink(x$fn))
txt_files %>% lapply(function(x)
unlink(x$local_path))
gd_imgs %>% lapply(function(x)
drive_rm(as_id(x$id)))
gd_imgs_cp %>% lapply(function(x)
drive_rm(as_id(x$id)))
return(cbind(urls, res))
}
Poniżej wyniki dla pierwszych dziesięciu obrazków:
(obraz) vs 1254.27 (OCR)
(obraz) vs 1959.51 (OCR)
(obraz) vs 126.72 (OCR)
(obraz) vs 104.24 (OCR)
(obraz) vs 1481.4 (OCR)
(obraz) vs 129.41 (OCR)
(obraz) vs 101.11 (OCR)
(obraz) vs 107.41 (OCR)
(obraz) vs 104.46 (OCR)
(obraz) vs 83.55 (OCR)
Jak widać problem pojawił się już na początku - niewłaściwe odczytanie wartości 9 dla drugiego obrazka. Niemniej jednak analiza wszystkich obrazków wskazuje na to, że ta metoda ma podobną skuteczność jak tesseract
.
Wyniki dla obu metod okazały się dobre, ale dla większości osób będą niesatysfakcjonujące (oczekuje się skuteczności na poziomie 100%). W dalszych krokach należałoby się zastanowić nad różnymi modyfikacjami obrazków lub dodatkowymi funkcjami czyszczącymi. Możnaby także połączyć obie te metody i w przypadku, gdy obie zwracają ten sam wynik założyć, że jest on poprawny. Dla bardziej zaawansowanych, można spróbować nauczyć tesseract
rozpoznawania znaków na konkretnym zbiorze danych.