XML詳解:第三部分 XML解析

XML解析

若是不一樣廠商開發的XML解析器提供的API都依賴於自向的產品,那麼應用程序要使用解析器,就只能使用特定於廠商的API。假如之後應用程序須要更換解析器,那麼就只能從新編寫代碼了。慶幸的是,目前幾乎全部的解析器都對兩套標準的API提供了支持,這兩套API就是DOMSAX

 

DOMDocument Object Model)的縮寫,即文檔對象模型,是W3C組織推薦的處理XML的標準接口。SAXSimple API for XML的縮寫,它不是某個官方機構的標準,但它是XML社區事實上的標準。雖然只是「民間」的標準,可是它在XML中的應用不比DOM少,幾乎全部XML解析器都支持它。

 

DOMSAX只是定義了一些接口,以及某些接口的默認實現(什麼事情也不作),一個應用程序要想利用DOMSAX訪問XML文檔,還須要一個實現了DOMSAX的解析器,即實現DOMSAX中定義的接口,提供DOMSAX定義的功能。

 

ApacheXerces是一個使用很是普遍的解析器,它提供了DOMSAX的調用接口。SAX的標準解析接口爲org.xml.sax.XMLReader,而Xerces中提供的解析接口的實現類爲org.apache.xerces.parsers.SAXParser,在應用程序中能夠採用以下方式訪問XML文檔:

org.xml.sax.XMLReader sp = new org.apache.xerces.parsers.SAXParser();

現有一個問題,雖然咱們使用的是標準的DOMSAX接口,但不一樣解析器的實現類是不一樣的,若是要使用另一種解析器,仍然須要修改應用程序,只不過修改的代碼量較少。有沒有辦法讓咱們在更換解析器時,不用對已發佈程序作任何改變呢?JAXP API能夠幫咱們實現這一點。爲了屏蔽具體廠商的實現,讓開發人員以一種標準的方式對XML進行編程,SUN制定了JAXPJava API for XML Processing規範。JAXP沒有提供解析XML的新方法,也沒有爲XML的處理提供新的功能,它只是在解析器之上封閉了一個抽象層,容許開發人員以獨立於廠商的API調用訪問XML數據。

 

JAXP開發包由javax.xml包及其子包、org.w3c.dom包及其子包、org.xml.sax包及其子包組成。在javax.xml.parsers包中,定義了幾個工廠類,用於加載DOMSAX的實現類,JAXP由接口、抽象類和一些輔助類組成,符合JAXP規範的解析器實現其中的接口和抽象類,開發人員只要使用JAXPAPI編程,底層的解析器就能夠任意更換。

應用程序àJAXP的接口與抽象類àXercesJAXP實現àXercesDOMSAX解析器

有了JAXP,開發人員就能夠隨意更換解析器的實現,而不須要修改應用程序。

使用DOM解析XML

DOM是獨立於程序語言的,W3C組織以IDL(接口中定義語言)的形式定義了DOM中接口。某種語言要實現DOM,須要將DOM接口轉換爲本語言中的對應結構。W3C提借了JavaECMAScript這兩種語言的實現。

 

 

Node對象是DOM結構中最基本的對象,表明了文檔樹中的一個節點,一般直接使用Node不多,通常使用DocumentElementAttrTextNode對象的子對象來操做文檔。雖然在Node接口中定義了對節點進行操做的通用方法,可是有一些Node對象的子對象,如Text對象並不存在子節點。

image003

一、  Node接口主要是對它的子節點進行增、刪、獲取。

二、  Node接口中定義了各類節點的類型常量,能夠用它們來判斷是哪一種節點。

三、  void normalize():將該節點全部的後代文本節點,包括屬性節點,調整爲規範化的形式,這僅是以結構(如:元素、註釋、處理指令、CDATA段、實體引用)來分隔文本節點,也就是說,在節點所在的這棵樹下,既不存在相鄰的文本節點,也不存在空的文本節點。

 

 

Node節點的getNodeNamegetNodeValue getAttributes 的值將根據如下節點類型的不一樣而不一樣。

Interface(節點類型)

getNodeName(節點名字)

getNodeValue(節點值)

getAttributes(節點屬性)

Attr

Attr.name 相同

Attr.value 相同

null

CDATASection

"#cdata-section"

CharacterData.data 相同,CDATA 節的內容

null

Comment

"#comment"

CharacterData.data 相同,該註釋的內容

null

Document

"#document"

null

null

DocumentFragment

"#document-fragment"

null

null

DocumentType

DocumentType.name 相同

null

null

Element

Element.tagName 相同

null

NamedNodeMap

Entity

entity name

null

null

EntityReference

引用的實體名稱

null

null

Notation

notation name

null

null

ProcessingInstruction

ProcessingInstruction.target 相同

ProcessingInstruction.data 相同

null

Text

"#text"

CharacterData.data 相同,該文本節點的內容

null

DOM樹中的節點類型

XML中最多見的節點類型是:文檔、元素、文本和屬性節點,在DOM API中對應的接口是DocumentElementTextAttr

文檔節點Document

文檔節點是文檔樹的根節點,也是文檔中其餘全部節點的父節點。要注意的是,文檔節點並非XML文檔的根元素,由於在XML文檔中,處理指令、註釋等內容能夠出如今根元素之外,因此在構造DOM樹時,根元素並不適合做爲根節點,因而就有了文檔節點,而根元素則做爲文檔節點的子節點。image004

四、  經過 Element getDocumentElement()方法獲得XML文檔的根元素。

五、  經過createXX相應的方法建立不一樣類型的節點。

六、  Element getElementById(String elementId):經過給出的ID類型的屬性值elementId來查找對應用的元素。一個ID類型的屬性值惟一標識了XML文檔中的一個元素。除非特別定義,名字爲「ID」或者「id」的屬性,其類型並非ID類型。

七、  NodeList getElementsByTagName(String tagname):以文檔順序返回標籤名字爲tagname的全部元素。若是參數爲「*」,則返回全部的元素。

八、  NodeList getElementsByTagNameNS(String namespaceURI,String localName):按照指定的名稱空間URI和元素的本地名返回全部匹配的元素。若是參數namespaceURI爲「*」,則表示全部的名稱空間,一樣,若是localName爲「*」,則匹配全部元素。

元素節點Element

image005

文本節點Text

image006

屬性節點Attr

屬性其實是屬於某個元素的,因此屬性節點不是元素的子節點。於是在DOM中,屬性節點沒有被做爲文檔樹的一部分,因此在屬性節點上調用getParentNodegetPreviousSiblinggetNextSibling時返回null

image007

NodeList接口

image008

Node

NodeList getChildNodes();

 

DocumentElement

NodeList getElementsByTagName(String tagname);

NodeList getElementsByTagNameNS(String namespaceURI, String localName);

NamedNodeMap接口

image009

Node

NamedNodeMap getAttributes();

 

DocumentType

NamedNodeMap getEntities();

NamedNodeMap getNotations();

DOM解析器工廠和DOM解析器

javax.xml.parsers包中,定義了DOM解析器工廠類DocumentBuilderFactory,用來產生DOM解析器。DocumentBuilderFactory工廠類是一個抽象類,在這個類中提供了一個靜態方法newInstance()方法,用來建立一個工廠類的實例。

 

DocumentBuilderFactory是個抽象類,那它的newInstance()產生的是哪一個實現呢?採用JAXP編程能夠任意更換解析器的關鍵就在於這個工廠類。DocumentBuilderFactory抽象類的實現由遵守JAXP規範的解析器提供商來給出的。解析器提供商爲自身的解析器編寫一個從DocumentBuilderFactory類繼承的工廠類,而後由這個工廠類實例負責產生解析器對象。

 

那麼DocumentBuilderFactorynewInstance()方法是如何找到解析器提供商給出的工廠類呢?可經過下面3種途徑依次查找解析器工廠類:

一、  首先查看是否設置了javax.xml.parsers.DocumentBuilderFactory系統屬性。這又可經過兩種方式來設置這個系統屬性:

    System.setProperty("javax.xml.parsers.DocumentBuilderFactory",

           "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");

建議不要使用此種方式,由於若是更換解析器,程序就要修改。另外一種是在用java.exe執行程序時,經過-D選項來設置系統屬性:

java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocument DOMTest

二、  查找JRE目錄中lib子目錄下的jaxp.properties文件,若是存在,則讀取該配置文件:

javax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

三、  classpath環境變量所指定JAR文件中,查找META-INF/services目錄下的javax.xml.parsers.DocumentBuilderFactory文件,使用這個文件中所指定的工廠類名來構造工廠的實例,這種方式被多數解析器提供商所採用,在他們的發佈的包包含解析器的JAR包中,每每會提供這個的文件,咱們來看看Apache提供的解析器實現包xercesImpl.jar

image010

其中javax.xml.parsers.DocumentBuilderFactory文件的內容以下:

org.apache.xerces.jaxp.DocumentBuilderFactoryImpl

若是上面3種途徑都沒有找到解析器工廠類,就使用平臺默認的解析器工廠。從JAXP1.2開始,SUN公司對ApacheXerces解析器從新包裝了一下,並將org.apache.xerces包名改成了com.sun.org.apache.xerces.internal,而後在JAXP的開發包中一塊兒提供,做爲默認的解析器。

 

在獲得工廠實例後,就能夠經過DocumentBuilderFactorynewDocumentBuilder()方法來建立DOM解析器實例了:

DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();

DocumentBuilder db = dbf.newDocumentBuilder();//獲得具體廠商的XML解析器

Document d = db.parse("text.xml");//經過解析器解析獲得文檔對象

 

DocumentBuilderFactory另外幾個重要的方法:

l  public void setValidating(boolean validating):指定解析器是否難被解析的文檔,默認爲false。注,該方法只能DTD驗證有效。若是使用Schema驗證,則可設爲false後,使用setSchema(Schema)方法將一個模式與解析關聯。

l  public void setSchema(Schema schema):若是要使用模式對XML文檔進行驗證,須要使用該方法。Schema對象由javax.xml.validation.SchemaFactory工廠類建立。

l  public void setIgnoringElementContentWhitespace(boolean whitespace):是否要忽略元素內容中的空白。默認是false。按照XML1.0的推薦標準,元素內容中的空白必須由解析器保留,但當根據DTD進行驗證時,解析器能夠知道文檔的特定部分不會支持空格(如具備元素型內容的元素),因此這一區域的任何空格都「可忽略的」。注,這一標誌只有經過setValidating打開驗證功能後纔有效。

使用DOM解析XML文檔的實例

--students.xml

<?xml version="1.0" encoding="GB2312"?>

 

<?xml-stylesheet type="text/xsl" href="students.xsl"?>

 

<students>

    <student sn="01">

        <name>張三</name>

        <age>18</age>

    </student>

   

    <student sn="02">

        <name>李四</name>

        <age>20</age>

    </student>

</students>

--end

遍歷文檔

publicclass DOMPrinter {

    /**

     * 輸出節點的類型、名字和值。

     */

    publicstaticvoid printNodeInfo(String nodeType, Node node) {

       System.out.println(nodeType + "\t" + node.getNodeName() + " : "

              + node.getNodeValue());

    }

    /**

     * 採用遞歸調用,輸出給定節點下的全部後代節點。

     * 注:爲了簡單起見,只對處理指令節點、元素節點、

     * 屬性節點和文本節點進行了處理。

     */

    publicstaticvoid traverseNode(Node node) {

       short nodeType = node.getNodeType();

       switch (nodeType) {

       case Node.PROCESSING_INSTRUCTION_NODE:

           printNodeInfo("處理指令", node);

           break;

       case Node.ELEMENT_NODE:

           printNodeInfo("元素", node);

           NamedNodeMap attrs = node.getAttributes();

           int attrNum = attrs.getLength();

           for (int i = 0; i < attrNum; i++) {

              Node attr = attrs.item(i);

              printNodeInfo("屬性", attr);

           }

           break;

       case Node.TEXT_NODE:

           printNodeInfo("文本", node);

           break;

       default:

           break;

       }

       Node child = node.getFirstChild();

       while (child != null) {

           // 遞歸調用

           traverseNode(child);

           child = child.getNextSibling();

       }

    }

    publicstaticvoid main(String[] args) {

       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

       try {

           DocumentBuilder db = dbf.newDocumentBuilder();

           Document doc = db.parse(new File("students.xml"));

           // Document接口提供了三個方法,分別用於獲取XML聲明的三個部分:

           // 版本、文檔編碼、獨立文檔。

           // 調用這三個方法須要的JDK版本最小是1.5

           System.out.println("<?xml='" + doc.getXmlVersion() + "' encoding='"

                  + doc.getXmlEncoding() + "' standalone='"

                  + doc.getXmlStandalone() + "'?>");

           traverseNode(doc);

       } catch (ParserConfigurationException e) {

           e.printStackTrace();

       } catch (SAXException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       }

    }

}

添加、刪除、修改和保存

publicclass DOMConvert {

    publicstaticvoid main(String[] args) {

       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

       try {

           DocumentBuilder db = dbf.newDocumentBuilder();

           Document doc = db.parse("students.xml");

 

           // ------------------添加節點------------------

           // 建立表示一個學生信息的各元素節點。

           Element eltStu = doc.createElement("student");

           Element eltName = doc.createElement("name");

           Element eltAge = doc.createElement("age");

 

           // 建立<student>元素的sn屬性節點。

           Attr attr = doc.createAttribute("sn");

           attr.setValue("03");

 

           // 建立表明學生信息的文本節點。

           Text txtName = doc.createTextNode("王五");

           Text txtAge = doc.createTextNode("19");

 

           // 將文本節點添加爲對應的元素節點的子節點。

           eltName.appendChild(txtName);

           eltAge.appendChild(txtAge);

 

           // nameage節點添加爲student節點的子節點。

           eltStu.appendChild(eltName);

           eltStu.appendChild(eltAge);

 

           // <student>元素添加sn屬性節點。

           eltStu.setAttributeNode(attr);

 

           // 獲得XML文檔的根元素。

           Element eltRoot = doc.getDocumentElement();

 

           // student節點添加爲根元素的子節點。

           eltRoot.appendChild(eltStu);

 

           NodeList nl = doc.getElementsByTagName("student");

 

           // ------------------刪除節點------------------

           Node nodeDel = nl.item(0);

           nodeDel.getParentNode().removeChild(nodeDel);

 

           // ------------------修改節點------------------

           // 注意:NodeList對象是活動的,因此前面刪除節點的操做會影響到NodeList對象,

           // NodeList中的節點對象會從新進行排列,此時,索引爲0的節點是

           // 先前節點列表中索引爲1的節點。

           Element eltChg = (Element) nl.item(0);

           Node nodeAgeChg = eltChg.getElementsByTagName("age").item(0);

           nodeAgeChg.getFirstChild().setNodeValue("22");

 

           // 輸出修改後的學生信息。

           for (int i = 0; i < nl.getLength(); i++) {

              Element elt = (Element) nl.item(i);

 

              Node nodeName = elt.getElementsByTagName("name").item(0);

              Node nodeAge = elt.getElementsByTagName("age").item(0);

 

              String name = nodeName.getFirstChild().getNodeValue();

              String age = nodeAge.getFirstChild().getNodeValue();

 

              System.out.println("-----------------學生信息-----------------");

              System.out.println("編號:" + elt.getAttribute("sn"));

              System.out.print("姓名:");

              System.out.println(name);

 

              System.out.print("年齡:");

              System.out.println(age);

              System.out.println();

           }

          

           // ------------------保存修改結果------------------

           // 利用文檔節點建立一個DOM輸入源。

           DOMSource source = new DOMSource(doc);

 

           // converted.xml文件構造一個StreamResult對象,用於保存轉換後結果。

           StreamResult result = new StreamResult(new File("converted.xml"));

 

           // 獲得轉換器工廠類的實例。

           TransformerFactory tff = TransformerFactory.newInstance();

           // 建立一個新的轉換器,用於執行恆等轉換,

           // 即直接將DOM輸入源的內容複製到結果文檔中。

           Transformer tf = tff.newTransformer();

           tf.setOutputProperty(OutputKeys.INDENT, "yes");//縮進

           tf.setOutputProperty(OutputKeys.ENCODING, "gb2312");//編碼

           // 執行轉換。

           tf.transform(source, result);

       } catch (ParserConfigurationException e) {

           e.printStackTrace();

       } catch (SAXException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       } catch (TransformerConfigurationException e) {

           e.printStackTrace();

       } catch (TransformerException e) {

           e.printStackTrace();

       }

    }

}

 

使用SAX解析XML

XMLReader(解析器接口)

SAX解析器接口和事件處理器接口在org.xml.sax包中定義,XMLReaderSAX2.0解析器必須實現的接口(SAX1.0解析器實現Parser接口,該接口已再也不使用)。

l  setEntityResolver(EntityResolver resolver):註冊一個實體解析器。

l  setDTDHandler(DTDHandler handler):註冊一個DTD事件處理器。

l  setContentHandler(ContentHandler handler):註冊一個內容事件處理器。

l  parse(InputSource input):解析XML文檔。

ContentHandler(內容事件處理器接口)

SAXAPI定義了許多事件,這些事件分別由事件處理器中相應的方法去響應。在SAX1.0前使用的是DocumentHandler接口,SAX2.0中已被ContentHandler取代。

 

startPrefixMapping(String prefix, String uri):在一個前綴URI名稱空間映射範圍的開始時被調用。

<students xmlns:stu="http://www.sunin.org/students">

       …

</stuents>

SAX解析器解析到<stuedents>元素時,就會調用startPrefixMapping方法,將stu傳遞給prefiex參數,將http://www.sunin.org/students傳遞給uri參數,而後產生<students>元素的startElement事件。在產生<students>元素的endElement事件後,解析器將調用endPrefixMapping方法。

SAX解析器工廠

Dom相似,JAXP也爲SAX解析器提供了工廠類——SAXParserFactory類。與DOM解析器工廠類不一樣的是,SAXParserFactory類的newInstance()方法查找的工廠類屬性爲:java.xml.parsers.SAXParserFactory,若是咱們使用ApacheXerces解析器,能夠配置以下:

java.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl

 

JAXP中定義的SAX解析器類是SAXParser,獲取SAXParser類的實例與獲取DocumentBuilder類的實例類似,在獲得工廠類的實例後,經過SAXParserFactory 實現類實例的newSAXParser()方法獲得 SAX解析器實例:public abstract SAXParser newSAXParser()

 

你能夠調用SAXParser或者XMLReader中的parser()方法業解析文檔,效果是徹底同樣的。不過在SAXParser中的parse()方法能接受更多的參數。能夠對不一樣的XML文檔數據源進行解析,所以使用起來比XMLReader要方便一些。

 

另外,與DOM不一樣的是,SAX自己也提供了建立XMLReader對象的工廠類,在org.xml.sax.helpers包中提供了XMLReaderFactory類,該類createXMLReader用於建立XMLReader對象。

 

實際上,SAXParserJAXPXMLReader實現類的一個包裝類,在SAXPars中定義了getXMLReader()方法,用於返回它內部的XMLReader實例:abstract  XMLReader getXMLReader()

使用SAX解析XML

解析並完整輸出XML文檔

/**

* 使用SAX解析XML文檔,實際上就是編寫事件處理器。

* 爲了簡化事件處理器接口的實現,咱們讓SAXPrinter類繼承DefaultHandler幫助類

*/

publicclass SAXPrinter extends DefaultHandler {

    // SAX解析器開始解析文檔時,將會調用這個方法

    publicvoid startDocument() throws SAXException {

       // 輸出XML聲明。

       System.out.println("<?xml version='1.0' encoding='GB2312'?>");

    }

 

    // SAX解析器讀取了處理指令,將會調用這個方法

    publicvoid processingInstruction(String target, String data)

           throws SAXException {

       // 輸出文檔中的處理指令。

       System.out.println("<?" + target + " " + data + "?>");

    }

 

    // SAX解析器讀取了元素的開始標籤後,將會調用這個方法

    publicvoid startElement(String uri, String localName, String qName,

           Attributes attrs) throws SAXException {

       // 輸出元素的開始標籤及其屬性。

       System.out.print("<" + qName);

       int len = attrs.getLength();

       for (int i = 0; i < len; i++) {

           System.out.print(" ");

           System.out.print(attrs.getQName(i));

           System.out.print("=\"");

           System.out.print(attrs.getValue(i));

           System.out.print("\"");

       }

       System.out.print(">");

    }

 

    // SAX解析器讀取了字符數據後,將會調用這個方法

    publicvoid characters(char[] ch, int start, int length)

           throws SAXException {

       // 輸出元素的字符數據內容。

       System.out.print(new String(ch, start, length));

    }

 

    // SAX解析器讀取了元素的結束標籤後,將會調用這個方法

    publicvoid endElement(String uri, String localName, String qName)

           throws SAXException {

       // 輸出元素的結束標籤。

       System.out.print("</" + qName + ">");

    }

 

    publicstaticvoid main(String[] args) {

       SAXParserFactory spf = SAXParserFactory.newInstance();

       SAXParser sp = null;

       try {

           sp = spf.newSAXParser();

           File file = new File("students.xml");

           sp.parse(file, new SAXPrinter());

       } catch (ParserConfigurationException e) {

           e.printStackTrace();

       } catch (SAXException e) {

           e.printStackTrace();

       } catch (IOException e) {

           e.printStackTrace();

       }

    }

}

使用ErrorHandler處理解析中的錯誤

DefaultHandler既現實了ContentHandler接口,又實現了ErrorHandler接口。

 

publicclass ErrorProcessor extends DefaultHandler {

    publicvoid warning(SAXParseException ex) throws SAXException {

       System.err.println("[Warning] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    publicvoid error(SAXParseException ex) throws SAXException {

       System.err.println("[Error] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    publicvoid fatalError(SAXParseException ex) throws SAXException {

       System.err.println("[Fatal Error] " + getLocationString(ex) + ": "

              + ex.getMessage());

    }

 

    /**

     * 獲取致使錯誤或者警告的文本結束位置的行號和列號。

     * 若是是實體引起錯誤,還獲取它的公共標識符和系統標識符。

     */

    private String getLocationString(SAXParseException ex) {

       StringBuffer str = new StringBuffer();

 

       String publicId = ex.getPublicId();

       if (publicId != null) {

           str.append(publicId);

           str.append(" ");

       }

 

       String systemId = ex.getSystemId();

       if (systemId != null) {

           str.append(systemId);

           str.append(':');

       }

 

       str.append(ex.getLineNumber());

       str.append(':');

       str.append(ex.getColumnNumber());

 

       return str.toString();

    }

 

    /**

     * 輸出元素的結束標籤,以便於查看不一樣類型的錯誤對文檔解析的影響。

     */

    publicvoid endElement(String uri, String localName, String qName)

           throws SAXException {

       System.out.println("</" + qName + ">");

    }

 

    publicstaticvoid main(String[] args) {

       try {

           /*

            *  利用XMLReaderFactory工廠類,建立XMLReader對象。

            *  注,這裏沒有使用JAXP中定義的SAX解析器工廠類和解析器類,

            *  而是使用了SAX自己定義的XMLReaderFactory工廠類與XMLRader

            *  解析器。固然最好是使用JAXP中的工廠類。

            */

           XMLReader xmlReader = XMLReaderFactory.createXMLReader();

           // 打開解析器的驗證功能。

           xmlReader

                  .setFeature("http://xml.org/sax/features/validation", true);

 

           ErrorProcessor ep = new ErrorProcessor();

           xmlReader.setErrorHandler(ep);

           xmlReader.setContentHandler(ep);

 

           // InputSource類位於org.xml.sax包中,表示XML實體的輸入源。

           // 它能夠用java.io.InputStream對象來構造,更多信息請參看JDKAPI文檔

           InputSource is = new InputSource(

                  new FileInputStream("students.xml"));

           xmlReader.parse(is);

       } catch (SAXException e) {

           System.out.println(e.toString());

       } catch (IOException e) {

           System.out.println(e.toString());

       }

    }

}

若是被解析的XML不符合對應的模式文檔(DTDSchema)或不存在模式文檔時,都會報錯

相關文章
相關標籤/搜索