Kontrola stavových kódů většího počtu URL v R

Potřeboval jsem rychle zkontrolovat stavové kódy HTTP dlouhého seznamu URL. Přitom jsem si vyzkoušel balíček furrr na paralelní zpracování a moc se mi zalíbil.

Author
Affiliation
Marek Prokop
Published

April 9, 2022

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).

library(tidyverse)
library(furrr)
library(rvest)
library(httr)
library(tictoc)

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:

start_url <- "https://www.wikipedia.org/"

urls <- read_html(start_url) |>
  html_elements("a") |>
  html_attr("href") |>
  xml2::url_absolute(start_url) |> 
  c(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.

check_url <- function(url, timeout) {
  resp <- try(HEAD(url, timeout(timeout)), silent = TRUE)
  if (class(resp) == "try-error") {
    status <- NA_integer_
    dest_url <- NA_character_
  } else {
    status <- resp$status_code
    dest_url <- resp$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()
status_codes <- urls |>
  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()
status_codes <- urls |>
  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 :-)