Try   HackMD

XML 處理 - SAX、DOM、PULL

Overview of Content

如有引用參考請詳註出處,感謝

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

XML 概述

XML(eXtensible Markup Language)可拓展標示語言,又稱為可拓展標記語言,它與 HTML 一樣都是「通用標記語言」SGML, Standard Generalized Markup Language

  • 其實這種「標記」語言並不算程式語言,因為它無法通過圖靈測試;主要算是 Open stardard 的結構領域,用來做不同裝置之間的通訊

    XML 不依賴任何的平台,它是普遍公開的通訊協定

    iOS

    XML

    Android

    MacOS

  • XML 是 W3C 制定的,它的特性如下

    1. 主要目的在「簡單並清晰的 展示(傳遞)數據」,它並不在意數據的索引、排序、查找… 等等的效率,其目的是為了展示,所以相對的也比較耗費空間

      好的展示數據是為了給人們觀看

    2. 重新定義 SGML 的內部值、參數,只留下常用的功能,像是保留 SGML 的結構化功能!

      • XML 的標記完全自由定義,不受約束(以下只有第一行為必須,其他標記可以用中文)

        ​​​​​​​​​​​​<?xml version="1.0" encoding="UTF-8"?> ​​​​​​​​​​​​<學生資訊> ​​​​​​​​​​​​ <名子>Kyle</名子> ​​​​​​​​​​​​ <學號>456789</學號> ​​​​​​​​​​​​ <性別>Man</性別> ​​​​​​​​​​​​</學生資訊>
      • 對於大小寫敏感

      • 標記成對出現,每一行都需要有結束標記的符號 </>

XML 結構

  • XML 擁有結構化的特性,開發時常見的 pom.xml 檔案就是 xml 結構… 範例如下

    ​​​​<?xml version="1.0" encoding="UTF-8"?> ​​​​<project ​​​​ xmlns="http://maven.apache.org/POM/4.0.0" ​​​​ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ​​​​ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> ​​​​ <modelVersion>4.0.0</modelVersion> ​​​​ <groupId>com.example</groupId> ​​​​ <artifactId>JavaEEHelloWorld</artifactId> ​​​​ <version>1.0-SNAPSHOT</version> ​​​​ <name>JavaEEHelloWorld</name> ​​​​ <packaging>war</packaging> ​​​​ <properties> ​​​​ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> ​​​​ <maven.compiler.target>11</maven.compiler.target> ​​​​ <maven.compiler.source>11</maven.compiler.source> ​​​​ <junit.version>5.9.2</junit.version> ​​​​ </properties> ​​​​ <dependencies> ​​​​ <dependency> ​​​​ <groupId>jakarta.servlet</groupId> ​​​​ <artifactId>jakarta.servlet-api</artifactId> ​​​​ <version>5.0.0</version> ​​​​ <scope>provided</scope> ​​​​ </dependency> ​​​​ <dependency> ​​​​ <groupId>org.junit.jupiter</groupId> ​​​​ <artifactId>junit-jupiter-api</artifactId> ​​​​ <version>${junit.version}</version> ​​​​ <scope>test</scope> ​​​​ </dependency> ​​​​ <dependency> ​​​​ <groupId>org.junit.jupiter</groupId> ​​​​ <artifactId>junit-jupiter-engine</artifactId> ​​​​ <version>${junit.version}</version> ​​​​ <scope>test</scope> ​​​​ </dependency> ​​​​ <dependency> ​​​​ <groupId>commons-lang</groupId> ​​​​ <artifactId>commons-lang</artifactId> ​​​​ <version>2.5</version> ​​​​ </dependency> ​​​​ <dependency> ​​​​ <groupId>org.apache.commons</groupId> ​​​​ <artifactId>commons-lang3</artifactId> ​​​​ <version>3.1</version> ​​​​ </dependency> ​​​​ </dependencies> ​​​​ <build> ​​​​ <resources> ​​​​ <resource> ​​​​ <directory>src/main/resources</directory> ​​​​ </resource> ​​​​ </resources> ​​​​ <plugins> ​​​​ <plugin> ​​​​ <groupId>org.apache.maven.plugins</groupId> ​​​​ <artifactId>maven-war-plugin</artifactId> ​​​​ <version>3.3.2</version> ​​​​ </plugin> ​​​​ </plugins> ​​​​ </build> ​​​​</project>

解析 XML 數據的方式

  • 解析 XML 數據的方式有不同種,每種都有其特性、優缺點、解析 XML 的思考方式,像是 SAXDOMPULL

  • 語法分析器一般來說會有兩種分析想法,然而 想法並沒有對錯(哪個更好),只有在正確的情況下使用對的方式才是最好的

    • 基於「物件」的界面,DOM 就是基於物件去開發

    • 基於「事件」的界面,SAX 就是基於事件去開發

      Image Not Showing Possible Reasons
      • The image was uploaded to a note which you don't have access to
      • The note which the image was originally uploaded to has been deleted
      Learn More →

SAX 解析

SAX 全名為 Simple API for XML,它是指一套 API,同時它也是一個開源套件

最早由 Dacid Megginson 使用 Java 語言開發

  • SAX 的不同之處

    它不同於其他大多數 XML 標準,SAX 沒有語言開發商必須遵守的標準 SAX 參考版本,因此 SAX 的不同實現可能採用區別很大的界面

SAX 原理 - 事件驅動

  • SAX 軟體設計是「事件驅動型」:

    簡單來說就是,它會邊依照順序讀取 XML 並 即時的反應給使用 SAX 的程序員

    Unsupported markdown: list

    解析

    Unsupported markdown: list

    程序員

    解析_XML

    數據

    • SAX 是透過標準的解析 XML 的界面,即時反饋給使用者,這個解析 XML 的界面是標準界面(Standard Interface),不會改變

    • SAX 也不會在記憶體中幫我們建立物件

  • 當掃描到文檔、元素的開始與結束都會透過 XML 的標準驅動 API 反饋給使用者,而 SAX 大多數都有實現 5 種類型的事件

    SAX 使用的事件驅動與用戶操作無關,事件只會與 XML 中的元素有關(元素驅動事件)

    1. 文檔Document)的開始、結束

    2. 每次觸發 XML 新的 元素開始、結束(標籤)

    3. MetaData 通常有單獨的事件處理

    4. 當處理文檔中的 合法元素 DTDSchema… 時,產生對應的事件

      DTD(Document Type Definition)是一種用來定義 XML 文件結構和合法元素的規範… 像是 CDATAIDIDIDREFIDREFSNMTOKENNMTOKENSENTITYENTITIESNOTATIONDOCTYPE

      以下範例說明著

      • bookstore 元素是一個包含至少一個 book 元素的元素

      • book 元素包含 titleauthorprice 元素

      • titleauthorprice 元素都包含字符數據(PCDATA

      ​​​​​​​​<!-- XML 文件(example.xml) --> ​​​​​​​​<!DOCTYPE bookstore [ ​​​​​​​​ <!ELEMENT bookstore (book+)> ​​​​​​​​ <!ELEMENT book (title, author, price)> ​​​​​​​​ <!ELEMENT title (#PCDATA)> ​​​​​​​​ <!ELEMENT author (#PCDATA)> ​​​​​​​​ <!ELEMENT price (#PCDATA)> ​​​​​​​​]> ​​​​​​​​<bookstore> ​​​​​​​​ <book> ​​​​​​​​ <title>Introduction to XML</title> ​​​​​​​​ <author>John Doe</author> ​​​​​​​​ <price>29.95</price> ​​​​​​​​ </book> ​​​​​​​​ <book> ​​​​​​​​ <title>Java Programming</title> ​​​​​​​​ <author>Jane Smith</author> ​​​​​​​​ <price>39.99</price> ​​​​​​​​ </book> ​​​​​​​​</bookstore>
    5. 每當產生錯誤事件時,會透過界面通知開發者

SAX - XML 解析界面、類

org.xml.sax 包 是 JavaSE 使用 SAX 的方式來解析 XML

  • SAX 使用的常見 XML 解析 界面 如下表

    XML 解析界面 概述 補充
    ContentHandler 文檔本身 相關的事件(像是開始、結束) 其中也包括元素(Element)監聽
    EntityResolver 實體關聯 的事件 較少使用
    DTDHandler XML 中的 標準事件 然而它定義的事件不夠完整,要完整 DTD 事件的話,可以使用 DeclHandler 界面
    ErrorHandler 錯誤事件 定義三種級別 1. warning(非 XML 規範)、2. error(XML 定義的錯誤)、3. fatalError(致命錯誤)

    大多數時候都會使用 DefaultHandler 類(有大部分的界面)

    image

  • SAX 使用的常見 XML 解析 如下表

    XML 解析類 概述 補充
    XMLReader 該類可以用 parse 方法,來啟動 XML 語法分析器,它可以接受 1. 文件名2. URL3. InputSource 我們可以以設定 XML 解析監聽器…
    setFeature(Boolean)用來控制語法分析器的工作
    setProperty(物件) 用來控制語法分析器的工作
    XMLReaderFactory 用來創建 XML 語法分析器物件 可創建 1. 系統預設2. 指定類型(透過設定類路徑)
    InputSource 控制 SAX 如何讀取文件
    SAXException SAX 定義的大多數方法都會拋出 SAXException

SAX 解析 XML 範例

  • 目標 XML 如下

    ​​​​<?xml version="1.0" encoding="utf-8"?> ​​​​<!DOCTYPE bookstore [ ​​​​ <!ELEMENT bookstore (book+)> ​​​​ <!ELEMENT book (title, author, price)> ​​​​ <!ELEMENT title (#PCDATA)> ​​​​ <!ELEMENT author (#PCDATA)> ​​​​ <!ELEMENT price (#PCDATA)> ​​​​ ]> ​​​​<bookstore> ​​​​ <book> ​​​​ <title>Introduction to XML</title> ​​​​ <author>John Doe</author> ​​​​ <price>29.95</price> ​​​​ </book> ​​​​ <book> ​​​​ <title>Java Programming</title> ​​​​ <author>Jane Smith</author> ​​​​ <price>39.99</price> ​​​​ </book> ​​​​</bookstore>
  • 使用 SAX API 來解析 XML 數據,並保存在記憶體中

    ​​​​data class BookInformation(var bookTitle: String = "", ​​​​ var authorName: String = "", ​​​​ var bookPrice: Float = -1F) { ​​​​ companion object { ​​​​ const val XML_KEY_BOOK = "book" ​​​​ const val XML_KEY_TITLE = "title" ​​​​ const val XML_KEY_AUTHOR = "author" ​​​​ const val XML_KEY_PRICE = "price" ​​​​ } ​​​​} ​​​​class BookXmlSaxHandler: DefaultHandler() { ​​​​ lateinit var bookList: MutableList<BookInformation> ​​​​ var curBookInfo: BookInformation? = null ​​​​ lateinit var curTag: String ​​​​ override fun startDocument() { ​​​​ bookList = mutableListOf() ​​​​ println("---- Document start ----") ​​​​ } ​​​​ override fun startElement( ​​​​ uri: String?, // 名稱空間 ​​​​ localName: String?, // 本地名,不帶前綴 ​​​​ qName: String?, // 帶前綴的限定名 ​​​​ attributes: Attributes? ​​​​ ) { ​​​​ if (qName == null || attributes == null) { ​​​​ return ​​​​ } ​​​​ when(qName) { ​​​​ BookInformation.XML_KEY_BOOK -> { ​​​​ println("create book instance") ​​​​ curBookInfo = BookInformation() ​​​​ } ​​​​ BookInformation.XML_KEY_TITLE, ​​​​ BookInformation.XML_KEY_AUTHOR, ​​​​ BookInformation.XML_KEY_PRICE -> { ​​​​ curTag = qName ​​​​ } ​​​​ } ​​​​ } ​​​​ override fun characters(ch: CharArray?, start: Int, length: Int) { ​​​​ if (ch == null) { ​​​​ return ​​​​ } ​​​​ val data = String(ch, start, length) ​​​​ when(curTag) { ​​​​ BookInformation.XML_KEY_TITLE -> { ​​​​ curBookInfo!!.bookTitle = data ​​​​ } ​​​​ BookInformation.XML_KEY_AUTHOR -> { ​​​​ curBookInfo!!.authorName = data ​​​​ } ​​​​ BookInformation.XML_KEY_PRICE -> { ​​​​ curBookInfo!!.bookPrice = data.toFloat() ​​​​ } ​​​​ } ​​​​ } ​​​​ override fun endElement(uri: String?, localName: String?, qName: String?) { ​​​​ if (BookInformation.XML_KEY_BOOK == qName) { ​​​​ if (curBookInfo != null) { ​​​​ bookList.add(curBookInfo!!) ​​​​ } ​​​​ curBookInfo = null ​​​​ println("create book set to null") ​​​​ } ​​​​ } ​​​​ override fun endDocument() { ​​​​ println("---- Document end ----") ​​​​ bookList.forEach { ​​​​ println(it) ​​​​ } ​​​​ } ​​​​}
  • 測試使用 SAX API 解析 XML:

    ​​​​fun main() { ​​​​ val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml") ​​​​ val bookXmlHandler = BookXmlSaxHandler() ​​​​ // 創建 XMLReader ​​​​ val xmlReader: XMLReader = XMLReaderFactory.createXMLReader().apply { ​​​​ // 設置其中的 content Handler ​​​​ contentHandler = bookXmlHandler ​​​​ } ​​​​ val fileInputStream = FileInputStream(xmlFile) ​​​​ try { ​​​​ xmlReader.parse(InputSource(fileInputStream)) ​​​​ } finally { ​​​​ fileInputStream.close() ​​​​ } ​​​​}

    image

DOM 解析

DOM(Document Object Model)提供了一個方便的編程界面,因此可以被視為一種官方的W3C標準

W3C(World Wide Web Consortium)制定了DOM的標準,並且 DOM 在瀏覽器中被廣泛使用它允許 JavaScript 通過瀏覽器直接操作 HTML 和 XML 文檔

DOM 原理 - JavaScript

  • DOM 會將 XML 完全解析完成後,在記憶體中建立一個 XML 副本(又稱之為物件樹),並且該副本中的所有元素皆以「物件的形式」儲存

    記憶體

    Unsupported markdown: list
    Unsupported markdown: list

    解析完成

    程序員

    解析_XML

    XML_對應的物件樹

    • DOM 會佔用,記憶體空間(建立 XML 對應的物件樹);但相對的… DOM 會比 SAX 還方便使用
  • DOM & JavaScript 的關係

    早期人們會把 DOM 視為是一種讓 JavaScript 在遊覽器間可移植的方法!但其實 DOM 的應用是遠遠超出這個範圍的

    • DOM 技術使的用戶界面可以「動態變化」,大大增加了交互性

      像是 JavaScript 可以透過它動態操作 HTML 的元素的顯示、隱藏

    • JavaScript 就是利用 DOM 來取得 HTML 文件的操縱入口,並獲得 HTML 對應的物件

DOM 規範結構

  • 根據 W3C DOM 規範,DOM 解析 HTML、XML 這兩類標記語言的界面… DOM 會將整個文件映射為一個由「層次節點」組成的物件

    • Node 節點

      根據 DOM 規範,HTML 文檔中的每一個成份都是節點;有分為「文檔節點」、「元素節點」、「屬性節點」、「文本節點」、「註釋節點

      除了 文檔節點 之外,所有節點都有父節點

      ​​​​​​​​<!DOCTYPE html> ​​​​​​​​<html> ​​​​​​​​<head> ​​​​​​​​ <title>Title</title> ​​​​​​​​</head> ​​​​​​​​<body> ​​​​​​​​ <form method="post" ​​​​​​​​ enctype="multipart/form-data" ​​​​​​​​ action="upload.handle"> ​​​​​​​​ <label> ​​​​​​​​ <input type="file" name="upload" value="upload"/> ​​​​​​​​ </label> ​​​​​​​​ <button>Upload</button> ​​​​​​​​ </form> ​​​​​​​​</body> ​​​​​​​​</html>

      HTML 文檔節點

      < html > 元素節點

      < header > 元素節點

      < title > 元素節點

      Title 文本節點

      < body > 元素節點

      < form > 元素節點

      method 文本節點

      enctype 文本節點

      action 文本節點

      < label > 元素節點

      < input > 元素節點

      < button > 元素節點

  • DOM API 的分級(共有 3 個分級)

    • Level 0 DOM / 0 級 DOM 是什麼

      在遊覽器正式規範 DOM 標準之前就存在的規範,它並非有文件的規範,所以是非正規的稱之為 DOM 0,正確一點應該稱其為 BOM(Brower Object Model

    1. 1 級 DOM

      DOM 核心DOM HTML 兩個模塊組層

      • DOM 核心能映射以「XML 為基礎」的文檔結構為物件,核心 API 會將文件中所有的內容都視為節點,再依照類型區分不同型態

      • DOM HTML(其實就是拓展核心)則通過添加 HTML 專用的物件與函數對 DOM 核心進行拓展,專門用來操作 HTML

        DOM HTML 在瀏覽器開發中較為常見,像是 HTMLHeadElement 元素就提供專屬 HTML 操作的 API

        Level 1 DOM

        DOM 核心

        DOM Html

    2. 2 級 DOM

      鑑於 1 級 DOM 僅以映射文檔結構到記憶體中(物件樹),2 級 DOM 通過對於源有的 DOM 拓展,針對 DOM 界面增加

      • DOM 視圖:描述跟蹤一個文檔的各種視圖(使用 CSS 樣式設計文檔前後的差異)

      • DOM 事件:對於用戶界面事件的反應(像是滑鼠點擊),對這一系列的事件進行標準化

        以往尚未標準化時稱之為 基本事件模型(Basic Event Model 或是傳統模型(Traditional model

      • DOM 樣式:基於 CSS 樣式的反應

      • DOM 遍歷:遍歷、操作文檔物件

      • 支持 XML 的命名空間

        • XML 的命名空間

          在 XML 中,命名空間(Namespace)是一種用來「區分元素、屬性名稱的機制」,以「避免名稱衝突」的問題

          命名空間在 XML 中通常以一個 URIUniform Resource Identifier)或 URL 來標識

          實際上這個 URL 並不要求是一個可訪問的網址

          ​​​​​​​​​​​​​​​​<!-- 定義 books 的命名空間--> ​​​​​​​​​​​​​​​​<root xmlns:books="http://example.com/books"> ​​​​​​​​​​​​​​​​ <!-- 使用命名空間 + 標記 --> ​​​​​​​​​​​​​​​​ <books:book> ​​​​​​​​​​​​​​​​ <books:title>Introduction to XML</books:title> ​​​​​​​​​​​​​​​​ <books:author>John Doe</books:author> ​​​​​​​​​​​​​​​​ <books:price>29.95</books:price> ​​​​​​​​​​​​​​​​ </books:book> ​​​​​​​​​​​​​​​​ ​​​​​​​​​​​​​​​​ <!-- 無命名空間,這個 book 跟上面的 books:book 是不同標記 --> ​​​​​​​​​​​​​​​​ <book> ​​​​​​​​​​​​​​​​ <title>The Great Novel</books:title> ​​​​​​​​​​​​​​​​ <author>Jane Smith</books:author> ​​​​​​​​​​​​​​​​ <price>19.99</books:price> ​​​​​​​​​​​​​​​​ </book> ​​​​​​​​​​​​​​​​</root>
    3. 3 級 DOM

      對上一級 DOM 拓展,引入「統一載入」、「保存文件」和「文檔驗證方法」

      DOM 核心再次拓展,支持 XML 1.0 的所有設計,像是 XML InfosetXPathXML Base… 等等

DOM 解析 XML 範例

  • 目標 XML 如下

    ​​​​<?xml version="1.0" encoding="utf-8"?> ​​​​<!DOCTYPE bookstore [ ​​​​ <!ELEMENT bookstore (book+)> ​​​​ <!ELEMENT book (title, author, price)> ​​​​ <!ELEMENT title (#PCDATA)> ​​​​ <!ELEMENT author (#PCDATA)> ​​​​ <!ELEMENT price (#PCDATA)> ​​​​ ]> ​​​​<bookstore> ​​​​ <book> ​​​​ <title>Introduction to XML</title> ​​​​ <author>John Doe</author> ​​​​ <price>29.95</price> ​​​​ </book> ​​​​ <book> ​​​​ <title>Java Programming</title> ​​​​ <author>Jane Smith</author> ​​​​ <price>39.99</price> ​​​​ </book> ​​​​</bookstore>
  • 使用 Javax 提供的 DOM API 來解析 XML 數據,使用 DOM API 的特點在於… 它會一次幫你處理好所有的物件映射關係(以 Node 的方式,儲存在記憶體中)

    這樣我們在使用上就不用特別關心 XML 資料流的處理

    ​​​​data class BookInformation(var bookTitle: String = "", ​​​​ var authorName: String = "", ​​​​ var bookPrice: Float = -1F) { ​​​​ companion object { ​​​​ const val XML_KEY_BOOK = "book" ​​​​ const val XML_KEY_TITLE = "title" ​​​​ const val XML_KEY_AUTHOR = "author" ​​​​ const val XML_KEY_PRICE = "price" ​​​​ } ​​​​} ​​​​class DomXml { ​​​​ fun parseByDom(inputStream: InputStream) { ​​​​ val list = mutableListOf<BookInformation>() ​​​​ val docBuilder = DocumentBuilderFactory.newInstance().run { ​​​​ newDocumentBuilder() ​​​​ } ​​​​ val doc = docBuilder.parse(inputStream) ​​​​ val bookList = doc.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK) ​​​​ for (i in 0 until bookList.length) { ​​​​ val element = bookList.item(i) as Element ​​​​ val bookInformation = BookInformation() ​​​​ val bookDetail = element.childNodes ​​​​ for (j in 0 until bookDetail.length) { ​​​​ val bookItem = bookDetail.item(j) ​​​​ when(bookItem.nodeName) { ​​​​ BookInformation.XML_KEY_TITLE -> { ​​​​ bookInformation.bookTitle = bookItem.textContent ​​​​ } ​​​​ BookInformation.XML_KEY_AUTHOR -> { ​​​​ bookInformation.authorName = bookItem.textContent ​​​​ } ​​​​ BookInformation.XML_KEY_PRICE -> { ​​​​ bookInformation.bookPrice = bookItem.textContent.toFloat() ​​​​ } ​​​​ } ​​​​ } ​​​​ list.add(bookInformation) ​​​​ } ​​​​ list.forEach { ​​​​ println(it) ​​​​ } ​​​​ } ​​​​}
  • 測試使用 DOM API 解析 XML:

    ​​​​fun main() { ​​​​ val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml") ​​​​ val inputStream = FileInputStream(xmlFile) ​​​​ try { ​​​​ DomXml().parseByDom(inputStream) ​​​​ } finally { ​​​​ inputStream.close() ​​​​ } ​​​​}

    image

DOM 創建 XML

  • DOM 與 SAX 的令一個不同點在於,它也有提供對應創建(也可以覆蓋)XML 的對應 API;以下我們修改 DOM Parse XML 的範例

    1. 首先我們先 Parse XML 得到物件樹,保存在記憶體 List<BookInformation> 內,並返回給 DOM 使用者

      • 這裡返回的並不是 DOM 在記憶體內建構的 Node 節點,而是應用 DOM Parse 後,自己建立的物件
      ​​​​​​​​class DomXml { ​​​​​​​​ ​​​​​​​​ private lateinit var curDocument: Document ​​​​​​​​ fun parseByDom(inputStream: InputStream): List<BookInformation> { ​​​​​​​​ val list = mutableListOf<BookInformation>() ​​​​​​​​ val docBuilder = DocumentBuilderFactory.newInstance().run { ​​​​​​​​ newDocumentBuilder() ​​​​​​​​ } ​​​​​​​​ val doc = docBuilder.parse(inputStream).apply { ​​​​​​​​ curDocument = this ​​​​​​​​ } ​​​​​​​​ val bookList = doc.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK) ​​​​​​​​ for (i in 0 until bookList.length) { ​​​​​​​​ val element = bookList.item(i) as Element ​​​​​​​​ val bookInformation = BookInformation() ​​​​​​​​ val bookDetail = element.childNodes ​​​​​​​​ for (j in 0 until bookDetail.length) { ​​​​​​​​ val bookItem = bookDetail.item(j) ​​​​​​​​ when(bookItem.nodeName) { ​​​​​​​​ BookInformation.XML_KEY_TITLE -> { ​​​​​​​​ bookInformation.bookTitle = bookItem.textContent ​​​​​​​​ } ​​​​​​​​ BookInformation.XML_KEY_AUTHOR -> { ​​​​​​​​ bookInformation.authorName = bookItem.textContent ​​​​​​​​ } ​​​​​​​​ BookInformation.XML_KEY_PRICE -> { ​​​​​​​​ bookInformation.bookPrice = bookItem.textContent.toFloat() ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​ list.add(bookInformation) ​​​​​​​​ } ​​​​​​​​ list.forEach { ​​​​​​​​ println(it) ​​​​​​​​ } ​​​​​​​​ return list ​​​​​​​​ } ​​​​​​​​}
    2. 再來撰寫一個透過 DOM 提供的 API,來將改變後的數據寫入到新 XML 中…

      將使用者設定的新數據,寫入到 DOM 的 Node 節點之中

      透過 element#getElementsByTagName 取得節點,並修改節點數據

      ​​​​​​​​class DomXml { ​​​​​​​​ fun updateXmlPrice(book: BookInformation, outputStream: FileOutputStream) { ​​​​​​​​ val bookList = curDocument.documentElement.getElementsByTagName(BookInformation.XML_KEY_BOOK) ​​​​​​​​ // 找到要修改的書籍 ​​​​​​​​ for (i in 0 until bookList.length) { ​​​​​​​​ val element = bookList.item(i) as Element ​​​​​​​​ val title = element.getElementsByTagName(BookInformation.XML_KEY_TITLE).item(0).textContent ​​​​​​​​ if (book.bookTitle != title) { ​​​​​​​​ continue ​​​​​​​​ } ​​​​​​​​ // 取得 price 節點 ​​​​​​​​ val prizeNode = element.getElementsByTagName(BookInformation.XML_KEY_PRICE).item(0) ​​​​​​​​ // 修改價格 ​​​​​​​​ prizeNode.textContent = book.bookPrice.toString() ​​​​​​​​ // 將修改後的 XML 寫回文件 ​​​​​​​​ val transformerFactory = TransformerFactory.newInstance() ​​​​​​​​ transformerFactory.newTransformer().apply { ​​​​​​​​ // 設定縮排 ​​​​​​​​ setOutputProperty(OutputKeys.INDENT, "yes") ​​​​​​​​ val streamResult = StreamResult(outputStream) ​​​​​​​​ // 文件節點 ​​​​​​​​ val domSource = DOMSource(curDocument) ​​​​​​​​ // 將修改後的物件,寫入目標文件 ​​​​​​​​ transform(domSource, streamResult) ​​​​​​​​ } ​​​​​​​​ println("XML updated successfully.") ​​​​​​​​ break ​​​​​​​​ } ​​​​​​​​ } ​​​​​​​​}
    • 使用 DOM 寫 XML 範例

      ​​​​​​​​fun main() { ​​​​​​​​ val xmlFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books.xml") ​​​​​​​​ val xmlRefactorFile = File("/home/alien/AndroidStudioProjects/Network/app/src/main/res/xml/books_refactor.xml") ​​​​​​​​ val inputStream = FileInputStream(xmlFile) ​​​​​​​​ val outputStream = FileOutputStream(xmlRefactorFile) ​​​​​​​​ try { ​​​​​​​​ val domXml = DomXml() ​​​​​​​​ val list = domXml.parseByDom(inputStream).apply { ​​​​​​​​ this[0].bookPrice = 100.876f ​​​​​​​​ } ​​​​​​​​ domXml.updateXmlPrice(list[0], outputStream) ​​​​​​​​ } finally { ​​​​​​​​ inputStream.close() ​​​​​​​​ } ​​​​​​​​}

      image

PULL 解析

如果是使用 Android 平台開發,除了 SAX、DOM 之外… Android 平台原生還提供了一個內置的 PULL 解析器!同樣可以用來解析 XML 文件

PULL 原理

  • PULL 採用與 SAX 類似的解析方式

    它也是採用「事件驅動」的方式解析 XML,當遇到元素時會產生事件並驅動,讓使用者透過界面接收到事件

PULL 解析 XML

  • 目標 XML 如下(需要將檔案放置在 assets 目錄下,之後會利用 Android Context 取得目標檔案)

    ​​​​<?xml version="1.0" encoding="utf-8"?> ​​​​<!DOCTYPE bookstore [ ​​​​ <!ELEMENT bookstore (book+)> ​​​​ <!ELEMENT book (title, author, price)> ​​​​ <!ELEMENT title (#PCDATA)> ​​​​ <!ELEMENT author (#PCDATA)> ​​​​ <!ELEMENT price (#PCDATA)> ​​​​ ]> ​​​​<bookstore> ​​​​ <book> ​​​​ <title>Introduction to XML</title> ​​​​ <author>John Doe</author> ​​​​ <price>29.95</price> ​​​​ </book> ​​​​ <book> ​​​​ <title>Java Programming</title> ​​​​ <author>Jane Smith</author> ​​​​ <price>39.99</price> ​​​​ </book> ​​​​</bookstore>

    image

  • 以下使用 Android 系統內提供的 XmlPullParser 來解析 XML 檔案

    ​​​​class PullXml { ​​​​ fun parseByAndroidPull(inputStream: InputStream): List<BookInformation> { ​​​​ val list = mutableListOf<BookInformation>() ​​​​ var curBookInformation: BookInformation? = null ​​​​ val xmlPullParser: XmlPullParser = Xml.newPullParser().apply { ​​​​ // 設定 input stream 以及解碼方式 ​​​​ setInput(inputStream, Charsets.UTF_8.name()) ​​​​ } ​​​​ var event = xmlPullParser.eventType ​​​​ while (event != XmlPullParser.END_DOCUMENT) { ​​​​ if (event == XmlPullParser.START_TAG) { ​​​​ when(xmlPullParser.name) { ​​​​ BookInformation.XML_KEY_BOOK -> { ​​​​ curBookInformation = BookInformation() ​​​​ } ​​​​ BookInformation.XML_KEY_TITLE -> { ​​​​ curBookInformation!!.bookTitle = xmlPullParser.nextText() ​​​​ } ​​​​ BookInformation.XML_KEY_AUTHOR -> { ​​​​ curBookInformation!!.authorName = xmlPullParser.nextText() ​​​​ } ​​​​ BookInformation.XML_KEY_PRICE -> { ​​​​ curBookInformation!!.bookPrice = xmlPullParser.nextText().toFloat() ​​​​ } ​​​​ } ​​​​ } else if (event == XmlPullParser.END_TAG) { ​​​​ if (curBookInformation != null) { ​​​​ list.add(curBookInformation) ​​​​ curBookInformation = null ​​​​ } ​​​​ } ​​​​ event = xmlPullParser.next() ​​​​ } ​​​​ return list ​​​​ } ​​​​}

    使用 Android 的機器測試(整合測試)才能測試 Pull 的 Parse

    • Android XmlPullParser 服務必須運行在 Android Runtime 中

      image

    ​​​​@RunWith(AndroidJUnit4::class) ​​​​class ExampleInstrumentedTest { ​​​​ @Test ​​​​ fun pullWithAndroidPull() { ​​​​ // Context of the app under test. ​​​​ val appContext = InstrumentationRegistry.getInstrumentation().targetContext ​​​​ val inputStream = appContext.resources.assets.open("books.xml") ​​​​ try { ​​​​ val pullXml = PullXml() ​​​​ val list = pullXml.parseByAndroidPull(inputStream) ​​​​ list.forEach { ​​​​ println("---- $it") ​​​​ } ​​​​ } finally { ​​​​ inputStream.close() ​​​​ } ​​​​ } ​​​​}

    image

PULL 創建 XML

  • Pull 雖然同 SAX 一樣不會在記憶體中建構物件樹,不過我們仍可以自己透過在記憶體中建立並輸出到目標檔案中(以下方法同樣也可以使用在 SAX)

    ​​​​fun createByAndroidPull(outputStream: OutputStream, list: List<BookInformation>) { ​​​​ val xmlNameSpace = null ​​​​ val encodeType = Charsets.UTF_8.name() ​​​​ val xmlSerializer = Xml.newSerializer().apply { ​​​​ setOutput(outputStream, encodeType) ​​​​ setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true) ​​​​ } ​​​​ xmlSerializer.startDocument(encodeType, true) ​​​​ xmlSerializer.startTag(xmlNameSpace, "bookstore") ​​​​ list.forEach { ​​​​ xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_BOOK) ​​​​ xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_TITLE) ​​​​ xmlSerializer.text(it.bookTitle) ​​​​ xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_TITLE) ​​​​ // Author ​​​​ xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_AUTHOR) ​​​​ xmlSerializer.text(it.authorName) ​​​​ xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_AUTHOR) ​​​​ // Price ​​​​ xmlSerializer.startTag(xmlNameSpace, BookInformation.XML_KEY_PRICE) ​​​​ xmlSerializer.text(it.bookPrice.toString()) ​​​​ xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_PRICE) ​​​​ xmlSerializer.endTag(xmlNameSpace, BookInformation.XML_KEY_BOOK) ​​​​ } ​​​​ xmlSerializer.endTag(xmlNameSpace, "bookstore") ​​​​ xmlSerializer.endDocument() ​​​​}
    • 如果是建立 attribute 的話,那數值會被寫在 tag 中

      ​​​​​​​​xmlSerializer.attribute(xmlNameSpace, BookInformation.XML_KEY_AUTHOR, it.authorName)

      建立出來的數據就會寫在 tag 中(如下)

      ​​​​​​​​<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> ​​​​​​​​<bookstore> ​​​​​​​​ <book title="Introduction to XML" ​​​​​​​​ author="Kyle" ​​​​​​​​ price="29.95" /> ​​​​​​​​ ​​​​​​​​ <book title="Java Programming" ​​​​​​​​ author="Jane Smith" ​​​​​​​​ price="39.99" /> ​​​​​​​​</bookstore>

    測試程式

    以下測試運行在 Android App 中(非整合測試)

    ​​​​fun testAndroidPull() { ​​​​ val appContext = this ​​​​ val inputStream = appContext.resources.assets.open("books.xml") ​​​​ // 開啟或創建文件在內部存儲空間 ​​​​ val fileOutputStream: FileOutputStream = appContext.openFileOutput("hello.xml", Context.MODE_PRIVATE) ​​​​ try { ​​​​ val pullXml = PullXml() ​​​​ val list = pullXml.parseByAndroidPull(inputStream).apply { ​​​​ // 修改數據 ​​​​ this[0].authorName = "Kyle" ​​​​ } ​​​​ pullXml.createByAndroidPull(fileOutputStream, list) ​​​​ } finally { ​​​​ inputStream.close() ​​​​ fileOutputStream.close() ​​​​ } ​​​​}

    image

其他

SAX vs DOM

  • SAX、DOM 兩者都是對於解析 XML 的方案,不過特點、解決思路都不盡相同,兩者的比較表如下…

    SAX DOM
    XML 物件建立 手動分析、建立 自動分析、建立
    XML 的限制 可以讀取不限大小 XML 檔 由於會在內存中自動幫我們建立物件樹,所以 XML 大小不能太大
    XML 讀取 以順序方式讀取(不能回頭) 建立完物件樹後,可隨意讀取
    XML 修改 只能讀取 XML,不能修改! 可以透過對物件樹的修改,來改動到 XML 檔案
    技術的特點 較為自由(有很多地方可以設定),但相對的也較為麻煩 易於操作、開發

Appendix & FAQ

tags: 網路開發