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.
1 2 3 4 5 |
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 = ""
):
1 2 |
rss_url <- "http://blog.prokulski.science/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.
1 |
xmlData <- xmlParse(rss_feed, options=HUGE, useInternalNodes=TRUE) |
Na początek zobaczmy jakie części mamy w korzeniu pliku:
1 |
xmlRoot(xmlData) %>% names() |
1 2 |
## channel ## "channel" |
Widać tylko jedną, zobaczmy co siedzi w środku:
1 |
xmlRoot(xmlData)[["channel"]] %>% names() |
1 2 3 4 5 6 7 8 9 10 |
## 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:
1 |
xmlRoot(xmlData)[["channel"]][["item"]] %>% names() |
1 2 3 4 5 6 |
## 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:
1 2 3 4 5 6 7 8 |
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:
1 |
rss_df |
title | pubDate | description | link |
---|---|---|---|
Przez losowy las Spotify | 2017-12-10 09:16:32 | Czy 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 | http://blog.prokulski.science/index.php/2017/12/10/spotify-random-forest/ |
Analiza procesów | 2017-12-02 08:27:35 | W 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 | http://blog.prokulski.science/index.php/2017/12/02/analiza-procesow/ |
Wino | 2017-11-23 08:01:52 | Jakie 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 | http://blog.prokulski.science/index.php/2017/11/23/wino/ |
Sondaże przedwyborcze | 2017-11-17 13:36:51 | Komu 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 | http://blog.prokulski.science/index.php/2017/11/17/sondaze-przedwyborcze/ |
Urodziny w Wikipedii | 2017-11-09 15:21:41 | Przedstawiciele 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 | http://blog.prokulski.science/index.php/2017/11/09/urodzenia-wg-wikipedii/ |
Jaki film obejrzeć? | 2017-10-27 10:54:39 | Kiedyś 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ć? | http://blog.prokulski.science/index.php/2017/10/27/na-co-do-kina/ |
Ankieta – wyniki (i jak je podsumować w R) | 2017-10-17 16:38:17 | Wyniki 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) | http://blog.prokulski.science/index.php/2017/10/17/ankieta-wyniki/ |
Kalendarz z Policją | 2017-10-14 09:54:47 | W 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ą | http://blog.prokulski.science/index.php/2017/10/14/kalendarz-z-policja/ |
Kto pracuje najbardziej efektywnie w Europie? | 2017-10-09 13:41:04 | Czy 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? | http://blog.prokulski.science/index.php/2017/10/09/kto-pracuje-najbardziej-efektywnie-w-europie/ |
Ankieta! | 2017-10-04 16:33:09 | Czytasz tego bloga? Jesteś fanem Dane i Analizy na Facebooku? (dlaczego nie?!) Wypełnij koniecznie ankietę! Zajmie Ci to góra pięć minut. | http://blog.prokulski.science/index.php/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.
1 2 |
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ąć:
1 2 3 4 5 |
xmlData <- xmlParse(xml_file, options=HUGE, useInternalNodes=TRUE) # kilka zbędnych linii dojścia to poniższego miejsca ;) xmlRoot(xmlData)[["country"]][["city"]][["place"]] %>% names() |
1 2 |
## 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:
1 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 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:
1 |
head(rowery, 20) |
bike_number | bikes | uid | number | long | lat | name |
---|---|---|---|---|---|---|
10588 | 3 | 28 | 4013 | 12.368813753128 | 51.340505159701 | Gottschedstr./Bosestr. |
10141 | 3 | 28 | 4013 | 12.368813753128 | 51.340505159701 | Gottschedstr./Bosestr. |
20150 | 3 | 28 | 4013 | 12.368813753128 | 51.340505159701 | Gottschedstr./Bosestr. |
NA | 0 | 72 | 4003 | 12.381554245949 | 51.339077567821 | Augustusplatz/Oper |
07026 | 5 | 125 | 4006 | 12.373212575912 | 51.339637215946 | Thomaskirchhof/Taxistand |
07033 | 5 | 125 | 4006 | 12.373212575912 | 51.339637215946 | Thomaskirchhof/Taxistand |
05678 | 5 | 125 | 4006 | 12.373212575912 | 51.339637215946 | Thomaskirchhof/Taxistand |
10622 | 5 | 125 | 4006 | 12.373212575912 | 51.339637215946 | Thomaskirchhof/Taxistand |
07022 | 5 | 125 | 4006 | 12.373212575912 | 51.339637215946 | Thomaskirchhof/Taxistand |
10189 | 1 | 128 | 4011 | 12.372853159904 | 51.337076853681 | Burgplatz/Stadthaus |
10027 | 1 | 801 | 4014 | 12.373319864273 | 51.320789736538 | K.-Liebknecht-Str./K.-Eisner-Str./Bäcker |
NA | 0 | 1615 | 4007 | 12.365182042122 | 51.343068681395 | Jahnallee/Thomasiusstr./Denkmal |
10719 | 5 | 1727 | 4005 | 12.373394966125 | 51.324537727221 | Südplatz (LVB Mobilitätsstation 10) |
05534 | 5 | 1727 | 4005 | 12.373394966125 | 51.324537727221 | Südplatz (LVB Mobilitätsstation 10) |
05515 | 5 | 1727 | 4005 | 12.373394966125 | 51.324537727221 | Südplatz (LVB Mobilitätsstation 10) |
05656 | 5 | 1727 | 4005 | 12.373394966125 | 51.324537727221 | Südplatz (LVB Mobilitätsstation 10) |
20515 | 5 | 1727 | 4005 | 12.373394966125 | 51.324537727221 | Südplatz (LVB Mobilitätsstation 10) |
10742 | 4 | 2031 | 4021 | 12.367199063301 | 51.332032789785 | Grassistr./Beethovenstr. |
05639 | 4 | 2031 | 4021 | 12.367199063301 | 51.332032789785 | Grassistr./Beethovenstr. |
05784 | 4 | 2031 | 4021 | 12.367199063301 | 51.332032789785 | Grassistr./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
) wykonujemyxmlValue
lubxmlAttrs
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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ą.