library(tidyverse)
library(furrr)
library(rvest)
library(httr)
library(tictoc)
Klient nedávno měnil CMS i doménu webu, došlo ke změně hodně URL, klasický zmatek, jako v těhle případech vždy. Asi měsíc po změně se ukázalo, že se něco nepovedlo a URL starého webu, které měly být přesměrované na nový web, nyní vrací chybu. Navíc jsou některé z nich ještě stále ve výsledcích hledání Googlu.
Vytáhli jsme tedy ze Search Console starého webu URL všech stránek, které se od změny domény alespoň jednou zobrazily, a mým úkolem bylo rychle zkontrolovat, jaké HTTP kódy vracejí. Konkrétně mě zajímalo, která URL fungují (vrací HTTP 200), neexistují (vrací chybu 404 nebo jinou 4xx), nebo na serveru způsobí nějakou chybu (kódy 5xx).
Pro potřeby tohoto zápisku jsem skutečná URL nahradil odkazy z úvodní stránky Wikipedie, ke kterým jsem navíc přidal 10 náhodných adres, aby mi to ukázalo nějaké chyby. Na principu to nic nemění. Ty odkazy jsem získal takhle:
<- "https://www.wikipedia.org/"
start_url
<- read_html(start_url) |>
urls html_elements("a") |>
html_attr("href") |>
::url_absolute(start_url) |>
xml2c(paste0(start_url, stringi::stri_rand_strings(10, 15)))
Jedná se o 340 adres, a náhodný vzorek deseti z nich vypadá takhle:
sample(urls, 10)
[1] "https://om.wikipedia.org/"
[2] "https://www.wikipedia.org/f5NHoRoonRkdi0T"
[3] "https://xh.wikipedia.org/"
[4] "https://os.wikipedia.org/"
[5] "https://hsb.wikipedia.org/"
[6] "https://sah.wikipedia.org/"
[7] "https://sq.wikipedia.org/"
[8] "https://ga.wikipedia.org/"
[9] "https://sm.wikipedia.org/"
[10] "https://sw.wikipedia.org/"
Kontrola jednoho URL
Pro kontrolu jednoho URL si připravím funkci check_url
. Ta zadané URL zkontroluje HTTP požadavkem HEAD (z balíčku httr), zjistí návratový kód a vrátí tibble s původním URL, výsledným URL (z toho se pozná případné přesměrování) a kódem odpovědi. Pro požadavek se také nastaví timeout v sekundách. Pokud server do této doby neodpoví, místo výsledného HTTP kódu se zapíše NA
.
<- function(url, timeout) {
check_url <- try(HEAD(url, timeout(timeout)), silent = TRUE)
resp if (class(resp) == "try-error") {
<- NA_integer_
status <- NA_character_
dest_url else {
} <- resp$status_code
status <- resp$url
dest_url
}tibble(url, dest_url, status)
}
Vyzkouším, zda funkce funguje s platným (ale přesměrovaným) URL.
check_url("https://wikipedia.org/", 1)
# A tibble: 1 × 3
url dest_url status
<chr> <chr> <int>
1 https://wikipedia.org/ https://www.wikipedia.org/ 200
A raději i s neplatným:
check_url("https://www.wikipedia.org/iououoiuoiuoiu", 1)
# A tibble: 1 × 3
url dest_url status
<chr> <chr> <int>
1 https://www.wikipedia.org/iououoiuoiuoiu https://en.wikipedia.org/iouo… 404
Kontrola celého seznamu URL
A teď již mohu pomocí funkce map_dfr
z balíčku purrr zkontrolovat celý seznam URL. Zároveň si budu pomocí funkcí tic
a toc
z balíčku tictoc měřit, jak dlouho to celé trvá s timeoutem nastaveným na 0.5 sekundy. Reálně by byl potřeba vyšší timeout, např. 3 sekundy, ale Wikiepedia je docela rychlá a já chci ukázat výstup, ve kterém se některá URL v časovém limitu zkontrolovat nepodařilo.
tic()
<- urls |>
status_codes map_dfr(check_url, 0.5)
toc()
59.27 sec elapsed
Trvá to docela dlouho a mohlo by to trvat ještě déle, pokud by byl server pomalejší. Teoreticky až počet URL v seznamu krát timeout. Tak dlouho se mi čekat nechce.
Proto raději zkusím balíček furrr, který nabízí obdobné funkce jako purrr, jenže paralelizované tak, aby využily víc jader a vláken procesoru. Natavím 6 vláken, takže načtení URL by mělo být skoro šestkrát rychlejší.
Zrychlení balíčkem furrr
plan(multisession, workers = 6)
tic()
<- urls |>
status_codes future_map_dfr(check_url, 0.5)
toc()
10.17 sec elapsed
Jo! Šestkrát rychlejší to sice není, ale i tak je zrychlení super. S tím už se pár tisíc URL zpracovat dá.
Zobrazení výsledků
A zbývá se podívat na výsledky. Jsou v dataframu (tibble), takže stačí běžné funkce z balíčku dplyr
Souhrnný přehled
|>
status_codes count(status, sort = TRUE)
# A tibble: 3 × 2
status n
<int> <int>
1 200 329
2 404 10
3 NA 1
Vadné URL
|>
status_codes filter(status != 200)
# A tibble: 10 × 3
url dest_url status
<chr> <chr> <int>
1 https://www.wikipedia.org/HmPsw2WtYSxSgZ6 https://en.wikipedia.org/Hm… 404
2 https://www.wikipedia.org/tF2KxtgdzehXaH9 https://en.wikipedia.org/tF… 404
3 https://www.wikipedia.org/xtgn1TlDJE8PPM9 https://en.wikipedia.org/xt… 404
4 https://www.wikipedia.org/8ESGr2Rn7YC7ktN https://en.wikipedia.org/8E… 404
5 https://www.wikipedia.org/f5NHoRoonRkdi0T https://en.wikipedia.org/f5… 404
6 https://www.wikipedia.org/DNbL6FfPm6QztsA https://en.wikipedia.org/DN… 404
7 https://www.wikipedia.org/8eLeJBm5SVbKUxT https://en.wikipedia.org/8e… 404
8 https://www.wikipedia.org/tubP9vI3wi8YxaP https://en.wikipedia.org/tu… 404
9 https://www.wikipedia.org/eJJDMz958gctfjW https://en.wikipedia.org/eJ… 404
10 https://www.wikipedia.org/eomyRJP0BqEE4Fj https://en.wikipedia.org/eo… 404
Timeouty
A pokud tam jsou i adresy, které nestihly timeout, pak jdou vypsat takhle:
|>
status_codes filter(is.na(status))
# A tibble: 1 × 3
url dest_…¹ status
<chr> <chr> <int>
1 https://itunes.apple.com/app/apple-store/id324715238?pt=208305… <NA> NA
# … with abbreviated variable name ¹dest_url
Případně je můžu znovu projet s vyšším timeoutem, třeba takhle:
|>
status_codes filter(is.na(status)) |>
pull(url) |>
future_map_dfr(check_url, 2)
A to je všechno :-)