XML Java核心技术 读书笔记

本文介绍了XML文档的基本结构、解析方法及其在Java中的实现。包括DOM解析器、SAX解析器和StAX解析器的使用,同时展示了如何利用Java生成XML文档。

XML文档的结构

XML文档应当以一个文档头开始,如:

<?xml version="1.0"?>或

<?xml version="1.0" encoding="UTF-8"?>

严格说来,文档头是可有可无的,但是强烈推荐使用文档头。

文档头之后通常是文档类型定义(Document Type Definition,DTD),如:

<!DOCTYPE web-app PUBLIC"-//Sun Microsystems,Inc.//DTD Web Application 2.2//EN""http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">   

文档类型定义是确保文档正确的一个重要机制,但是这不是必需的。

最后,XML文档的正文包含根元素根元素包含其他一些元素。如:

<?xml version="1.0"?>
<!DOCTYPE configuration ...>
<configuration>
	<title>
		<font>
			<name>Helvetica</name>
			<size>36</size>
		</font>
	</title>
	...
</configuration>
注意:在设计XML文档结构时,最好使元素只包含子元素或只包含文本,也就是应该避免以下情况 :

<font>

Helvetica

<size>36</size>

</font>

在XML规范中,这叫混合式内容。如果避免了混合内容,可以简化解析过程。


XML元素可以包含属性,如<size unit="pt">36</size>

属性的灵活性比元素差,关于使用元素或属性的一个通常经验法则是,属性只应该在修改值的解释时使用,而不是在指定值时使用。

注意:在HTML中属性的使用规则很简:凡是不显示在网页上的都是属性,如<a href="http://java.sum.com">Java Technology</a>;然而,这个规则对于大多数XML并不那么管用。因为XML文件的数据并非像通常意义那样是让人浏览的。元素和文本是XML文档的主要要素,以下是你会遇到的其他一些标记的说明:

  • 字符引用的形式是&#十进制值或&#x十六进制值。
  • 实例引用的形式是&name。以下实例引用 

&lt;

&gt;

&amp;

&quot;

&apos;

它们表示:小于,大于 ,&,引号,省略号等字符。可以在DTD中定义其他的实体引用。

  • CDATA部分用<![  和 ]]>来限定界限。它们是字符数据的一种特殊形式。你可以使用它们来包含那些含有<,>,&之类字符的字符串,而不必将它们解释为标记,如:<![ CDATA[  <&> are my favorite delimiters  ] ]>,CDATA部分不能包含字符吕]]>。它常被用做将传统数据纳入XML文档的一种特殊方法。
  • 处理指令是指那些专门在处理XML文档的应用程序中使用的指令,它们将用<?   和   ?>来限定其界限,例如:<?xml-stylesheet href="mystyle.css" type="text/css"?>
每个XML都以下一个处理指令开头:<?xml version="1.0"?>

  • 注释用 <!-  和  -->限定其界限,例如:<!-- This is a comment.  -->注释不能含有字符串--。注释只是为了给文档的读者提供信息,其中绝不含有隐藏的命令,命令是由处理指令来实现。

解析XML文档

Java库提供了两个XML解析器:

像文档对象模型(Document Object Model,DOM)解析器这样的树型解析器,它们将读入的XML文档转换成树结构。

像用于XML的简单API(Simple API for XML,SAX)解析器这样的流机制解析器,它们在读入XML文档时生成相应的事件。

DOM解析器对于实现我们的大多数目的都很容易。但如果处理很长的文档,使用它生成树结构将会消耗大量内存,或者如果你只是对于某些元素感兴趣,而不关心它们的上下文,那么你应该考虑使用流机制解析器。

DOM解析器的接口已经被W3C标准化了。Java中org.w3c.dom包中包含了接口类型的定义,如Document和Element等。可通过以下代码来获得Document:

			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("...xml");
			Document doc = buider.parse(f);


通过GetDocumentElement方法将返回文档根元素:

Element root = doc.getDocumentElement();

getChildNodes方法将返回一个类型为NodeList的集合,包含了所有的子元素。其中item方法将得到指定索引项,getLength方法则提供项的总数,以下代码枚举所有子元素:

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				...
			}

注意:分析子元素要很仔细。如下面文档 :

		<font>
			<name>Helvetica</name>
			<size>36</size>
		</font>

你期望font有两个子元素,但解析器却报告有5个:

<font>和<name>之间的空白字符

name元素

</name>和<size>之间的空白字符

size元素

</size>和</font>之间的空白字符


如果只希望得到子元素,可以通过以下代码忽略空白字符:

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				if(child instanceof Element){
					Element childElement = (Element)child;
					...
				}
			}

如果你的文档在有DTD(下面会讲到),那么解析器会知道哪些元素没有文本节点子元素,而且它会帮你禁止空白字符。

也可以通过getFirstChild得到第一个子元素,用getNextSiblingt得到下一个兄弟节点,可用以下代码遍历子节点:

			for(Node chiNode = root.getFirstChild(); chiNode != null; chiNode = chiNode.getNextSibling()){
				...
			}


当你分析name和size元素时,想检索到它们包含的文本字符串,而这些文本字符串本身包含在Text类型的子节点中。既然知道这些Text节点是唯一子元素,可以用getFirstChild方法而不用再遍历一个NodeList,然后可以用getData方法检索存储在Text节点中的字符串。

			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				if(child instanceof Element){
					Element childElement = (Element)child;
					Text textNode = (Text)childElement.getFirstChild();
					String text = textNode.getData().trim();
					...
				}
			}
注意:getData的返回值调用trim方法是个好主意,如下XML:

			<size>
				36
			</size>
那么,解析器将会把所有的换行符和空格都包含到文本节点中去。调用trim方法可以把实际数据前后的空白字符删掉。

如果要枚举节点属性,可调用getAttributes方法,返回一个NamedNodeMap对象,其中包含描述属性的节点对象:

					NamedNodeMap attributes = element.getAttributes();
					for(int i = 0; i < attributes.getLength(); i++){
						Node attribute = attributes.item(i);
						String name = attribute.getNodeName();
						String value = attribute.getNodeValue();
						...
					}
或者,如果知道属性名,则可以直接得到相应属性值:

String unit = element.getAttribute("unit");


验证XML文档 

如果要规范文档结构,可以提供一个文档类型定义(DTD),DTD包含了用于解释文档是如何构成的规则 ,这些规则规范了每个元素的合法子元素和属性。如:

<!ELEMENT font(name,size)>

这个规则表明,一个font元素总是有两个子元素,分别是name和size。

具体DTD语法请参看Java核心技术或W3C。

对下一个XML文档 :

<?xml version="1.0"?>
<!DOCTYPE staff[
	<!ELEMENT staff (employee)*>
	<!ELEMENT employee (name,salary)>
	<!ATTLIST employee nationality CDATA "china">
	<!ELEMENT name (#PCDATA)>
	<!ELEMENT salary (#PCDATA)>
]>
<staff>
	<employee nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</employee>
	<manager nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</manager>
</staff>
使用以下代码进行解析:

import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class XMLDTDStudy {
	public static void main(String[] args) {
		try{
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setValidating(true);	//打开验证特性
			factory.setIgnoringElementContentWhitespace(true);	//设置为匆略文本节点的空白字符
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staffWithDTD.xml");
			Document doc = buider.parse(f);
			Element root = doc.getDocumentElement();
			NodeList children = root.getChildNodes();
			for(int i = 0; i < children.getLength(); i++){
				Node child = children.item(i);
				System.out.println(i + ":  " + child.getNodeName());
			}
		}catch(ParserConfigurationException e){
			e.printStackTrace();
		}catch(IOException e){
			e.printStackTrace();
		}catch(SAXException e){
			e.printStackTrace();
		}
	}
}
得到结果如下:

程序报告了manager元素并没有在DTD中声明并忽略空白字符。

使用XPath定位信息

<?xml version="1.0"?>
<staff>
    <employee nationality="china">
        <name>bin</name>
        <salary>500</salary>
    </employee>
    <employee>
        <name>zhou</name>
        <salary>800</salary>
    </employee>
</staff>

XPath可以描述XML文档中的一组节点,如/staff/employee则描述了根元素staff的子元素中所有的employee元素。可以用[]操作符选择特定元素:/staff/employee[1]表示选择第一行(索引号从1开始)

使用@操作可以得到属性值,如:"/staff/employee[1]/@nationality"得到第一个员工的国籍china

XPath有很多有用的函数,如:"count(/staff/employee)"返回根元素staff的子元素中employee元素的数量。

Java SE5.0增加了一个API计算XPath表达式,先从XPathFactory对象创建一个XPath对象。

         XPathFactory xpfactory = XPathFactory.newInstance();

            XPath path = xpfactory.newXPath();


然后,调用evaluate方法计算XPath对象表达 :

String name = path.evaluate("/staff/employee[1]/salary", doc);

如果XPath表达式产生一组节点,则如下调用:

NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);

如果结果只有一个节点,则如下调用:

Node node = (Node) path.evaluate("/staff/employee[1]", doc,XPathConstants.NODE);

如果结果是一个数字,则使用

int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue();

不必从文档的根节点开始搜索,可以从任意一个节点或节点列表开始,如果你有前一个计算得到的一个节点node,就可以调用:

result = path.evaluate(expression,node);

               DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staff.xml");
			Document doc = buider.parse(f);
			
			XPathFactory xpfactory = XPathFactory.newInstance();
			XPath path = xpfactory.newXPath();
			NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);
			for(int i = 0; i < nodes.getLength(); i++){
				System.out.println(path.evaluate("name", nodes.item(i)));	//输出每个员工的名字
			}
			
			System.out.println(path.evaluate("/staff/employee[1]/salary", doc));	//输出第一个员工国籍
			System.out.println(path.evaluate("count(/staff/employee)", doc));	//得到员工总数
			int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue();	//得到第一个员工的工资

		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
		}


使用命名空间

Java语言使用包来避免名字冲突。XML也有类似的命名空间机制 ,用于元素名和属性名。

名字空间是由统一资源标识符(URI)来标识,如:http://www.w3.org/2001/XMLSchema

下面是一个典型例子:

<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<xsd:element name="gridbag" type="GridBagType">
	...
</xsd>
下面的属性:xmlns:alias="namespaceURI"用于定义命名空间和别名,上面例子中别名为xsd。这样,xsd:schema实际上指的是“命名空间http://www.w3.org/2001/XMLSchema中的schema”

注意:只有子元素继承了它们父元素的命名空间,而不带显式别名前缀的属性不是命名空间的一部分,如:

<configuration xmlns="http://www.horstmann.com/corejava"
	xmlns:si="http://www.bipm.fr/enus/3_SI/si.html">
	<size value="210" si:unit="mm"/>
	...
</configuration>
在这个示例中,元素configuration和size是URI http://www.horstmann.com/corejava的命名空间的一部分。属性si:unit是URI http://www.bipm.fr/enus/3_SI/si.html命名空间的一部分,然而,属性值不是任何命名空间的一部分。

默认地,Sun公司的DOM解析器是关闭了命名空间处理特性的。要打开命名空间处理特性,可以调用DocumentBuilderFactory类的setNamespaceAware方法:

factory.setNamespaceAware(true);

这样工作生产的所有生成器都支持命名空间了。每个节点有三个属性:

  • 带有别名前缀的限定名,由getNodeName和getTagName等方法返回。
  • 命名空间URI,由getNamescapceURI方法返回
  • 不带别名前缀和命名空间的本地名,由getLocalName方法返回。
如解析以下元素:

<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">

会得到:

  • 限定名为xsd:shecma
  • 命名空间URI为http://www.w3.org/2001/XMLSchema
  • 本地名为shecma
注意:如果命名空间特性被关闭,getLocalName和getNamespaceURI方法将返回null。

如果XPath要解析有命名空间的XML,还需要一些工作,

首先将XML内容修改为:

<?xml version="1.0"?>
<xsd:staff xmlns:xsd="http://www.test">
	<xsd:employee nationality="china">
		<name>bin</name>
		<salary>500</salary>
	</xsd:employee>
</xsd:staff>

再实现一个NamespaceContext接口,它做的工作是将文档中提取命名空间:

import java.util.Iterator;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import org.w3c.dom.Document;

public class UniversalNamespaceResolver implements NamespaceContext {
    private Document sourceDocument;

    public UniversalNamespaceResolver(Document document) {
        sourceDocument = document;
    }

    public String getNamespaceURI(String prefix) {
        if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
            return sourceDocument.lookupNamespaceURI(null);
        } else {
            return sourceDocument.lookupNamespaceURI(prefix);
        }
    }

    public String getPrefix(String namespaceURI) {
        return sourceDocument.lookupPrefix(namespaceURI);
    }

    public Iterator getPrefixes(String namespaceURI) {
        // not implemented yet
        return null;
    }
}

测试代码如下:
import java.io.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class DomStudy {

	public static void main(String[] args) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setNamespaceAware(true);
		try {
			DocumentBuilder buider = factory.newDocumentBuilder();
			File f = new File("staff.xml");
			Document doc = buider.parse(f);
			
			XPathFactory xpfactory = XPathFactory.newInstance();
			XPath path = xpfactory.newXPath();
			path.setNamespaceContext(new UniversalNamespaceResolver(doc));	//设置XPath的命名空间
			
			NodeList nodes = (NodeList)path.evaluate("/xsd:staff/xsd:employee", doc,XPathConstants.NODESET);
			for(int i = 0; i < nodes.getLength(); i++){
				Node node = nodes.item(i);
				System.out.println(node.getNamespaceURI());
				System.out.println(node.getLocalName());
				System.out.println(node.getNodeName());
			}
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
		}
	}
}
得到结果:

http://www.test
employee
xsd:employee


流机制解析器

当XML文档很大时,并且处理算法非常简单,可能在运行时解析节点,而不必看到所有的树形结构时,使用DOM可能显得效率低下,这时,应使用流机制解析器。

SAX解析器

SAX解析器在解析XML输入的构件时就报告事件,但不会以任何方式存储文档,而由事件处理器处理数据。实际上,DOM解析器是在SAX解析器的基础上建立起来的,它在接收到解析器事件时建立DOM树。

在使用SAX解析器,需要一个处理器来定义不同的解析器事件的事件动作,ContentHandler接口定义了若干个回调方法,下面是最重要的几个:

  • startElement和endElement在每当遇到起始事终止标签时调用
  • characters每当遇到字符数据时调用
  • startDocument和endDocument分别在文档开始和结束各调用一次。
如解析如下片断时:

<font>
	<size units="ps">36</size>
</font>
解析器确保产生以下调用:

1.startElement ,元素名:font

2.startElement, 元素名:size , 属性:units="pt"

3.characters, 内容:36

4.endElement, 元素名:size

5.endDocument, 元素名:font

处理器必须覆盖这些方法,让它们执行在解析文件时想要执行的动作。

注意:与DOM解析器一样,命名空间处理特性默认关闭。

如果使用下面代码处理上面带有命名空间的staff.xml

import java.io.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class SAXStudy {
	public static void main(String[] args) {
		DefaultHandler handler = new DefaultHandler(){	//定义一个DefaultHandler,并覆盖startElement方法,输出相关信息
			public void startElement(String uri,  String localName,
					String qName,Attributes attributes)throws SAXException{
				System.out.println("URI:" + uri + "  LocalName:" + localName + "   qName:" + qName );
			}
		};
		
		SAXParserFactory factory = SAXParserFactory.newInstance();
		factory.setNamespaceAware(true);
		try{
			SAXParser saxParser = factory.newSAXParser();
			InputStream in = new FileInputStream("staff.xml");
			saxParser.parse(in, handler);
			in.close();
		}
		catch(ParserConfigurationException e){
			e.printStackTrace();
		}
		catch(SAXException e){
			e.printStackTrace();
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(IOException e){
			e.printStackTrace();
		}
	}
}

使用StAX解析器

StAX解析器是一种“拉解析器(pull parser)”,与安装事件处理器不同,只需要使用下面这样的基本循环来迭代所有的事件:

			InputStream in = new FileInputStream("staff.xml");
			XMLInputFactory factory  = XMLInputFactory.newFactory();
			XMLStreamReader parser = factory.createXMLStreamReader(in);
			while(parser.hasNext()){
				int event = parser.next();
				call parser methods to obtain event details
			}
如解析以下片断:

<font>
	<size units="ps">36</size>
</font>

解析器将产生下面的事件:

1.START_ELEMENT, 元素名:font

2.CHARACTERS, 内容:空白字符

3.START_ELEMENT, 元素名:size

4.CHARACTERS, 内容:36

5.END_ELEMENT, 元素名:size

6.CHARACTERS, 内容:空白字符

7.END_ELEMENT, 元素名:font

下面是一个实现:

import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;

public class StAXTest {

	public static void main(String[] args) {
		try{	
			InputStream in = new FileInputStream("staff.xml");
			XMLInputFactory factory  = XMLInputFactory.newFactory();
			XMLStreamReader parser = factory.createXMLStreamReader(in);
			while(parser.hasNext()){
				int event = parser.next();
				if(event == XMLStreamConstants.START_ELEMENT){
					QName qname = parser.getName();
					System.out.println(qname.toString());
				}
			}
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(XMLStreamException e){
			e.printStackTrace();
		}
	}
}


生成XML文档

通过调用DocumentBuilder类的newDocument方法得到一个空文档:

Document doc = builder.newDocument();

使用Document类的createElement方法可以构建文档里的元素:

Element rootElement = doc.createElement(rootName);

Element childElement = doc.createElement(childName);

使用createTextNode方法构建文本节点:

Text textNode = doc.createTextNode(textContents);

使用以下方法给文档加上根元素,给父结节加上子节点:

doc.appendChild(rootElement);

rootElement.appendChild(childElement);

childElement.appendChild(textNode);

调用Element类的setAttribute方法设置元素属性:

rootElement.setAttrbute(name,value);

将doc输出到文件中,可以使用Transformer类,通过transform输出doc树

import java.io.File;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;

public class WriteXML {
	public static void main(String[] args) {
		try{
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			Document doc = builder.newDocument();
			
			Element staff = doc.createElementNS("http://www.test", "xsd:staff");	//创建根结点
			Element employee = doc.createElement("xsd:employee");	//创建employee结点
			
			Element nameElem = doc.createElement("name");	//创建name结点
			Text nameText = doc.createTextNode("bin");	//创建文本结点
			Element salaryElem = doc.createElement("salary");
			Text salaryText = doc.createTextNode("500");
			
			//将结果组织到doc树中
			doc.appendChild(staff);
			staff.appendChild(employee);
			employee.appendChild(nameElem);
			nameElem.appendChild(nameText);
			employee.appendChild(salaryElem);
			salaryElem.appendChild(salaryText);
			
			//将doc树输出到文件中
			Transformer t = TransformerFactory.newInstance().newTransformer();
			//设置输出格式
			t.setOutputProperty(OutputKeys.METHOD,"xml");
			t.setOutputProperty(OutputKeys.INDENT, "yes");

			File f = new File("writerStaff.xml");
			t.transform(new DOMSource(doc), new StreamResult(f));
		}
		catch(ParserConfigurationException e){
			e.printStackTrace();
		}
		catch(TransformerConfigurationException e){
			e.printStackTrace();
		}
		catch(TransformerException e){
			e.printStackTrace();
		}
	}
}

使用StAXXML文档 

StAX API使我们可以直接将XML树写出,先构建一个XMLStreamWriter:

XMLOutputFactory factory = XMLOutputFactory.newInstance();

XMLStreamWriter writer = factory.createXMLStreamWriter(out);

要产生XML文件头,调用:

writer.writeStartDocument();

然后要产生元素则调用 :

writer.writeStartElement(name); 

添加属性需要调用:

writer.writeAttribute(name,value);

写出字符则调用:

writer.writeCharacters(text);

要写出没有子节点的元素可调用:

writer.writeEmptyElement); 

在添加完所有子节点后,调用:

writer.writeEndElement(); 

这会导致当前元素被关闭

最后,在文档的结尾,调用

writer.writeEndDocument();

调用将关闭所有的元素。

注意:与使用DOM/XSLT方式一样,不必担心属性值和字符数据的转义字符。并且,StAX当前的版本还没有任何对产生缩进输出的支持。

下面是一个实例:

import java.io.*;
import javax.xml.stream.*;

public class StAXWriterXML {
	public static void main(String[] args) {
		try{	
			FileOutputStream out = new FileOutputStream("StAXWriterStaff.xml");
			XMLOutputFactory factory = XMLOutputFactory.newInstance();
			XMLStreamWriter writer = factory.createXMLStreamWriter(out);
			
			writer.writeStartDocument();
			writer.writeStartElement("staff");
			writer.writeStartElement("employee");
			writer.writeStartElement("name");
			writer.writeCharacters("bin");
			writer.writeEndElement();
			writer.writeEndElement();
			writer.writeEndDocument();

			writer.close();
			out.close();	
		}
		catch (XMLStreamException e) {
			e.printStackTrace();
		}
		catch(FileNotFoundException e){
			e.printStackTrace();
		}
		catch(IOException e){
			e.printStackTrace();
		}
	}
}

XSL转换

XSL转换 机制可以指定将XML文档转换为其他格式的规则,例如,纯文本,XHTML或其他任何XML格式。XSLT通常用于将一个机器可读的XML格式转译为另一种机器可读的XML格式,或者将XML转译为适于人类阅读的表示格式。

具体方法请参看Java核心技术。

转载于:https://www.cnblogs.com/bin1991/p/3636629.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值