關卡 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函數有xencoding和其他參數可以使用。 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共有兩個參數:xxpathx可以是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

回到剛剛的節點n2n1。 這兩個節點在結構上不同的,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字元等等
# 但是我們就先練習到這裡了