使用DOM4J解析大容量XML文件

dom4j本身提供了两种解析xml的方式:dom解析和sax解析。关于dom解析和sax解析各自的优缺点这里不再多述,只强调的一点是由于越来越多的应用会遇到大数据场景,SAX解析方式刚好是解决此类场景的完美方案,因此“DOM4J解析大数据的方案”就是"如何利用SAX方式解析大数据的方案"(当然JAXP中的sax解析也是同样的方案),本文梳理总结下实际工作中使用DOM4J解析大容量XML文件的实现。

XML文件结构以滴答团购数据为例:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. <urlset>  
  2. <url>  
  3. <loc>http://beijing.didatuan.com/team.php?id=130515</loc>  
  4. <data>  
  5.     <display>  
  6.         <website>嘀嗒团</website>  
  7.         <siteurl>http://beijing.didatuan.com</siteurl>  
  8.         <city>北京</city>  
  9.         <title>【王府井】仅26.5元,享市场价100元『北京横店影视电影城』单人电影票一张!2D、3D通兑!</title>  
  10.         <category>2</category>  
  11.         <subcategory>电影</subcategory>  
  12.         <dpshopid/>  
  13.         <range>王府井/东单</range>  
  14.         <major>1</major>  
  15.         <address>北京市东城区王府井大街253号王府井百货北馆8楼</address>  
  16.         <image>  
  17.         http://i2.didaimg.com/pic/team/2013/0809/mobilew/13760173024175.jpg  
  18.         </image>  
  19.         <startTime>1376236800</startTime>  
  20.         <name>横店影视电影城单人票</name>  
  21.         <seller>北京横店影视电影城</seller>  
  22.         <phone/>  
  23.         <endTime>1388419200</endTime>  
  24.         <value>100.00</value>  
  25.         <price>26.50</price>  
  26.         <rebate>2.65</rebate>  
  27.         <bought>46023</bought>  
  28.     </display>  
  29.     <shops>  
  30.         <shop>  
  31.             <seller>北京横店影视电影城</seller>  
  32.             <phone>010-65231588</phone>  
  33.             <addr>北京市东城区王府井大街253号王府井百货北馆8楼</addr>  
  34.             <longitude>116.41093969345093</longitude>  
  35.             <latitude>39.91338185899138</latitude>  
  36.             <trafficInfo>乘坐1号线王府井站C口出,沿王府井步行街步行约5分钟</trafficInfo>  
  37.             <range>王府井/东单</range>  
  38.         </shop>  
  39.     </shops>  
  40. </data>  
  41. </url>  
  42. <!--..大量url节点..-->  
  43. </urlset>  

dom4j读取、解析xml文件的方法:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. SAXReader reader = new SAXReader();  
  2. Document document = reader.read(new File("..."));  

如果只是这么简单的去读取解析大文件有很大的可能会遇到内存溢出的异常,不要说1G的文件了,就算是100M都有可能会异常。这种方式还会引出另一个疑问:SAXReader这样read文件究竟是用的SAX解析方式还是DOM解析方式?如果没有去查阅其API文档或者分析源码的话,仅从其使用方式看好像是dom解析:毕竟读出来一个完整的Document对象然后在其他地方再继续处理这个文档对象。事实果真如此么?要是不嫌麻烦就去看看源码吧,嫌麻烦就看看API吧。

事实上,就像其类名已经明确指出的一样,SAXReader的内部解析方式就是sax方式。并且提供了很灵活的句柄接口以便在sax解析过程中不同时刻触发不同事件时调用句柄接口,这也是与jdom相比,dom4j更多的被称赞为面向接口方案的优秀特征。重点分析下dom4j定义的事件句柄接口ElementHandler:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.  org.dom4j  
  2. Interface ElementHandler  
  3.   
  4. public interface ElementHandler  
  5.   
  6. ElementHandler interface defines a handler of Element objects. It is used primarily in event based processing models such as for processing large XML documents as they are being parsed rather than waiting until the whole document is parsed.  
  7.   
  8. Version:  
  9.     $Revision: 1.8 $  
  10. Author:  
  11.     James Strachan   
  12.   
  13. Method Summary  
  14.  void   onEnd(ElementPath elementPath)  
  15.           Called by an event based processor when an elements closing tag is encountered.  
  16.  void   onStart(ElementPath elementPath)  
  17.           Called by an event based processor when an elements openning tag is encountered.  
与jaxp相关事件处理器的定义相比,即使是方法的名字也更显得"纯正化"。ElementHandler事件处理器在sax解析过程中至关重要,熟悉其原理和规则就能有技巧的实现实际需求。它仅仅定义了两个方法:onEnd和onStart,其被调用时机文档说得很清楚了,在时机被触发时分别干什么事就是该接口的具体实现要考虑的。

需要特别说明的是,SAX解析之所以能利用很小的、有限的内存空间去解析大容量的数据,秘密也在于这两个方法,即该处理器被绑定的文档节点在被流式读取完毕时需要释放掉其已占用的内存空间,无论具体的业务逻辑实现是怎样的,这一步是必须要实现的公共逻辑,否则SAXReader解析xml时会将该节点纳入其最终返回的文档模型对象document中,占用内存会与DOM方式解析一样越来越大。XML节点释放资源的方法是

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. Node    detach()  
  2.            Removes this node from its parent if there is one.   
该方法由dom4j的dom模型树中的最顶级接口Node定义,因此所有节点都可以调用。

这里需要给出一个样例:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class MainDataElementHandler implements ElementHandler {  
  2.       
  3.     private ElementParser<Bean> parser;  
  4.       
  5.     private Writer writer;  
  6.   
  7.     public MainDataElementHandler(ElementParser<Bean> parser, BufferedWriter writer) {  
  8.         super();  
  9.         this.parser = parser;  
  10.         this.writer = writer;  
  11.     }  
  12.   
  13.     @Override  
  14.     public void onEnd(ElementPath path) {  
  15.         Element urlNode = path.getCurrent();  
  16.         Bean outputBean = null;  
  17.           
  18.         try {  
  19.             outputBean = parser.parseElement(urlNode);  
  20.         } catch (Exception e) {  
  21.             e.printStackTrace();  
  22.         } finally {  
  23.             urlNode.detach();  
  24.         }  
  25.           
  26.         if (outputBean != null) {  
  27.             writer.write(outputBean);  
  28.         }  
  29.     }  
  30.   
  31.     @Override  
  32.     public void onStart(ElementPath path) {  
  33.         Element urlNode = path.getCurrent();  
  34.         urlNode.detach();  
  35.     }  
  36.   
  37. }  
个人认为这个样例虽然极为简单,但是已经解决了应该解决的大部分问题:

一 解决节点释放资源的问题

参考网上其他有价值的文章,有观点认为仅仅在onEnd方法中调用节点的detach()方法并不能真正释放资源,仍然会导致内存爆掉的现象,必须在onStart方法中调用该节点的detach()方法才有效,本人没有试过,为保险起见,在两个方法里都调用一次。这里可能会导致的一个疑问是如果在节点的start读取时刻就detach该节点,那么在onEnd时该节点还存在么?实际动手实验下,就会发现onEnd方法中Element urlNode = path.getCurrent();得到的urlNode节点对象是健全完整的,可以对其进行增删改查操作。这里的另一个疑问是如果节点真的只有在onStart方法中detach才能有效释放资源,那么为什么只在onEnd方法中detach就无效呢?毕竟按照正常逻辑思维在end后才释放资源是更自然的,由于没有跟踪查看源代码(鄙人也很懒),这里只有一个猜测:SAXReader在流式解析数据时,遇到某路径上绑定的处理器并触发之后,在onStart方法调用之后、onEnd方法调用之前,会改变最终返回的文档模型对象document的构造机制,这个操作肯定不会是当前节点调用detach方法时完成的(否则onEnd中调用也应有效),节点调用detach方法更大的可能实现是给该节点一个标识,标识不需要纳入最终的文档模型对象document。

二 解决对当前节点的解析问题

如果把"对当前节点的解析"放在onEnd方法本身中当然也是可行的,但是更好的设计思想是由专有的模块去实现,因为xml中不同的节点具有不同数据结构,为了更灵活的多样化的应对具体节点的解析,当然要"面向接口"编程了,这里定义了一个接口来做这个事情:ElementParser<T>

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /**  
  2.  * 专一职责:解析xml某种类型的节点,返回泛型化的数据类型T  
  3.  * @author warhin  
  4.  *  
  5.  * @param <T>  
  6.  */  
  7. public interface ElementParser<T> {  
  8.       
  9.     T parseElement(Element e);  
  10.   
  11. }  

这个接口与ElementHandler接口的关系有些类似静态代理模式,姑且称之为"泛静态代理模式"吧。

事实上这个接口的定义不应该由用户来完成,应该由dom4j本身实现,只是不知道dom4j最新的版本有没有纳入类似的接口。

三 解决业务逻辑的问题

最后,writer.write(outputBean);这行代码可以抽象的认为是业务逻辑的具体实现,不管是要将解析出来的pojo写入文件,还是存入数据库,还是输出到网络中等等。
目前看起来好像所有的问题都解决了,事实上,稍加思考,会发现流程上还有一个很重要的问题没有提及:ElementHandler如何被SAXReader绑定到具体的节点上?
dom4j给出了两种方案:

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. void    addHandler(String path, ElementHandler handler)  
  2.           Adds the ElementHandler to be called when the specified path is encounted.  
  3. void    setDefaultHandler(ElementHandler handler)  
  4.           When multiple ElementHandler instances have been registered, this will set a default ElementHandler to be called for any path which does NOT have a handler registered.  
具体绑定方法动手实践下就知道了,这里也有几个很重要的问题需要思考:如果多个ElementHandler绑定同一个节点时,在解析时会全部触发么?或者一个节点能绑定多个ElementHandler么?经过测试,发现结论如下:dom4j对事件触发器是全局唯一的,这点儿有别于ECMA对于dom事件模型的规范定义,也就是说dom4j中不会有事件冒泡传播的特性。当然在SAXReader解析数据过程中也可以不设置事件触发器,就像文章最开始的解析那样,这下想必大家应该能猜测到那样调用dom4j的默认行为了吧。对于xpath上未绑定ElementHandler的节点可以用第二个方法设置全局默认触发器做一些公共事情,比如释放资源。

花了几个小时归纳整理这篇文章,只是因为在网上没有找到几篇有价值的文章,不管是用百度还是谷歌,能找到的也只是简单的对节点的增删改查操作。或许大家都像我一样浮躁,但是偶尔,我们也可以静下来。这里不保证本文全部观点的正确性,转载请注明。
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
&1.XML简介 XML的背景 1) XML代表可扩展的标记语言(eXtensible Markup Language); 2) XML由W3C联盟发展维护; 3) XML是一种元语言,可以用来定义其它标签语言; 4) XML没有定义任何标记,它提供了一种工具定义标记以及它们之间的结构关系; 5) XML是一种用于结构化文本交换的标记语言; 6) XML代表了内容的结构也代表了内容本身; 7) XML继承自SGML(标准通用标记语言)。SGML的前身GML由IBM在1960年发明,用于描述设备无关的文本 8) XML是SGML的子语言,由SGML简化而来,起初的目的是为Web的结构文档提供服务。 9) W3C组织在1998年2月10日发布XML1.0版,第二个版本发布于2000年10月9日。 10)XML是分层的树形结构的自解释的一种可扩展的标记语言。 XML和HTML的关系 1) 事实上,HTML和XML不能够进行比较的。因为XML是一种元语言,而HTML是一种定义好的标记语言。XML是描述数据的语言,HTML是描述数据 外观的语言。 2) HTML有一套确定的标记。在W3C标准规范中定义了HTML标记的含义并由部分浏览器实现了。 3) HTML标记非常适合描述人类易读的可视化文本。 4) HTML定义了许多表现指令用以优化在浏览器中的显示。 5) 在HTML标记中,数据固有的结构丢失了。 6) HTML没有维持数据类型信息的完整性、约束、或实体间的其它关系,而XML提供了这些特征。 对XML文件的要求,格式良好(符合XML的语法要求),有效的XMLXML的用途,数据传输的中间件,做为通用文档格式 1,数据交换 2,web服务 3,内容管理 4,web集成 5,充当配置文件 格式良好的XML文件的规范 XML文档是一种严格规范的语言,必须按照规范来写。 XML文档的框架结构 XML文档=序言+元素+杂项(可选) 1)序言 序言=(XML声明)+(杂项)+(文档类型声明) XML声明 如: 文档类型声明 规定元素的结构和属性列表的取值 如: 2)元素 空元素 非空元素 内容 内容=(子元素|字符数据|字符数据段|引用|处理指令|注释)* 字符数据 字符数据中,不能含有&,,',",需要采用实体引用的方式 字符数据段 引用 3)杂项 处理指令 XML为其它应用程序准备的接口。 注释 空白符指空格、回车等 XML文件事例 ... XML文件的写法 xml文档是有标签和标签中的内容组成的,标签和标签中的内容合成为元素 非空元素 (起始标签) aaa(内容) (结束标签) 空元素 格式良好的XML文档的规范 1)文档声明位于文件的开头 2)文档中只能定义一个唯一的根元素(根元素是在文档声明之后的最外层的元素) 3)标签必须闭合,且不能交叉,有元素的起始标签,就必须有其结束标签(空元素除外) 4)层层嵌套,每一对标签总是出于另一对标签的内或和其处于同一层,不能交叉(根元素chuw) 5)大小写敏感,起始标签和结束标签的名字要一致 6)属性值必须用引号引起来 7)注意特殊字符,使用需要使用转义字符。 注意: 1.第一行一定要写 2.xml文件是由元素和元素的内容以及属性组成的,一个xml文件中只能有一个根元素,标签和标签的内容加在一起叫作一个元素。xml是大小写 敏感的,只能以字母或下划线开头。 字符转义 < 代表 "" , " 代表双引号。 &amp 代表 "&" , &apos; 代表单引号 字符数据段,不解析 ....的部分不必使用转义,其中内容会直接输出。 引用 实体声明 使用实体,引用 &引用名 事例: (XML文件的声明) (注释) (根元素) (子元素) b) then {return 1} else{return 0} }]]> (空元素) 张三 XSLT,是用于对XML进行文档格式转换,把一个XML文档转换成另一种格式的XML文档,但是其中内容是不变的。 &2.DTD(Document Type Difinition DTD文档是用于规定XML文档的结构。只有结构符合所引用的DTD文件的XML文件才能称之为有效的XML文件。 DTD(文档类型定义) !DOCTYPE 定义DTD文件名 !ELEMENT 定义元素中可用的数据类型 #PCDATA 可解析字符串。 1)DTD的调用 方式一:调用内部文档类型定义。 <!DOCTYPE studinfo[]> 注意点: (1) studinfo与(#PCDATA)有空格 (2) 文档类型名与根元素名必须一致 方式二:调用外部文档类型定义,需要写一个DTD文档 注意点: (1)standalone="no" (2)注意dtd文件的路径 2)DTD的结构 1.元素类型声明 指明元素的名称和元素含有的内容。 元素类型声明= 元素内容说明='EMPTY'|'ANY'|混合内容|元素内容|'#PCDATA' #PCDATA: 只有可析的字符数据才能作为元素的内容 元素内容: 元素内部只能出现指定的子元素 事例:带有子元素的元素结构定义 "?" 表示子元素只可以出现一个,也可以不出现 (0到1) "+" 表示子元素必须出现,可以出现多个 (1到多) "*" 表示子元素可以出现多个,也可以不出现,(0到多) Enumerated:枚举类型,由“|”分隔的可能的子元素,在可能出现的子元素中只能有一个出现。 EMPTY:元素内容为空才写(空元素,其中不能有内容) (无子元素) ANY:元素内容可以任意,也可以为空 混合内容:标记文本和可析字符串 2.元素属性表声明 属性:由“=”分隔的成对的属性名和属性值构成,只能出现在元素标记的内部。 结束标记不能带属性,一个元素可以具有多个属性 语法: 属性取值类型 1、CDATA:可析字符(文本) 2、Enumerated:枚举类型,由“|”分隔的可能的属性值列表 属性默认值: 注意 1、#REQUIRED:必须提供属性值 2、IMPLIED:可提供也可不提供 3、FIXED:不能修改 事例: zhangshan 实体声明 实体:存储了任意符合规则的xml文档单元片断。 1、内部通用实体 定义格式 引用格式 &实体名; 2、外部通用实体 定义格式 引用格式 &实体名; 事例: <!DOCTYPE studinfo[ ]> zhangshan 20 beijing haidian &schoolinfo; 记号声明 用记号标识非xml格式的数据 定义格式 3)DTD的缺陷,DTD中的类型比较少,有一些约束在DTD中是表达不了的,DTD不支持XML语法. &3.NameSpace(命名空间) namespace是为了解决XML文档中的命名冲突问题的。 DTD不支持命名空间,解析器无法测试命名空间。 1、作用: 解决XML文档中命名冲突的问题,即将 XML文档与引用URI标识的名域相结合,来 限定其中的元素和属性名。 示例: t1 man s1 girl 2、名域的声明 (1)直接定义 xmlns:名域前缀="名域的URI"(唯一性) 类比:名字与身份证 名域的使用,名域前缀:需要限定的元素名 示例: zhangshan man stu1 girl (2)缺省定义 xmlns=名域的URI zhangshan man stu1 girl 3、名域的使用 (1)用名域限定元素 zhangshan man stu1 girl (2)用名域限定属性 zhangshan man caoyang,no1 stu1 girl haidian,no2 4、名域的作用范围 名域能够把声明它的元素和该元素的所有子元素关联起来,除非它们被其他的名域声明所覆盖。 &XML Schema Schema 也是XML文档,用于定义XML文档的结构。 目标名域 使用目标名域中的限制,要只用这个schema文件结构生成XML文档中必须使用者个默认名域。 schema文件的扩展名为 .xsd schema文档的语法和XML的语法相同。 schema文档也有根元素,根元素为schema。 参照类型约束,来约束元素的内容 元素的默认和固定值 1) 简单元素可以有一个默认或固定值: 2) 当没有指定值,一个默认的值自动分配给元素。在这个例子中,默 认值为red: 3) 一个固定值也是自动分配给元素的,你不能指定 其它值。在这个例子中,默认值为red: ref关键字 参照....定义。 属性结构的定义,属性也可以使用固定值或者是默认值 schema文件中也可以定义元素结构和类型,schema文件中对元素内容加以限制 schema中可以定义元素内容类型 1. 简单类型 2. 自定义类型,复杂类型,子元素组合 简单类型,基本类型加限制 simpleType:简单类型定义 restriction:基本类型引用 enumeration:使用枚举类型,从给定值中选择且必选其一。 minInclusive:简单类型加最小值约束,且包含设定的最小值 maxExclusive:简单类型加最大值约束,且包含设定的最大值 以下是其使用的事例 . 简单类型加限制 只能使用非负整数3到7包含3和7。 . 复杂类型定义 复杂类型定义事例: 整体schema文档事例 演示simpleType用法,即自定义类型 学生(id = "1000") 姓名 年龄(必须是int,并且只能取一定范围的值) 父亲(父母二者选一) 母亲 专业(枚举类型) 知识点: simpleType restriction enumeration minInclusive maxExclusive 针对以上schema文件的有效的XML文件事例 shang zhang 30 lishi computer XML文档的解析 SAX :事件机制驱动。在遇到相应的节点,会发出一个事件,当解析器发现元素开始、元素结束、文本、文档的开始或结束等,发送事件,程序员编写响应这些事件的代码,保存数据。 SAX的优缺点:   优点: 不用事先调入整个文档,占用资源少; SAX解析器代码比DOM解析器代码小,适于Applet,下载   缺点: 不是持久的;事件过后,若没保存数据,那么数据就丢了,比较消耗间; 只能顺序处理,不能选择读取。不支持文档的创建和修改。 无状态性;从事件中只能得到文本,但不知该文本属于哪个元素;   使用场合:Applet;只需XML文档的少量内容,很少回头访问;机器内存少 SAX处理流程 UNIX color 分析这个代码片断的 SAX 处理器一般情况下将产生以下事件: startDocument() startElement (samples) characters (white space) startElement (server) characters (UNIX) endElement (server) characters (white space) startElement (monitor) characters (color) endElement (monitor) characters (white space) endElement (samples) SAX API 允许开发人员捕捉这些事件并对它们作出反应。 SAX 处理涉及以下步骤: 创建一个事件处理程序。 创建 SAX 解析器。 向解析器分配事件处理程序。 解析文档,同向事件处理程序发送每个事件。 要实现SAX解析XML文档的方法, XMLReader和XMLReaderFactory XMLReader read=XMLReaderFactory.createXMLReader();生成XMLReader对象, 使用XMLReader对象的方法parse(File f,ContentHandler handler),ContentHandler是一个定义好事件处理方法的接口, DTDHandler ,EntityResolver ,ErrorHandler这三个接口和 ContentHandler接口相同,但是其中定义的是其他事件处理方法,一般只是实现ContentHandler接口。 使用SAX解析XML文档,需要先有SAXParserFactory对象,他的对象是通过自身的静态方法newInstance(),然后再通过SAXParserFactory对象来创建SAXParser对象,使用SAXParser对象的 parse(File f,DefaultHandler handler),DefaultHandler是用来处理解析发出的事件的类,就可以进行解析。DefaultHandler实现了ContentHandler,DTDHandler ,EntityResolver ,ErrorHandler,四个接口,但是只给出了空实现,便于使用者覆盖相应的方法。 SAX解析的其他方法请参阅JAVA API文档。(javax.xml.parsers ,org.xml.sax ,org.xml.sax.helpers) DOM (文档对象模型)Document Object Modle 为 XML 文档的已解析版本定义了一组接口。解析器读入整个文档,然后构建一个驻留内存的树结构,然后代码就可以使用 DOM 接口来操作这个树结构。   优点:整个文档树在内存中,便于操作;支持删除、修改、重新排列等多种功能;   缺点:将整个文档调入内存(包括无用的节点),浪费间和空间;   使用场合:一旦解析了文档还需多次访问这些数据;硬件资源充足(内存、CPU) DOM解析是将整个的XML文档元素结构读入内存,由根元素向下形成子元素分级树状关系。 DOM的解析 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(name); 以上三行代码,使用来生成Document对象的,Document对象就代表了读入内存后结构化的XML文档。 使用Document对象的getDocumentElement()方法获得子节点,可以通过不断的迭代便利所有节点,这些节点就代表了元素,从根元素开始一直到最内层的子元素。 getElementsByTagName(String tagname) 这个方法可以通过元素的名来或得该元素内层元素,也就是或得了这个节点的所有子节点(NodeList)NodeList的方法getLength()(列表中的节点数),通过循环使用 Node item(int index) (返回集合中的第 index 个项)方法,来获得每个子节点。 DOM解析的其他方法请参阅JAVA API文档。(org.w3c.dom) 选择 DOM 还是选择 SAX,这取决于下面几个因素: 应用程序的目的: 数据容量: 数据多少部分会被使用 对速度的需要: DOM4j,JDOM 开源的XML文档解析器,实现DOM,SAX接口,保留了DOM和SAX的基本语法,对解析性能进行了优化。 JDOM 需要jdom.jar和 JDOM的背后使用的是使用SAX对文件进行扫描的。 SAXBuilder builder=new SAXBuilder();//创建解析使用SAX扫描之后的结果保存成DOM结构的树。JDOM对解析过程作了简化。 JDOM中的方法便的更加易用。例如:getRootElement(),getChildren("...")方法的返回值是java.util.List类型,getText()方法直接可以返回元素中的内容,他的返回值类型是String类型。 JDOM也可以对XML文档进行写操作。 XMLOutputter类的对象可以向文件中写信息,setEncoding("")设置内码,setNewLine(true),设置换行。output(Document docFileWriter write)方法可以向文件中写入。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值