關卡 1
這門課程的目的是想要教大家如何處理XML或HTML的資料。這是大部分的網頁資料(HTML)所採用的格式。
關卡 2
XML的全名是eXtensible Markup Language,是一種讓電腦可以快速理解資訊的標記語言。 XML的透過標記來讓電腦理解資訊的內容,並且清楚的切割開標籤與內容。
關卡 3
舉例來說,若我們要用XML來記載這門課程的資訊,記載方式可以如下:“<course>DataScienceAndR</course><title>RDataEngineer-02-XML</title><author>Wush Wu</author>”。 用<>
框起來的文字代表標籤,上述文件中共有三個標籤:course、title和author。 以course標籤為例,標籤的開始是<course>,結束則是</course>,會多一個“/”記號。 透過這些標籤,電腦可以清楚的知道DataScienceAndR代表的是course,RDataEngineer-02代表的是title,而Wush Wu代表的是author。 對電腦而言,標籤與內容很清楚地被分開了,不會被混淆。
關卡 4
如果我們收到一個文件,內容如下: ‘<th>廠商名稱</th><td>台灣翔登股份有限公司</td>’ 請問同學,「廠商名稱」這段文字是什麼標籤呢?
th
關卡 5
現代的網頁都是以類似的格式傳遞資訊,讓電腦進行處理。 而除了簡單的標籤與內文之外,我們也可以定義標籤的屬性。 舉例來說:“<title type=‘regular’>RDataEngineer-02-XML</title>”這樣的訊息中, 除了標籤與內容外,電腦還會知道,這個標籤「title」還附帶有屬性:「type」,而且屬性的值是「regular」。
關卡 6
如果我們收到一份文件,內容如下: ‘<th class=“T11b” bgcolor=“#ffdd83” align=“left” valign=“middle” width=“200”>廠商名稱</th> <td class=“newstop” bgcolor=“#EFF1F1”>台灣翔登股份有限公司</td>’ 請問同學,th標籤的class屬性的值為何呢?’
T11b
關卡 7
XML的文件中,標籤可以有結構關係。 我們拿一小段稍後要處理的文件作範例: ‘<tr> <th class=“T11b” bgcolor=“#ffdd83” align=“left” valign=“middle” width=“200”>廠商名稱</th> <td class=“newstop” bgcolor=“#EFF1F1”>台灣翔登股份有限公司</td> </tr>’ 這份文件中,th和td標籤以及他們的內容,都會被歸類在tr標籤之內。
關卡 8
依照慣例,我們會將tr稱為th的父標籤(parent),而th與td兩者都是tr的子標籤(children),每個標籤最多只有一個父標籤。 這是因為th和td兩個標籤,寫在<tr>和</tr>之間。
關卡 9
在HTML網頁中,幾乎所有的標籤都有父標籤,除了html這個標籤以外。 所以我們在處理HTML文件時,會稱呼這個標籤為整個文件的根(root)。
關卡 10
如果同學對XML或HTML的背景知識有興趣,之後可以輸入wiki_html()或wiki_xml()了解更仔細的背景知識。
關卡 11
這門課程中,我們要介紹的是R 的xml2套件。
關卡 12
請同學先安裝xml2套件。
check_then_install("xml2", "0.1.2")
關卡 13
請同學載入xml2套件。
library(xml2)
關卡 14
我們已經準備了一段簡單的XML文件,並且儲存於變數x1
。請同學輸入x1
看看這個文件。
x1
關卡 15
請問下列哪一個標籤不存在於x1文件中?
course
關卡 16
在利用xml2套件處理XML或HTML文件之前,必須要先作解析,將文件建立成一種特殊的R 物件後,才能讓進行挖掘資訊。 這裡我們使用的是read_xml
函數。 請同學輸入:?read_xml
打開說明頁面。
?read_xml
關卡 17
根據說明文件,read_xml
函數有x
、encoding
和其他參數可以使用。 x
則可以是一個檔案路徑(file path)、一個網址(url),或是一個XML文本的字串向量(literal xml)。 請問同學,x1
是不是符合read_xml
的參數x
的條件呢?
Yes
關卡 18
請同學利用doc1 <- read_xml(x1)
,將x1解析後的結果儲存到變數doc1
。
doc1 <- read_xml(x1)
關卡 19
接著我們可以輸入doc1
來看看xml2解析後的結果。
doc1
關卡 20
請同學輸入指令檢查doc1
的型態。
class(doc1)
關卡 21
目前xml2中的物件,大致上可以分成三種:xml_document、xml_node和xml_nodeset。 xml_document就代表整個XML文件。xml_node對應到上述介紹的XML標籤。 而在經過read_xml
後,每個標籤會被轉化為一個xml_node,xml_nodeset則是一群標籤的集合。 接下來的例子會具體介紹如何使用這些物件。
關卡 22
在挖掘網頁資訊時,最困難的部分就是要從成千上萬的標籤中,找到我們感興趣的,再將標籤內的資訊(可能是屬性,也可能是內容)給擷取出來。 所以接下來,我們要講解挖掘網頁資訊的三步驟:1. 找到標籤 2. 查詢屬性 3.檢查內容。
關卡 23
第一部分找標籤是這些步驟中最困難的,因為每份文件的標籤可能都不同, 我們需要先找出我們感興趣的內容屬於那一類的標籤,接著再用xml2等套件把我們想找的標籤給定位出來。 第一段的方式,必須要透過其他工具的輔助,目前在R 中並沒有很好的方法,只能透過嘗試、嘗試、再嘗試才能找到我們的目標標籤。 這裡我們要介紹的是,在已經知道目的標籤時,如何利用xml2來找出目標。
關卡 24
我們來看一個很泛用的函數:xml_find_all
,請同學打開它的說明文件。
?xml_find_all
關卡 25
根據說明文件,xml_find_all
共有兩個參數:x
與xpath
。 x
可以是xml_document、xml_node或xml_nodeset。 而xpath
(XML Path Language)則是一種特別的格式,讓我們可以和電腦溝通我們要搜尋的標籤。 有興趣的同學可以直接閱讀wiki_xpath()。
關卡 26
我們先操作練習一下xml_find_all
後,再講解xpath
。 請同學輸入:xml_find_all(doc1, "/a/b")
。
xml_find_all(doc1, "/a/b")
關卡 27
同學會看到xml_find_all
找了唯一的標籤b給我們。 同學應該可以猜到,xpath最後類似路徑的格式,其實就是在描述標籤的相對位置。 但是如果我們輸入的是:xml_find_all(doc1, "/b")
呢?請同學試試看。
xml_find_all(doc1, "/b")
關卡 28
我們可以看到xml_find_all
回報沒有找到任何結果。 在XPath的規範中,我們要尋找的標籤名稱,就是整個路徑的最後一個位置。 所以“/a”就代表要找“<a>…</a>”,而“/b”則代表要找“<b>…</b>”,斜線則代表標籤在文件中的相對位置。 “/a”代表這個標籤在根部,也就是沒有父標籤。 “/a/b”則代表這個標籤“<b>…</b>”的父標籤是“<a>…</a>”,並且再往父標籤的方向走,就到底了。
關卡 29
請問下列哪一個XPath路徑可以找到x1
中的“c”標籤? x1
的內容為:“<a><b>B</b><c>C1</c><c class=‘x’>C2</c></a>”。
/a/c
關卡 30
接著,我們請同學輸入:ns <- xml_find_all(doc1, "/a/c")
。
ns <- xml_find_all(doc1, "/a/c")
關卡 31
請同學檢查ns
的型態。
class(ns)
關卡 32
我們可以透過[[
和[
,從xml_nodeset中取出xml_node或是xml_nodeset。 xml2在這邊的設計和R 的list非常接近,所以同學可以用處理list的經驗來作判斷。
關卡 33
依照list的經驗,請問ns[1]
的型態會是?
xml_nodeset
關卡 34
依照list的經驗,請問ns[[1]]
的型態會是?
xml_node
關卡 35
我們可以把第一個c標籤,存到變數n1
。請同學輸入:n1 <- ns[[1]]
。
n1 <- ns[[1]]
關卡 36
根據x1
的內容:“<a><b>B</b><c>C1</c><c class=‘x’>C2</c></a>”, 請問n1
,也就是<a>…</a>底下的第一個<c>..</c>標籤,他的內容是什麼呢?
C1
關卡 37
我們可以透過xml_text(n1)
取出xml_node的內容。 在這裡,我們應該要看到“C1”,請同學試試看。
xml_text(n1)
關卡 38
我們也可以用xml_parent
來看一個標籤的父標籤。 請同學試試看輸入:xml_parent(n1)
。
xml_parent(n1)
關卡 39
接著請執行:n2 <- ns[[2]]
。
n2 <- ns[[2]]
關卡 40
我們可以檢查一下xml_text(n2)
的輸出,確認這是第二個c標籤。
xml_text(n2)
關卡 41
另一個檢查內容的函數是xml_contents
。 當含有子標籤時,它和xml_text
的行為會不一致。
關卡 42
請同學輸入:a <- xml_find_one(doc1, "/a")
。 這裡我們使用xml_find_one
作選取,所以輸出的就只有一個node,型態就會是xml_node。
a <- xml_find_one(doc1, "/a")
關卡 43
接著我們試試看輸入:xml_text(a)
,同學會看到在<a>…</a>之間的所有文字。
xml_text(a)
關卡 44
但是我們若輸入:xml_contents(a)
,R 就會回傳一個xml_nodeset給我們。請同學試試看。
xml_contents(a)
關卡 45
我們也能透過xml_children(a)
來取得所有以a 為父標籤的標籤們(在此案例中為是一個b標籤和兩個c標籤)。 請同學試試看。
xml_children(a)
關卡 46
回到剛剛的節點n2
和n1
。 這兩個節點在結構上不同的,n2
附帶了屬性,而n1
沒有。 請同學先看看xml_attrs(n1)
的結果。
xml_attrs(n1)
關卡 47
同學應該看到一個空的結果,因為第一個c標籤並沒有夾帶屬性的資訊。
關卡 48
再請同學看看xml_attrs(n2)
的結果。
xml_attrs(n2)
關卡 49
同學應該看到一個帶有名字的字串向量。其中名稱為“class”的元素的值為“x”。 比對一下x1
的內容:“<a><b>B</b><c>C1</c><c class=‘x’>C2</c></a>” 在給定某個標籤對應的節點後,我們是不是可以取出一個XML標籤的屬性呢?
關卡 50
在使用XPath尋找標籤時,屬性是可以派上用場的。 舉例來說,xml_find_all(doc1, "/a/c[@class]")
,就在搜尋時增加:「標籤必須要帶有名稱為“class”的屬性」,所以這時候R 就只會回傳第二個c標籤,因為第一個c標籤並不帶有 class屬性。 請同學試試看。
xml_find_all(doc1, "/a/c[@class]")
關卡 51
我們甚至可以指定屬性的值,例如:xml_find_all(doc1, "/a/c[@class='g']")
就代表我們要找的c標籤不只是有class屬性而已,這個屬性還必須要是“g”。 在這裡,也要請同學注意我們是如何交替的使用雙引號和單引號。 由於這裡的“g”必須要加上引號,但是整個文字的外面已經套上雙引號,如果重複使用雙引號的話會造成R 在判斷字串的困難。 因此這裡要使用單引號。請同學試試看。
xml_find_all(doc1, "/a/c[@class='g']")
關卡 52
最後我們要介紹一種在XPath中常常使用的定位方式:“//”。 這裡的“//”代表的就是任意位置。 當我們在處理複雜的網頁資料時,如果每次都要從根部尋找正確的路徑,是非常不方便的。 此時,透過“//a”,我們就可以找到在所有位置都出現的a標籤。 這樣的語法等等會在最後一關中用到。
關卡 53
也請同學不要忘記,“//”的用法是可以搭配屬性過濾使用的。
關卡 54
以上的課程內容,我們介紹了在給定標籤的名稱(a標籤<a>…</a>或b標籤<b>…</b>)、標籤的位置(根部是“/”,任意位置是“//”)及標籤的屬性後,如何利用xml_find_all來搜尋標籤。
關卡 55
我們也知道當找到這些標籤後,要怎麼取出標籤的內容(xml_contents和xml_text)與屬性(xml_attrs)。 另外我們也可以沿著標籤往父標籤(xml_parent),或是取出子標籤(xml_children)。 最後跟同學說明,這些操作都是向量式的喔! 可以對xml_nodeset使用如xml_text等函數,一次操作大量的xml_nodeset。 稍後的練習中就會用到這些功能。
關卡 56
接著就請同學透過上述所學,從政府的決標公告網頁中試著找出不同的資訊。 請同學在完成之後存檔,並輸入submit()
來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen With Encoding… -> 選取:UTF-8
# 這個段落的程式碼,是先幫助同學觀察資料的,不包含在作答的檢查範圍
if (FALSE) {
# 我們先來看看政府電子採購網<http://web.pcc.gov.tw>所爬下來的決標資料。
# 這個檔案的路徑已經設定到tender_path了。首先,請同學先用`readLines`
# 來看這個檔案的前100行。
readLines(tender_path, n = 100)
# 這樣的內容要直接處理是很挑戰的
# 但是我們可以先透過瀏覽器來觀察這個HTML文件所夾帶的內容
browseURL(tender_path)
# 同學的預設瀏覽器應該會打開這個網頁
}
# 首先請同學用read_html載入網頁內容
tender <- read_html(tender_path)
# 接下來,我們的目標是抓出所有的投標廠商名稱
# 透過瀏覽器的開發工具可以發現,裝載著廠商名稱的標籤是像這樣的:
# <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的內容必須要是"廠商名稱"
##
# 首先,找出所有的上一層是tr的th標籤的 nodesets
# 提示:你的xpath應該為 "//tr/th"
ths <- xml_find_all(tender, "//tr/th")
stopifnot(class(ths) == "xml_nodeset")
stopifnot(length(ths) == 116)
# 請取出每個ths中的th標籤的值,並且和 " 廠商名稱" 作比較
player_name_reference <- rawToChar(as.raw(c(227L, 128L, 128L, 229L, 187L, 160L, 229L, 149L, 134L, 229L,
144L, 141L, 231L, 168L, 177L))) # " 廠商名稱"
Encoding(player_name_reference) <- "UTF-8"
ths_text <- xml_text(ths)
Encoding(ths_text) <- "UTF-8"
is_target <- ths_text == player_name_reference
stopifnot(class(is_target) == "logical")
stopifnot(sum(is_target) == 4)
stopifnot(which(is_target)[1] == 36)
# 接著,請利用 `[`來從ths中選出那些值為 " 廠商名稱" 的xml_nodeset
ths2 <- ths[is_target]
stopifnot(class(ths2) == "xml_nodeset")
stopifnot(length(ths2) == 4)
# 因為我們的目標是th旁邊的td,所以要先透過`xml_parent`回到tr層級
trs <- xml_parent(ths2)
stopifnot(class(trs) == "xml_nodeset")
stopifnot(length(trs) == 4)
# 然後我們直接用xml_children取得這些tr的所有子標籤
trs_children <- xml_children(trs)
stopifnot(class(trs_children) == "xml_nodeset")
stopifnot(length(trs_children) == 8) # 一個tr有兩個子標籤
# 取出這些標籤的值
trs_children_text <- xml_text(trs_children)
Encoding(trs_children_text) <- "UTF-8"
stopifnot(class(trs_children_text) == "character")
stopifnot(length(trs_children_text) == 8)
# 只挑出那些值「不是」 " 廠商名稱"的元素
players <- trs_children_text[trs_children_text != player_name_reference]
# 其實這樣取出的廠商名稱還是很髒,有一大堆換行、tab字元等等
# 但是我們就先練習到這裡了