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
| 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 | </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 | </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 | </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 | </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 | </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ć? | </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) | </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ą | </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? | </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. | </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_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) wykonujemyxmlValuelubxmlAttrs
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ą.