Reference: http://xml2.r-lib.org/index.html
library(xml2); library(dplyr); library(stringr); library(purrr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
## Warning: package 'purrr' was built under R version 3.5.2
An XML element is everything from (including) the element’s start tag to (including) the element’s end tag.
帶有內容的element: <tag> 內容文字 </tag>
不帶有內容的element: <sunnyday/>
在< >
內「非element名稱」的其他設定:如 <sunnyday temperature="25C" />
單純文字檔,以<?xml...>
開頭,附檔名通常為.xml
範例:取自 https://www.dgbas.gov.tw/public/data/open/Stat/price/PR0103A1M.xml
<?xml version="1.0" encoding="utf-8"?>
<DataSet Tab_NAME="消費者物價特殊分類指數-月" Sender_NAME="行政院主計總處">
<Obs1>
<Item>總指數(不含食物)(民國105年=100)</Item>
<TIME_PERIOD>1981M01</TIME_PERIOD>
<FREQ>M</FREQ>
<TYPE>原始值</TYPE>
<Item_VALUE>62.05</Item_VALUE>
</Obs1>
<Obs2>
<Item>總指數(不含蔬菜水果)(民國105年=100)</Item>
<TIME_PERIOD>1981M01</TIME_PERIOD>
<FREQ>M</FREQ>
<TYPE>原始值</TYPE>
<Item_VALUE>60.35</Item_VALUE>
</Obs2>
</DataSet>
'<?xml version="1.0" encoding="utf-8"?>
<DataSet Tab_NAME="消費者物價特殊分類指數-月" Sender_NAME="行政院主計總處">
<Obs1>
<Item>總指數(不含食物)(民國105年=100)</Item>
<TIME_PERIOD>1981M01</TIME_PERIOD>
<FREQ>M</FREQ>
<TYPE>原始值</TYPE>
<Item_VALUE>62.05</Item_VALUE>
</Obs1>
<Obs2>
<Item>總指數(不含蔬菜水果)(民國105年=100)</Item>
<TIME_PERIOD>1981M01</TIME_PERIOD>
<FREQ>M</FREQ>
<TYPE>原始值</TYPE>
<Item_VALUE>60.35</Item_VALUE>
</Obs2>
</DataSet>' %>%
read_xml -> example
XML documents are formed as element trees.
An XML tree starts at a root element and branches from the root to child elements.
All elements can have sub elements (child elements).
root只會有一個樹結。
Obtain root tree:
example %>%
xml_root -> rootTree
檢視
rootTree %>% print
## {xml_document}
## <DataSet Tab_NAME="消費者物價特殊分類指數-月" Sender_NAME="行政院主計總處">
## [1] <Obs1>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs2>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
由上述的結果,我們得知rootTree是個特別的xml_document
class, 其文件內容記錄在{xml_document}
的下方。
class(rootTree)
## [1] "xml_document" "xml_node"
rootTree帶有兩個classes: xml_document
跟xml_node
;前者是以文字字串角度來看,後者是以樹狀結構來看。
xml_node
class的物件可以進行子樹(subtree)的裁剪。
xml_child(n)
可裁出rootTree第一層的子樹中的第n株子樹,在範例裡有Obs1及Obs2兩株子樹可裁。
xml_child
不指定n時會裁出第一顆子樹。
rootTree %>%
xml_child -> subTree1; print(subTree1)
## {xml_node}
## <Obs1>
## [1] <Item>總指數(不含食物)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>62.05</Item_VALUE>
rootTree %>%
xml_child(2) -> subTree2; print(subTree2)
## {xml_node}
## <Obs2>
## [1] <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>60.35</Item_VALUE>
注意:裁剪出的子樹依然保有樹狀結構class,即保有xml_node class。(print時有
{xml_node}
標示。 )
xml_node在print時會有[1],[2],…,[n],顯示下一層子樹會有幾株。要查詢有幾株子樹也可以用
xml_length()
:
subTree2 %>% xml_length
## [1] 5
範例:剪出Obs2下的Item_Value子樹
rootTree %>%
xml_child(2) %>% # 裁出第2株子樹
xml_child(5) # 再裁出接下來的第5株子樹
## {xml_node}
## <Item_VALUE>
xml_parent()
xml_parent()
由子樹找回上層母樹
subTree1 %>%
xml_parent() -> parentOfsubTree1; parentOfsubTree1
## {xml_node}
## <DataSet Tab_NAME="消費者物價特殊分類指數-月" Sender_NAME="行政院主計總處">
## [1] <Obs1>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs2>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
parentOfsubTree1透過
xml_child()
自然可裁出subTree1
xml_siblings()
xml_siblings()
會找出對應的兄弟姐妹樹
subTree1 %>%
xml_siblings -> siblingsOfsubTree1; siblingsOfsubTree1
## {xml_nodeset (1)}
## [1] <Obs2>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
siblingsOfsubTree1
和subTree2
有點不同。siblingsOfsubTree1
是一個list集合,subTree2
是其中一個元素。雖然subTree2
是唯一的sibling tree,但要siblingsOfsubTree1[[1]]
才會是subTree2
。
siblingsOfsubTree1[[1]]
## {xml_node}
## <Obs2>
## [1] <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>60.35</Item_VALUE>
subTree2
## {xml_node}
## <Obs2>
## [1] <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>60.35</Item_VALUE>
xml_contents()
: xml_node tree依第一層子樹列出各別子樹原始xml文字內容
rootTree %>%
xml_contents %>% print
## {xml_nodeset (2)}
## [1] <Obs1>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs2>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
subTree1 %>%
xml_contents %>% print
## {xml_nodeset (5)}
## [1] <Item>總指數(不含食物)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>62.05</Item_VALUE>
xml_text()
會取出xml_contents
中各element的值(即去除element tag<tag></tag>
),並以字串方式保留
subTree1 %>%
xml_contents %>%
xml_text
## [1] "總指數(不含食物)(民國105年=100)" "1981M01"
## [3] "M" "原始值"
## [5] "62.05"
subTree1 %>%
xml_contents %>%
{.[[5]]} %>% # 取出第5個content
xml_double # 拿出其內容並以數值儲存
## [1] 62.05
Reference: https://www.w3schools.com/xml/xpath_intro.asp
XPath是用來描述取得子樹的一行文字。
先前的子樹裁剪是由rootTree開始,透過xml_child()
,xml_children()
等,一層一層剪下去。若知道某一顆子樹的XPath,我們可以用xml_find_all(rootTree,Xpath)
一行指示就取到那顆樹,較有效率。
xml_path()
: 查詢xml_node樹的XPath
subTree1 %>%
xml_path -> xpathOfsubTree1; xpathOfsubTree1
## [1] "/DataSet/Obs1"
"https://www.dgbas.gov.tw/public/data/open/Stat/price/PR0103A1M.xml" %>%
read_xml -> cpiXML
cpiXML %>%
xml_root -> cpiRoot; cpiRoot
## {xml_document}
## <DataSet Tab_NAME="消費者物價特殊分類指數-月" Sender_NAME="行政院主計總處">
## [1] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [3] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [4] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [5] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [6] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [7] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [8] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [9] <Obs>\n <Item>總指數(不含蔬果水產及能源)(民國105年=100)</Item>\n <TIME_PERIOD>19 ...
## [10] <Obs>\n <Item>總指數(不含蔬果水產及能源)(民國105年=100)</Item>\n <TIME_PERIOD>19 ...
## [11] <Obs>\n <Item>總指數(不含設算租金)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [12] <Obs>\n <Item>總指數(不含設算租金)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [13] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M02 ...
## [14] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M02 ...
## [15] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [16] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [17] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [18] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [19] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [20] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## ...
cpiRoot %>%
xml_length()
## [1] 5472
cpiRoot %>%
xml_child ->
firstObsTree; firstObsTree
## {xml_node}
## <Obs>
## [1] <Item>總指數(不含食物)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>62.05</Item_VALUE>
firstObsTree %>%
xml_child -> firstObsItemTree; firstObsItemTree
## {xml_node}
## <Item>
firstObsItemTree %>%
xml_path
## [1] "/DataSet/Obs[1]/Item"
XPath裡/
後方名字代表node element name:
/DataSet
: 走到DataSet node
/DataSet/Obs
: 走到DataSet node之後再走到Obs node
/DataSet/Obs
可以走到那些子樹並裁出來
"/DataSet/Obs" %>%
xml_find_all(cpiRoot,.)
## {xml_nodeset (5472)}
## [1] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [3] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [4] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [5] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [6] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [7] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [8] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [9] <Obs>\n <Item>總指數(不含蔬果水產及能源)(民國105年=100)</Item>\n <TIME_PERIOD>19 ...
## [10] <Obs>\n <Item>總指數(不含蔬果水產及能源)(民國105年=100)</Item>\n <TIME_PERIOD>19 ...
## [11] <Obs>\n <Item>總指數(不含設算租金)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [12] <Obs>\n <Item>總指數(不含設算租金)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [13] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M02 ...
## [14] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M02 ...
## [15] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [16] <Obs>\n <Item>總指數(不含蔬菜水果)(民國105年=100)</Item>\n <TIME_PERIOD>1981M ...
## [17] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [18] <Obs>\n <Item>總指數(不含蔬果及能源)【即核心物價】(民國105年=100)</Item>\n <TIME_PERI ...
## [19] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## [20] <Obs>\n <Item>總指數(不含食物及能源)(民國105年=100)</Item>\n <TIME_PERIOD>1981 ...
## ...
符合此XPath子樹會有5472株。
/DataSet/Obs
若要再走下去會有5472種走法,有時要選出符合某種條件的路就使用/DataSet/Obs[條件描述]
。
條件描述為predicate。
條件描述會以每一株的contents來看
使用xml_find_first()
裁出第一株子樹,並用xml_contents()
看一下contents
"/DataSet/Obs" %>%
xml_find_first(cpiRoot,.) %>%
xml_contents()
## {xml_nodeset (5)}
## [1] <Item>總指數(不含食物)(民國105年=100)</Item>
## [2] <TIME_PERIOD>1981M01</TIME_PERIOD>
## [3] <FREQ>M</FREQ>
## [4] <TYPE>原始值</TYPE>
## [5] <Item_VALUE>62.05</Item_VALUE>
[...]
裡…必需是contents條件。
選出/DataSet/Obs
中contents符合TYPE='原始值'
且Item='總指數(不含食物)(民國105年=100)'
"/DataSet/Obs[TYPE='原始值' and Item='總指數(不含食物)(民國105年=100)']" %>%
xml_find_all(cpiRoot,.) -> cpiSelect; cpiSelect
## {xml_nodeset (456)}
## [1] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M01 ...
## [2] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M02 ...
## [3] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M03 ...
## [4] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M04 ...
## [5] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M05 ...
## [6] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M06 ...
## [7] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M07 ...
## [8] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M08 ...
## [9] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M09 ...
## [10] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M10 ...
## [11] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M11 ...
## [12] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1981M12 ...
## [13] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M01 ...
## [14] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M02 ...
## [15] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M03 ...
## [16] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M04 ...
## [17] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M05 ...
## [18] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M06 ...
## [19] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M07 ...
## [20] <Obs>\n <Item>總指數(不含食物)(民國105年=100)</Item>\n <TIME_PERIOD>1982M08 ...
## ...
原本XPath
/DataSet/Obs
子樹會有5472株,符合contents描述的只剩下456株。
更多可用的operators: https://www.w3schools.com/xml/xpath_operators.asp
Terminal nodes in XMLs are nodes that do no have any “children”. These nodes contain the information we generally want to extract into a tidy data frame.
cpiSelect 1st child的結構:只包含terminal nodes
cpiSelect[[1]] %>%
xml_structure()
## <Obs>
## <Item>
## {text}
## <TIME_PERIOD>
## {text}
## <FREQ>
## {text}
## <TYPE>
## {text}
## <Item_VALUE>
## {text}
as_list()
把cpiSelect的456株子樹轉成帶有456個elements的List。
cpiSelect %>%
as_list -> cpiSelectList
試著把第一株樹存成data frame
.x<-cpiSelectList[[1]]
data.frame(
項目=.x$Item[[1]],
期間=.x$TIME_PERIOD[[1]],
頻率=.x$FREQ[[1]],
格式=.x$TYPE[[1]],
CPI指數=as.numeric(.x$Item_VALUE[[1]]),
stringsAsFactors = F
)
## 項目 期間 頻率 格式 CPI指數
## 1 總指數(不含食物)(民國105年=100) 1981M01 M 原始值 62.05
寫一個函數,讓cpiSelectList中的任一個element(即cpiSelectList[[i]]
, i為element位置)輸入後會輸出一個一筆資料的data frame.
treeList2df<-function(.x){
data.frame(
項目=.x$Item[[1]],
期間=.x$TIME_PERIOD[[1]],
頻率=.x$FREQ[[1]],
格式=.x$TYPE[[1]],
CPI指數=as.numeric(.x$Item_VALUE[[1]]),
stringsAsFactors = F
)
}
測試一下函數
cpiSelectList[[2]] %>%
treeList2df()
## 項目 期間 頻率 格式 CPI指數
## 1 總指數(不含食物)(民國105年=100) 1981M02 M 原始值 62.63
使用迴圈將cpiSelectList疊成完整資料data frame
cpiSelectList %>%
purrr::map_dfr(treeList2df) -> cpiData
devtools::install_github('dantonnoriega/xmltools')
library(xmltools)
xmltools::xml_dig_df()
可將只有terminal nodes的nodeset轉成list of dataframes.
cpiSelect %>%
xmltools::xml_dig_df(.) -> cpiSelectList
xml_dig_df()
是個list,在應用上每個元素可以是不同結構的dataframe.
若每個dataframe有相同變數項目可推疊成一個dataframe,可透過如下的步驟完成
cpiSelectList %>%
map_dfr(~.x) -> cpiSelectDF
"/DataSet/Obs/Item" %>% # 從Root的DataSet選出所有Obs子樹下的Item節點
xml_find_all(cpiRoot,.) %>%
xml_contents %>% xml_text %>% # 取出contents, 再粹出內容字串
as.factor %>% levels %>% print
## [1] "總指數(不含蔬果及能源)【即核心物價】(民國105年=100)"
## [2] "總指數(不含蔬果水產及能源)(民國105年=100)"
## [3] "總指數(不含蔬菜水果)(民國105年=100)"
## [4] "總指數(不含設算租金)(民國105年=100)"
## [5] "總指數(不含食物)(民國105年=100)"
## [6] "總指數(不含食物及能源)(民國105年=100)"
"/DataSet/Obs[Item='總指數(不含蔬果及能源)【即核心物價】(民國105年=100)' and TYPE='原始值']" %>%
xml_find_all(cpiRoot,.) -> coreCPI
coreCPI %>%
as_list %>%
map_dfr(treeList2df) ->
coreCPIdf