A webaratás (web harvesting) vagy webkaparás (web scraping) egy olyan technika, amellyel automatizált módon ki tudjuk nyerni a weboldalakból a számunkra szükséges adatokat. Ezek éppúgy lehetnek hosszabb-rövidebb szövegek, mint táblázatos vagy strukturálatlan formában jelen lévő numerikus adatok. A cél tehát az, hogy egy olyan automatizmust hozzunk létre, amely a kérdéses adatokat kitermeli a megadott oldalakról és ezeket az igényeinknek megfelelő formába önti.
Ebben a blogposztban az 1896 és 2020 (2021) közötti nyári olimpiák nemzetek (nemzeti olimpiai bizottságok) szerinti bontásban elkészített éremtáblázatait aratom le a Wikipédiáról. Ezekre egy későbbi projekt miatt van szükségem.
Az 1896-os nyári olimpia éremtáblázata nemzetek szerinti bontásban a Wikipédián
Elöljáróban fontos még megjegyezni, hogy nem minden weboldal tulajdonosa szereti vagy engedi ezt a tevékenységet. A webaratás elkezdése előtt erről mindig előzetesen tájékozódni kell! A Wikipédia, mint szabad enciklopédia esetében ilyen korlátozás nincsen. Az oldalak a CC-BY-SA-3.0 licenc alapján használhatók. Ez a forrás megnevezésének feltételével lehetővé teszi az átdolgozások, származékos művek létrehozását.
Az olimpiai adatok learatásának elvi alapjai
A webaratás akkor hatékony, ha ugyanazzal a kóddal egyszerre sok oldalról tudjuk begyűjteni az adatokat. Jelen esetben 29 olimpiáról és ugyanennyi weblapról van szó. Ez már akkora mennyiség, amelynél érdemes belekezdeni a programozásba. A kérdéses Wikipédia oldalakon viszont nemcsak az általunk megszerezni kívánt a táblázatok szerepelnek, hanem sok minden más is. Ebben az információhalmazban kell valahogy megragadni ezeket a táblákat.
Az 1896-os nyári olimpia éremtáblázatának oldala az angol nyelvű Wikipédián
Az egyik lehetséges módszer a Dokumentum Objektum Modell (Document Object Model / DOM) elemzése. A DOM lényege az, hogy a weblap hátterében álló HTML kód minden egyes eleme egy hierarchikus felépítésű rendszer részét képezi. Egy fa alakú struktúrát kell itt elképzelni. Ebben a rendszerben természetesen a táblázatunkat reprezentáló HTML kódrészletnek is pontosan meghatározható helye van. Jelen esetben a probléma az, hogy nem teljesen azonos felépítésű a 29 Wikipédia oldal, vagyis a DOM hierarchiájában nem mindig ugyanazon a szinten helyezkedik el ez a táblázat. Márpedig ha minden oldalra külön szabályt kell alkotni, vagy legalábbis vizsgálni kell annak a lehetőségét, akkor az alaposan lerontja a munka hatékonyságát. A DOM-tól függetlenül hivatkozhatnánk egyébként a HTML kód egyes konkrét elemeire is. De mivel a minket érdeklő oldalakon több táblázat, azaz a forráskódban több <table>...</table>
elem található, ezért ezzel sem megyünk sokra.
Egy másik lehetőség annak kihasználása lenne, hogy a weboldalak kinézetét szabályozó CSS stílusleíró nyelv bizonyos esetekben megkívánja a HTML elemek egyedi vagy csoportos azonosítását. Ha például sok táblázat van egy oldalon, akkor az id=“azonosító” attribútum alkalmazásával egyedi neveket lehet rendelni azokhoz. Például így: <table id="azonosító">...</table>
. A táblázatok egy csoportja pedig, az előbbihez hasonló módon megadva, a class=“azonosító” attribútum megléte esetén hivatkozható. Ezeket az úgynevezett szelektorokat tehát azért építik be a weblapokba, hogy egyedi kinézetet biztosíthassanak az oldal egyes elemeinek. Ugyanakkor a szelektorok nemcsak a CSS alkalmazásakor hasznosak, hanem a webaratáskor is megkönnyíthetik a szükséges tartalom kiválasztását. Mert az azonosítók segítségével hivatkozni tudunk arra. Alább a CSS szelektorokat fogom felhasználni az olimpiák helyszínére vonatkozó adatok kiválasztásakor. Az éremtáblázatok esetében viszont sajnos ez sem segít nekünk.
A helyzet tehát az, hogy az érintett Wikipédia oldalakon rendszerint több táblázat is van, amelyek nincsenek egyedileg azonosítva, és a DOM-ban elfoglalt helyük is változó. De semmi gond! Egy kis agyalás után erre is van megoldás.
Ebben a helyzetben azt fogom csinálni, hogy egy character típusú vektorba letöltöm az aktuálisan vizsgált oldal HTML kódját. Vagyis a vektor minden eleme a HTML dokumentum egy-egy sorát reprezentálja majd. Az oldal kódjában biztos pontnak számít a “Rank” szó, mivel ez minden számunkra fontos táblázat fejlécében előfordul, az irreleváns táblázatokban viszont nem található meg. (Az angol nyelvű Wikipédiával dolgoztam.) Megkeresem tehát a HTML oldal azon sorát, amely ezt a szót tartalmazza. Mivel egy táblázatról beszélünk, így bizonyosak lehetünk abban, hogy valahol előtte kell lennie egy <table>
és utána egy </table>
HTML elemnek. Ezért lekérem azokat a sorokat is, amelyekben ezek előfordulnak. Itt nyilván számolnunk kell azzal az eshetőséggel, hogy az adott oldalon több táblázat szerepel és ennek megfelelően több találatot kapunk. A lehetséges sorok közül a <table>
elem esetében azt kell kiválasztanunk, amely a közvetlenül megelőzi vagy megegyezik a “Rank”-ot tartalmazó sorral, a </table>
elemnél pedig azt, amely közvetlenül követi vagy megegyezik az említett viszonyítási pont sorával. Ezután már minden további nélkül ki tudjuk szűrni a HTML dokumentumból a releváns sorokat, majd azokból a releváns részeket.
A feladat gyakorlati megvalósítása
Az alábbiakban R nyelven (v4.0.3) írt kódot használok a feladat végrehajtásához. A magyarázó szövegek közé ékelt fekete kódblokkok tartalmát az RStudio-ban egymás alá illesztve elvileg bárki által reprodukálható az itt bemutatott műveletsor. A kódblokkok # kezdetű sorai pusztán magyarázó funkcióval bírnak, ezekre a program futtatásakor nincs szükség.
Az olimpiai éremtáblázatok learatása során a szövegbányászati feladatokhoz a stringr, a webaratáshoz pedig az rvest csomagot használom. Az alább közölt kódot megjegyzésekkel láttam el az abban való könnyebb eligazodás érdekében.
# A szükséges csomagok betöltése.
# A legelső használat előtt az install.packages("...") utasítással telepíteni
# kell ezeket. A ... helyére az adott csomag neve írandó.
library(stringr)
library(rvest)
# Ebben az adatkeretben fogjuk gyűjteni a learatott adatokat.
tablaGyujto <- data.frame(orszag = as.character(),
arany = as.numeric(),
ezust = as.numeric(),
bronz = as.numeric(),
osszesen = as.numeric(),
ev = as.numeric(),
helyszin = as.character())
# A nyári olimpiák éveinek legenerálása.
ev <- setdiff(seq(1896, 2020, 4), c(1916, 1940, 1944))
# Az alábbiakban sorban végig megyünk az olimpiák évein és
# a megfelelő Wikipédia oldalról kinyerjük a szükséges adatokat.
for (i in ev) {
# Az adott évhez tartozó Wikipédia oldal URL-jének legenerálása.
url <- str_glue("https://en.wikipedia.org/wiki/{i}_Summer_Olympics_medal_table")
# Az oldal HTML kódjának beolvasása.
tabla <- readLines(url)
# A "Rank" szót tartalmazó sor helyének megállapítása az oldalon belül.
biztosPont <- str_which(tabla, "Rank")
# A "Rank"-hoz legközelebbi <table> elem helyének megállapítása.
tablaStart <- str_which(tabla, "<table")
tablaStart <- tail(tablaStart[tablaStart <= biztosPont], 1)
# A "Rank"-hoz legközelebbi </table> elem helyének megállapítása.
tablaStop <- str_which(tabla, "</table>")
tablaStop <- head(tablaStop[tablaStop >= biztosPont], 1)
# Az oldal HTML kódjának leszűkítése a táblázat helyére és
# a sorok összevonása egyetlen karakterláncba.
tabla <- tabla[tablaStart:tablaStop] %>%
paste(collapse = "")
# A karakterlánc elejének és végének megtisztítása a felesleges elemektől.
tabla <- str_sub(tabla, str_locate(tabla, "<table")[1], str_locate(tabla, "</table>")[2])
# Egy virtuális HTML dokumentum létrehozása a karakterláncból,
# majd ennek adatkeretté alakítása.
tabla <- minimal_html(tabla) %>%
html_table() %>%
as.data.frame()
# Az olimpia évének hozzáadása az adatkerethez.
tabla$ev <- i
# Az olimpia helyszínének begyűjtése, megtisztítása és hozzáadása
# az adatkerethez.
tabla$helyszin <- read_html(url) %>%
html_nodes(css = ".location") %>%
html_text() %>%
str_remove_all("\n") %>%
str_replace("/", "/ ") %>%
str_squish()
# A "Rank" nevű oszlop eltávolítása. Erre nincs szükségünk, mert egyébként
# bárhogy átrendezhetjük a táblázatot a jövőben.
tabla$Rank <- NULL
# Az adatkeret utolsó, összegző sorának eltávolítása.
tabla <- tabla[-nrow(tabla),]
# Az adatkeret oszlopainak átnevezése.
colnames(tabla) <- colnames(tablaGyujto)
# Az adatkeret tartalmának hozzáadása az eddig begyűjtött adatokhoz.
tablaGyujto <- rbind(tablaGyujto, tabla)
}
Az adatok összegyűjtése után a táblázatunk 7 mezőt és 1344 rekordot tartalmaz. Nézzük meg ennek az első olimpiára vonatkozó sorait! Ha jól dolgoztunk, akkor az adataink megegyeznek a fentebb képként beillesztett táblázat tartalmával.
tibble::as.tibble(tablaGyujto[tablaGyujto$ev == 1896,]) %>% print(n = Inf)
## # A tibble: 11 × 7
## orszag arany ezust bronz osszesen ev helyszin
## <chr> <int> <int> <int> <int> <dbl> <chr>
## 1 United States 11 7 2 20 1896 Athens, Greece
## 2 Greece* 10 18 19 47 1896 Athens, Greece
## 3 Germany 6 5 2 13 1896 Athens, Greece
## 4 France 5 4 2 11 1896 Athens, Greece
## 5 Great Britain 2 3 2 7 1896 Athens, Greece
## 6 Hungary 2 1 3 6 1896 Athens, Greece
## 7 Austria 2 1 2 5 1896 Athens, Greece
## 8 Australia 2 0 0 2 1896 Athens, Greece
## 9 Denmark 1 2 3 6 1896 Athens, Greece
## 10 Switzerland 1 2 0 3 1896 Athens, Greece
## 11 Mixed team 1 0 1 2 1896 Athens, Greece
Végezetül távolítsuk el az országok (nemzeti olimpiai bizottságok) neve mellett látható rövidítéseket és egyéb felesleges karaktereket egy reguláris kifejezéssel.
tablaGyujto$orszag <- str_remove(tablaGyujto$orszag, "\\s\\(.+")
Alább álljanak itt a táblázatból a Magyarországra vonatkozó adatok.
tibble::as.tibble(tablaGyujto[tablaGyujto$orszag == "Hungary",]) %>% print(n = Inf)
## # A tibble: 27 × 7
## orszag arany ezust bronz osszesen ev helyszin
## <chr> <int> <int> <int> <int> <dbl> <chr>
## 1 Hungary 2 1 3 6 1896 Athens, Greece
## 2 Hungary 1 2 2 5 1900 Paris, France
## 3 Hungary 2 1 1 4 1904 St. Louis, United States
## 4 Hungary 3 4 2 9 1908 London, Great Britain
## 5 Hungary 3 2 3 8 1912 Stockholm, Sweden
## 6 Hungary 2 3 4 9 1924 Paris, France
## 7 Hungary 4 5 0 9 1928 Amsterdam, Netherlands
## 8 Hungary 6 4 5 15 1932 Los Angeles, United States
## 9 Hungary 10 1 5 16 1936 Berlin, Germany
## 10 Hungary 10 5 12 27 1948 London, Great Britain
## 11 Hungary 16 10 16 42 1952 Helsinki, Finland
## 12 Hungary 9 10 7 26 1956 .mw-parser-output .plainlist ol,.mw…
## 13 Hungary 6 8 7 21 1960 Rome, Italy
## 14 Hungary 10 7 5 22 1964 Tokyo, Japan
## 15 Hungary 10 10 12 32 1968 Mexico City, Mexico
## 16 Hungary 6 13 16 35 1972 Munich, West Germany
## 17 Hungary 4 5 13 22 1976 Montreal, Canada
## 18 Hungary 7 10 15 32 1980 Moscow, Soviet Union
## 19 Hungary 11 6 6 23 1988 Seoul, South Korea
## 20 Hungary 11 12 7 30 1992 Barcelona, Spain
## 21 Hungary 7 4 10 21 1996 Atlanta, United States
## 22 Hungary 8 6 3 17 2000 Sydney, Australia
## 23 Hungary 8 6 3 17 2004 Athens, Greece
## 24 Hungary 3 5 2 10 2008 Beijing, China
## 25 Hungary 8 4 6 18 2012 London, Great Britain
## 26 Hungary 8 3 4 15 2016 Rio de Janeiro, Brazil
## 27 Hungary 6 7 7 20 2020 Tokyo, Japan
A Wikipédiáról learatott olimpiai adatokkal további terveim vannak. Hogy mit akarok ezekkel tenni, az majd kiderül a következő posztból. 🙂