R Learning Notes: XML, JSON

此篇僅做記錄用,本文所有文字內容,皆來自:
R語言翻轉教室


這門課程的目的是想要教大家如何處理XML或HTML的資料。這是大部分的網頁資料(HTML)所採用的格式。

我們要介紹的是R 的xml2套件

XML的全名是eXtensible Markup Language,是一種讓電腦可以快速理解資訊的標記語言。XML的透過標記來讓電腦理解資訊的內容,並且清楚的切割開標籤與內容。

如果我們收到一個文件,內容如下: ‘<th>廠商名稱</th><td>台灣翔登股份有限公司</td>’

th是「廠商名稱」這段文字的標籤。


現代的網頁都是以類似的格式傳遞資訊,讓電腦進行處理。

而除了簡單的標籤與內文之外,我們也可以定義標籤的屬性。 舉例來說:”<title type=’regular’>RDataEngineer-02-XML</title>”這樣的訊息中,除了標籤與內容外,電腦還會知道,這個標籤「title」還附帶有屬性:「type」,而且屬性的值是「regular」。


XML的文件中,標籤可以有結構關係。

我們拿一小段稍後要處理的文件作範例:

‘<tr> <th class=”T11b” bgcolor=”#ffdd83" align=”left” valign=”middle” width=”200">廠商名稱</th><td class=”newstop” bgcolor=”#EFF1F1">台灣翔登股份有限公司</td> </tr>’
這份文件中,th和td標籤以及他們的內容,都會被歸類在tr標籤之內。

依照慣例,我們會將tr稱為th的父標籤(parent),而th與td兩者都是tr的子標籤(children),每個標籤最多只有一個父標籤。這是因為th和td兩個標籤,寫在<tr>和</tr>之間。

在HTML網頁中,幾乎所有的標籤都有父標籤,除了html這個標籤以外。所以我們在處理HTML文件時,會稱呼這個標籤為整個文件的根(root)。

我們已經準備了一段簡單的XML文件,並且儲存於變數`x1`

x1

[1] “<a><b>B</b><c>C1</c><c class=’x’>C2</c></a>”


在利用xml2套件處理XML或HTML文件之前,必須要先作解析,將文件建立成一種特殊的R物件後,才能讓進行挖掘資訊。 這裡我們使用的是`read_xml`函數。

據說明文件,`read_xml`函數有`x`、`encoding`和其他參數可以使用。

`x`則可以是一個檔案路徑(file path)、一個網址(url),或是一個XML文本的字串向量(literal xml)。


doc1 <- read_xml(x1)
doc1

{xml_document}
<a>
[1] <b>B</b>
[2] <c>C1</c>
[3] <c class=”x”>C2</c>

目前xml2中的物件,大致上可以分成三種:xml_document、xml_node和xml_nodeset。

xml_document就代表整個XML文件。

xml_node對應到上述介紹的XML標籤。

而在經過`read_xml`後,每個標籤會被轉化為一個xml_node,xml_nodeset則是一群標籤的集合。


接下來的例子會具體介紹如何使用這些物件。

在挖掘網頁資訊時,最困難的部分就是要從成千上萬的標籤中,找到我們感興趣的,再將標籤內的資訊(可能是屬性,也可能是內容)給擷取出來。

挖掘網頁資訊的三步驟:

1. 找到標籤

2. 查詢屬性

3.檢查內容


第一部分找標籤是這些步驟中最困難的,因為每份文件的標籤可能都不同,我們需要先找出我們感興趣的內容屬於那一類的標籤,接著再用xml2等套件把我們想找的標籤給定位出來。

第一段的方式,必須要透過其他工具的輔助,目前在R中並沒有很好的方法,只能透過嘗試、嘗試、再嘗試才能找到我們的目標標籤。

這裡我們要介紹的是,在已經知道目的標籤時,如何利用xml2來找出目標。

?xml_find_all

據說明文件,`xml_find_all`共有兩個參數:`x`與`xpath`。

`x`可以是xml_document、xml_node或xml_nodeset。 而`xpath`(XML Path Language)則是一種特別的格式,讓我們可以和電腦溝通我們要搜尋的標籤。

xml_find_all(doc1,”/a/b”)

{xml_nodeset (1)}
[1] <b>B</b>

同學會看到`xml_find_all`找了唯一的標籤b給我們。

xpath最後類似路徑的格式,其實就是在描述標籤的相對位置。


但是如果我們輸入的是:

> xml_find_all(doc1, “/b”)
{xml_nodeset (0)}

我們可以看到`xml_find_all`回報沒有找到任何結果。

在XPath的規範中,我們要尋找的標籤名稱,就是整個路徑的最後一個位置。

所以”/a”就代表要找”<a>…</a>”,而”/b”則代表找”<b>…</b>”,斜線則代表標籤在文件中的相對位置。

“/a”代表這個標籤在根部,也就是沒有父標籤。
 “/a/b”則代表這個標籤”<b>…</b>”的父標籤是”<a>…</a>”,並且再往父標籤的方向走,就到底了。


ns <- xml_find_all(doc1, “/a/c”)

{xml_nodeset (2)}
[1] <c>C1</c>
[2] <c class=”x”>C2</c>


我們可以透過`[[`和`[`,從xml_nodeset中取出xml_node或是xml_nodeset。

xml2在這邊的設計和R 的list非常接近,所以同學可以用處理list的經驗來作判斷。


我們也可以用`xml_parent`來看一個標籤的父標籤。

xml_parent(n1)

{xml_node}
<a>
[1] <b>B</b>
[2] <c>C1</c>
[3] <c class=”x”>C2</c>


另一個檢查內容的函數是`xml_contents`。 當含有子標籤時,它和`xml_text`的行為會不一致。

xml_text(a)

[1] “BC1C2”

同學會看到在<a>…</a>之間的所有文字。
xml_contents(a)

{xml_nodeset (3)}
[1] <b>B</b>
[2] <c>C1</c>
[3] <c class=”x”>C2</c>

R 就會回傳一個xml_nodeset給我們。
xml_children(a)

{xml_nodeset (3)}
[1] <b>B</b>
[2] <c>C1</c>
[3] <c class=”x”>C2</c>

透過`xml_children(a)`來取得所有以a為父標籤的標籤們。


回到前面討論的:

ns <- xml_find_all(doc1, “/a/c”)

{xml_nodeset (2)}
[1] <c>C1</c>
[2] <c class=”x”>C2</c>

我們可以把第一個c標籤,存到變數`n1`。

n1 <- ns[[1]]
xml_text(n1)

C1

n2 <- ns[[2]]
xml_test(n2)

C2

節點`n2`和`n1`。 這兩個節點在結構上不同的,`n2`附帶了屬性,而`n1`沒有。

xml_attrs(n1)

named character(0)

看到一個空的結果,因為第一個c標籤並沒有夾帶屬性的資訊
xml_attrs(n2)

class 
 “x”

一個帶有名字的字串向量。其中名稱為”class”的元素的值為”x”。

在使用XPath尋找標籤時,屬性是可以派上用場的。

> xml_find_all(doc1,”/a/c[@class]”)

{xml_nodeset (1)}
[1] <c class=”x”>C2</c>

就在搜尋時增加:「標籤必須要帶有名稱為”class”的屬性」,所以這時候R就只會回傳第二個c標籤,因為第一個c標籤並不帶有 class屬性。


我們甚至可以指定屬性的值

xml_find_all(doc1, “/a/c[@class=’g’]”)

{xml_nodeset (0)}

就代表我們要找的c標籤不只是有class屬性而已,這個屬性還必須要是”g”。在這裡,也要請同學注意我們是如何交替的使用雙引號和單引號。

由於這裡的”g”必須要加上引號,但是整個文字的外面已經套上雙引號,如果重複使用雙引號的話會造成R在判斷字串的困難。 因此這裡要使用單引號。


最後我們要介紹一種在XPath中常常使用的定位方式:”//”。 這裡的”//”代表的就是任意位置。

當我們在處理複雜的網頁資料時,如果每次都要從根部尋找正確的路徑,是非常不方便的。此時,透過”//a”,我們就可以找到在所有位置都出現的a標籤。


以上的課程內容,我們介紹了在給定標籤的名稱(a標籤<a>…</a>或b標籤<b>…</b>)、標籤的位置(根部是”/”,任意位置是”//”)及標籤的屬性後,如何利用xml_find_all來搜尋標籤。

我們也知道當找到這些標籤後,要怎麼取出標籤的內容(xml_contents和xml_text)與屬性(xml_attrs)。

另外我們也可以沿著標籤往父標籤(xml_parent),或是取出子標籤(xml_children)。

這些操作都是向量式的喔!可以對xml_nodeset使用如xml_text等函數,一次操作大量的xml_nodeset。


作業:抓出資料內的廠商名稱

1. 檢視檔案的內容,並用網頁的方式開啟觀察

readLines(tender_path, n = 100)
browseURL(tender_path)

2. 載入網頁內容

tender <- read_html(tender_path)
透過瀏覽器的開發工具可以發現,裝載著廠商名稱的標籤是像這樣的:tr > th &td

<tr>
 <th class=”T11b” bgcolor=”#ffdd83" align=”left” valign=”middle” width=”200"> 廠商名稱</th>
 <td class=”newstop” bgcolor=”#EFF1F1"> 台灣翔登股份有限公司</td></tr>

所以我們的做法是:
 1. 找出所有的tr標籤
 2. 找出底下有th的tr
 3. th的內容必須要是”廠商名稱”

3. 找出所有上一層是tr的th標籤的 nodesets

ths <-xml_find_all(tender,”//tr/th”)

4. 取出每個ths中th標籤的值

ths_text <- xml_text(ths)

5. 拿ths_text和 “ 廠商名稱” 進行比較,找出TRUE

player_name_reference <- rawToChar(as.raw(c(227L, 128L, 128L, 229L, 187L, 160L, 229L, 149L, 134L, 229L,144L, 141L, 231L, 168L, 177L)))

[1] “ 廠商名稱”

is_target <- ths_text == player_name_reference

[1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [14] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [27] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE TRUE FALSE FALSE FALSE

5. 利用 `[`,從ths中選出那些值為 “ 廠商名稱” 的xml_nodeset。

ths2 <- ths[is_target]

{xml_nodeset (4)}
[1] <th width=”200" align=”left” valign=”middle” bgcolor=”#ffdd83" class=”T11b”> 廠商名稱 …
[2] <th width=”200" align=”left” valign=”middle” bgcolor=”#ffdd83" class=”T11b”> 廠商名稱 …
[3] <th width=”200" align=”left” valign=”middle” bgcolor=”#ffdd83" class=”T11b”> 廠商名稱 …
[4] <th width=”200" align=”left” valign=”middle” bgcolor=”#ffdd83" class=”T11b”> 廠商名稱 …
>

6. 目標是找到td的內容,先回到tr層級

trs <- xml_parent(ths2)

7. 然後,透過tr找到他的子標籤(td &th),取出這些標籤的值

trs_children <- xml_children(trs)
trs_children_text <- xml_text(trs_children)

[1] “ 廠商名稱” 
[2] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t連距電機股份有限公司\r\n\t\t”
[3] “ 廠商名稱” 
[4] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t台灣翔登股份有限公司\r\n\t\t”
[5] “ 廠商名稱” 
[6] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t愛悌股份有限公司\r\n\t\t” 
[7] “ 廠商名稱” 
[8] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t邰莫電機股份有限公司\r\n\t\t”

8. 挑出不是“ 廠商名稱”的值。

players <- trs_children_text[trs_children_text != player_name_reference]

[1] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t連距電機股份有限公司\r\n\t\t”
[2] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t台灣翔登股份有限公司\r\n\t\t”
[3] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t愛悌股份有限公司\r\n\t\t” 
[4] “\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t邰莫電機股份有限公司\r\n\t\t”


JSON

這門課程要跟各位同學介紹JSON(JavaScript Object Notation)資料的處理。

JSON在網際網路上已經是最廣泛使用的資料格式之一,尤其常見於各種傳遞資料的API之中。舉例來說,台北的YouBike、或甚至是Facebook上的API資料,都是以JSON格式傳輸。

cat(facebook_error, sep = “\n”)

{ “error”: {
 “message”: “An active access token must be used to query information about the current user.”,
 “type”: “OAuthException”,
 “code”: 2500,
 “fbtrace_id”: “EkWgGmWkEt2”
 }}

每個JSON物件中則包含一對對「名稱」與「值」的組合。

上述例子中,Facebook錯誤訊息的這個JSON物件只包含一對名稱與值的組合。名稱是:”error”,值則是另一個JSON物件。


我們可以利用jsonlite套件,將一個JSON文件轉換成R 的list。

Vignettes in package ‘jsonlite’:

json-mapping A mapping between JSON data and R objects (source,pdf)
json-paging          Combining pages of JSON data with jsonlite (source,html)
json-apis            Fetching JSON data from REST APIs (source, html)
json-aaquickstart    Getting started with JSON and jsonlite (source,html)
json-opencpu         Simple JSON RPC with OpenCPU(source, pdf)
vignette(“json-aaquickstart”,”jsonlite”)

Its main strength is that it implements a bidirectional mapping between JSON data and the most important R data types. Thereby we can convert between R objects and JSON without loss of type or information, and without the need for any manual data munging.

在Vignettes的第一段,很清楚的說明了jsonlite的功能:讓R
和JSON物件之間能在不損失資訊的狀況下轉換。

作者給了一個簡單的範例:使用jsonlite提供的`toJSON`和`fromJSON`,可以完整的還原一個R的data.frame。

這種方式,是驗證轉換過程中是否有遺失資訊的一種方式。


Simplification is the process where JSON arrays automatically get converted from a list into a more specific R class.

文件的第二段描述jsonlite在把JSON物件轉換成R物件時,會自動嘗試把JSON物件轉換成更適合的物件。

舉例來說,如果直接轉換`[“Amsterdam”, “Rotterdam”, “Utrecht”, “Den Haag”]`,結果可能會變成一個長度為4的list,且每個元素都是長度為1 的字串向量。

x1 <- c(“Amsterdam”, “Rotterdam”, “Utrecht”, “Den Haag”)
fromJSON(x1,simplifyVector = FALSE )

[[1]]
[1] “Amsterdam”

[[2]]
[1] “Rotterdam”

[[3]]
[1] “Utrecht”

[[4]]
[1] “Den Haag”

fromJSON(x1)

[1] “Amsterdam” “Rotterdam” “Utrecht” “Den Haag”

jsonlite的vignettes後面的段落有處理JSON細節的仔細介紹,未來同學如果有需要使用,建議先將作者提供的vignettes讀過,能省掉很多找bug的時間。

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.