Parsowanie XMLa w R

Jak w R obsłużyć dane zapisane w XML? Do redakcji ;-) przychodzą dziesiątki, jeśli nie setki ;-)) maili w których pytacie jak pobrałem dane z Veturilo, które wykorzystałem wiosną tego roku we wpisach jeden oraz dwa. Pobrałem je z pliku XML. Było to w PHP, ale da się też w R. Do obsługi XMLa jest oczywiście odpowiedni pakiet o jakże zaskakującej nazwie XML.

library(tidyverse) # tibble(), %>%
library(lubridate) # obsługa dat

library(RCurl) # pobranie plików XML z internetu
library(XML) # obsługa XMLa

Zanim dojdziemy do Veturilo przećwiczymy obsługę XMLa na przykładzie kanału RSS (plik dla czytnika RSS jest również XMLem). Jeśli nie znasz składni XMLa warto o niej doczytać. Za przykład niech posłuży RSS niniejszego bloga. getURL() z pakietu RCurl pobierze nam zawartość pliku (może to być też plik z dysku - wtedy można go wczytać na przykład przez read_lines() i skleić linie w jeden ciąg na przykład poprzez paste() z parametrem collapse = ""):

rss_url <- "/index.php/feed/"
rss_feed <- getURL(rss_url)

Mając ciąg znaków z zawartością pliku XML musimy go przetworzyć na odpowiedni obiekt, z którego będziemy wyłuskiwać interesujące nas dane.

xmlData <- xmlParse(rss_feed, options=HUGE, useInternalNodes=TRUE)

Na początek zobaczmy jakie części mamy w korzeniu pliku:

xmlRoot(xmlData) %>% names()
##   channel 
## "channel"

Widać tylko jedną, zobaczmy co siedzi w środku:

xmlRoot(xmlData)[["channel"]] %>% names()
##             title              link              link       description 
##           "title"            "link"            "link"     "description" 
##     lastBuildDate          language      updatePeriod   updateFrequency 
##   "lastBuildDate"        "language"    "updatePeriod" "updateFrequency" 
##         generator              item              item              item 
##       "generator"            "item"            "item"            "item" 
##              item              item              item              item 
##            "item"            "item"            "item"            "item" 
##              item              item              item 
##            "item"            "item"            "item"

Tutaj jest kilka elementów opisujących kanał RSS i sporo elementów typu item. Gdy otworzymy plik w jakimś notatniku czy nawet w przeglądarce łatwo zrozumiemy co jest czym. Zobaczmy co składa się (jakie pola) na obiekt typu item:

xmlRoot(xmlData)[["channel"]][["item"]] %>% names()
##         title          link      comments       pubDate       creator 
##       "title"        "link"    "comments"     "pubDate"     "creator" 
##      category      category      category      category      category 
##    "category"    "category"    "category"    "category"    "category" 
##      category          guid   description    commentRss      comments 
##    "category"        "guid" "description"  "commentRss"    "comments"

O! To jest mięsko które nas interesuje. Tu znajdziemy tytuł wpisu (title), link do niego (link), datę publikacji (pubDate) oraz treść (description). Do wartości każdego z elementów możemy dotrzeć poprzez funkcję xmlValue(). Sprowadźmy więc wszystkie wartości elementów z obiektów typu item do ramki danych:

rss_df <- tibble(
   title = xpathSApply(xmlData, "//channel/item/title", xmlValue),
   pubDate = xpathSApply(xmlData, "//channel/item/pubDate", xmlValue) %>%
      parse_date_time(., "%a, %d %b %Y %H:%M:%S %z", locale = "US") %>%
      with_tz("Europe/Warsaw"),
   description = xpathSApply(xmlData, "//channel/item/description", xmlValue),
   link = xpathSApply(xmlData, "//channel/item/link", xmlValue)
)

I gotowe! Prawda, że banalne? Wynik jest następujący:

rss_df
titlepubDatedescriptionlink
Przez losowy las Spotify2017-12-10 09:16:32Czy machine learning może pomóc w poszukiwaniu ulubionej muzyki? Dzisiaj zajmiemy się poszukiwaniem nowej muzyki. Już kiedyś o tym było w oparciu o LastFM, dzisiaj będzie w oparciu o Spotify. Taki szybki wpis. Na początek potrzebujemy kilku elementów: własnej aplikacji współpracującej z API Spotify – zbudować ją można zaczynając z dashboardu Spotify, potrzebujemy numerku client_id … Czytaj dalej Przez losowy las Spotify</2017/12/10/spotify-random-forest/>
Analiza procesów2017-12-02 08:27:35W którym miejscu można skrócić proces, na której fazie trwa on najdłużej? Dzisiaj zajmiemy się analizą dowolnego procesu, w którym można wskazać jakieś fazy. Zadanie wzięło się z pracy, którą wykonuję na co dzień: który element procesu wytwórczego oprogramowania trwa najdłużej? Czy to, że przygotowanie systemu IT trwa tak długo to wina IT (analiza wymagań, … Czytaj dalej Analiza procesów</2017/12/02/analiza-procesow/>
Wino2017-11-23 08:01:52Jakie wina sprzedają się najlepiej? Czy Polacy lubią wino czerwone czy białe? Słodkie czy wytrawne? Jak wybrać wino dobre i tanie? Dane na potrzeby analizy pobierzemy ze sklepu internetowego. Odpowiedni skrypt (i pobrane dane) znajdziecie w repo na GitHubie, w pliku get_data.R. Skrypt jest dość długi, nie wnosi niczego ciekawego do samej analizy. Po zebraniu … Czytaj dalej Wino</2017/11/23/wino/>
Sondaże przedwyborcze2017-11-17 13:36:51Komu rośnie, a komu spada? Czy PSL przekroczy próg 5% i załapie się do Sejmu? Czy “totalna opozycja” już przegrywa z partią rządzącą czy jeszcze nie? Takie pytania rozgrzewają komentatorów polskiej sceny politycznej mniej więcej raz w tygodniu, przy okazji publikacji kolejnych sondaży przedwyborczych. Zajmiemy się więc tymi sondażami. Długo szukałem archiwalnych danych sondażowych, aż … Czytaj dalej Sondaże przedwyborcze</2017/11/17/sondaze-przedwyborcze/>
Urodziny w Wikipedii2017-11-09 15:21:41Przedstawiciele jakiego zawodu są najczęściej opisywani w Wikipedii? Czy miesiąc urodzenia predysponuje do wykonywania danego zawodu? Czy w polskiej Wiki więcej jest o Polakach czy innych nacjach? Pomysł Któregoś dnia, siedzimy w knajpie z J.P. a właściwie z J.A., z okazji jego urodzin. I tak od słowa do słowa pada “a dzisiaj urodziny ma też … Czytaj dalej Urodziny w Wikipedii</2017/11/09/urodzenia-wg-wikipedii/>
Jaki film obejrzeć?2017-10-27 10:54:39Kiedyś już pisałem o filmach i danych jakie dawno temu zgromadziłem z Filmwebu. Dzisiaj zajmiemy się polecaniem filmów do obejrzenia. Czyli – systemy rekomendacji. Dlaczego rekomendacje są ważne W najprostszym ujęciu chodzi o sprzedaż. Albo dosłowną sprzedaż towarów, albo odsłony (czy też odtworzenia) w serwisach internetowych (i tym samym odsłony reklam). Im więcej ludziom polecisz, … Czytaj dalej Jaki film obejrzeć?</2017/10/27/na-co-do-kina/>
Ankieta – wyniki (i jak je podsumować w R)2017-10-17 16:38:17Wyniki ankiety to jedno, a drugie to narzędzie do ich uzyskania i przedstawienia. Łączymy przyjemne z pożytecznym. Ankietę przygotowałem korzystając z Google Forms – to jedna z najwygodniejszych opcji (systemów do ankiet jest wiele), szczególnie jeśli chcemy mieć od razu dane (odpowiedzi) w postaci tabeli (w tym przypadku arkusza Google Sheets). Kiedy ankieta jeszcze trwała … Czytaj dalej Ankieta – wyniki (i jak je podsumować w R)</2017/10/17/ankieta-wyniki/>
Kalendarz z Policją2017-10-14 09:54:47W jednym z ostatnich odcinków “Ucha prezesa” było coś o tym, że teraz rząd ma mówić o tym, że jest bezpiecznie (zamiast “Przez osiem lat Polki i Polacy…”). Sprawdzimy zatem bezpieczeństwo na polskich drogach. Ale punktem wyjścia będzie specyficzny sposób pokazania danych dziennych. Widok kalendarza Dane dzienne (takie, które mają jedną wartość dla każdego dnia … Czytaj dalej Kalendarz z Policją</2017/10/14/kalendarz-z-policja/>
Kto pracuje najbardziej efektywnie w Europie?2017-10-09 13:41:04Czy wydajność pracy w różnych krajach Europy jest jednakowa? Jak skorzystać z danych Eurostatu w R? Jestem fanem facebookowej strony z różnymi mapkami. Na stronie tej pokazał się ostatnio obrazek z informacją o średniej liczbie godzin jaką w pracy spędzają mieszkańcy poszczególnych krajów (linkował do tego postu). Ale szczerze powiedziawszy co to za informacja? To … Czytaj dalej Kto pracuje najbardziej efektywnie w Europie?</2017/10/09/kto-pracuje-najbardziej-efektywnie-w-europie/>
Ankieta!2017-10-04 16:33:09Czytasz tego bloga? Jesteś fanem Dane i Analizy na Facebooku? (dlaczego nie?!) Wypełnij koniecznie ankietę! Zajmie Ci to góra pięć minut.</2017/10/04/ankieta/>

Teraz czas na Veturilo. NextBike wystawia odpowiedniego XMLa ze stanem stacji we wszystkich obsługiwanych miastach. Na Warszawę składają się “miasta” (city) o numerze 210 (stacje miejskie) oraz 372 (stacje sponsorskie). Ponieważ w Warszawie sezon Veturilo się skończył i na stacjach nie ma rowerów weźmiemy inne miasto - takie z numerem 1 (bodaj Lipsk w Niemczech). Na chwilę obecną (2017-12-12 13:52:08) to działało. Oczywiście można wczytywać całego XMLa, ale szkoda ruchu sieciowego, stąd parametr ?city=xx (z odpowiednim xx) który zawęża wyniki do wskazanego miasta. Jeśli interesują Cię inne miasta - zerknij do XMLa od NextBike i wyszukaj w nim odpowiedni numerek.

xml_url <- "http://nextbike.net/maps/nextbike-official.xml?city=1"
xml_file <- getURL(xml_url)

Podobnie jak poprzednio parsujemy XMLa i szukamy odpowiedniego poziomu, do którego trzeba sięgnąć:

xmlData <- xmlParse(xml_file, options=HUGE, useInternalNodes=TRUE)

# kilka zbędnych linii dojścia to poniższego miejsca ;)

xmlRoot(xmlData)[["country"]][["city"]][["place"]] %>% names()
##   bike   bike   bike 
## "bike" "bike" "bike"

Tym razem jednak wartości jakie nas interesują są w ramach parametrów tagu XML. Zatem zamiast użyć xmlValue() użyjemy xmlAttrs(). Wyciągniemy na początek atrybuty wszystkich node’ów typu place do listy:

lista <- xpathSApply(xmlData, "//country/city/place", xmlAttrs)

a następnie listę przepuścimy (element po elemencie) przez funkcję, która wybierze z atrybutów to co nas interesuje:

# funkcja wyciągająca interesujące nas dane
getPlaceData <- function(l) {

   tibble(
      # rozbijamy listę numerów rowerów na pojedyncze sztuki
      bike_number = l["bike_numbers"] %>%
         strsplit(",") %>%
         unlist(), 
      
      # dodajemy pozostałe dane o stacji:
      bikes =l["bikes"], # liczba rowerów przypiętych na stacji

      ## w bazie danych wystardczyłby np. UID stacji,
      ## reszta to stałe dane - do trzymania w osobnej tabeli
      uid =l["uid"], # unikalny ID stacji (chyba w całym systemie NextBike)
      number = l["number"], # numer stacji (chyba w mieście)
      long = l["lng"], lat = l["lat"], # położenie stacji
      name = l["name"] # nazwa stacji
      )
}

# każdy element listy przepuszczamy przez powyższą funkcję
rowery <- lista %>% map_df(getPlaceData)

Funkcja buduje mini-tabelkę, gdzie każdy wiersz zawiera wszystkie dane o stacji i numer roweru na tej stacji. Takich wierszy jest tyle ile rowerów stoi na stacji. Dzięki temu w przyszłości wystarczy w dużej tabeli (takiej z kilku dni) wyszukać numer roweru i dostaniemy jego drogę (konkretnie: numery stacji na jakich był przypięty). Oczywiście aby mieć historię trzeba dane z XMLa pobierać co jakiś czas (na przykład co 5 minut). I uzupełniać je o timestamp. Zobaczmy do otrzymujemy dla naszego XMLa:

head(rowery, 20)
bike_numberbikesuidnumberlonglatname
10588328401312.36881375312851.340505159701Gottschedstr./Bosestr.
10141328401312.36881375312851.340505159701Gottschedstr./Bosestr.
20150328401312.36881375312851.340505159701Gottschedstr./Bosestr.
NA072400312.38155424594951.339077567821Augustusplatz/Oper
070265125400612.37321257591251.339637215946Thomaskirchhof/Taxistand
070335125400612.37321257591251.339637215946Thomaskirchhof/Taxistand
056785125400612.37321257591251.339637215946Thomaskirchhof/Taxistand
106225125400612.37321257591251.339637215946Thomaskirchhof/Taxistand
070225125400612.37321257591251.339637215946Thomaskirchhof/Taxistand
101891128401112.37285315990451.337076853681Burgplatz/Stadthaus
100271801401412.37331986427351.320789736538K.-Liebknecht-Str./K.-Eisner-Str./Bäcker
NA01615400712.36518204212251.343068681395Jahnallee/Thomasiusstr./Denkmal
1071951727400512.37339496612551.324537727221Südplatz (LVB Mobilitätsstation 10)
0553451727400512.37339496612551.324537727221Südplatz (LVB Mobilitätsstation 10)
0551551727400512.37339496612551.324537727221Südplatz (LVB Mobilitätsstation 10)
0565651727400512.37339496612551.324537727221Südplatz (LVB Mobilitätsstation 10)
2051551727400512.37339496612551.324537727221Südplatz (LVB Mobilitätsstation 10)
1074242031402112.36719906330151.332032789785Grassistr./Beethovenstr.
0563942031402112.36719906330151.332032789785Grassistr./Beethovenstr.
0578442031402112.36719906330151.332032789785Grassistr./Beethovenstr.

Te dane można opatrzyć aktualną datą i godziną oraz zapisać na przykład do bazy danych. Ten prosty kod (pobranie XMLa z sieci, parsowanie XMLa i wyciągnięcie odpowiednich danych) możemy zapakować w skrypt uruchamiany cyklicznie i wstawić go do unixowego Crona lub jakiegoś Task Schedulera w Windows. Są nawet R-pakiety do tego. Podsumowując - aby pobrać dane z XMLa trzeba:

  • pooglądać tego XMLa, najwygodniej w edytorze tekstowym
  • po przeanalizowaniu struktury (sprawdzamy drzewko node’ów oraz czy wartości są w tagach czy jako ich atrybuty) na sucho przechodzimy do R:
  • parsujemy XMLa (xmlParse)
  • dla każdego node’a (używając do tego celu xpathSApply) wykonujemy xmlValue lub xmlAttrs

Prawda, że proste? Tylko strasznie upierdliwe, bo każdy XML może być inny… a te z Sharepointa to zgroza (szczególnie nazwy pól). A jak w R pobrać dane z listy Sharepoint? Trzeba pobrać XMLa, co jest najtrudniejszym elementem. Dam Wam hint, bo przez to przechodziłem przez to niedawno:

GetSharepointList <- function(SiteURL, ListID, ViewID, UserName, UserPass) {
   rawData <- getURL(paste0(SiteURL, "/_vti_bin/owssvr.dll?Cmd=Display",
                            "&List=", ListID,
                            "&View=", ViewID,
                            "&Query=*" ,
                            "&rowLimit=0",
                            "&XMLDATA=1",
                            "&noredirect=true"),
                     userpwd = paste0(UserName, ":", UserPass),
                     .encoding = 'UTF-8',
                     .mapUnicode = FALSE,
                     capath = system.file("CurlSSL", "cacert.pem", package = "RCurl"),
                     ssl.verifyhost = FALSE,
                     ssl.verifypeer = FALSE,
                     verbose = FALSE)
   
   # przekształcenie XMLa na listę
   xmlData <- xmlParse(rawData, options=HUGE, useInternalNodes=TRUE)
   rs_data_list <- xmlApply(xmlRoot(xmlData)[["data"]], xmlAttrs)
   
   return(rs_data_list)
}

To gotowa funkcja, która w parametrach przyjmuje:

  • URL do witryny Sharepoint
  • ID listy
  • ID widoku
  • login i hasło (otwartym tekstem) dostępu do Sharepointa

W odpowiedzi dostajemy listę (w rozumieniu obiektu typu list) z zawartością.