在java中将XML文件转换为CSV
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3293371/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Convert XML file to CSV in java
提问by ant
@Before There will be probably some duplicate questions suggestions, I don't think that is the case maybe read this first, I'll try to be as brief as possible. Title gives basic idea.
@Before 可能会有一些重复的问题建议,我不认为是这种情况,可以先阅读本文,我会尽量简短。标题给出了基本的想法。
Here is an example XML(case 1) :
这是一个示例 XML(案例 1):
<root>
<Item>
<ItemID>4504216603</ItemID>
<ListingDetails>
<StartTime>10:00:10.000Z</StartTime>
<EndTime>10:00:30.000Z</EndTime>
<ViewItemURL>http://url</ViewItemURL>
....
</item>
Here is an example XML(case 2) :
这是一个示例 XML(案例 2):
<Item>
<ItemID>4504216604</ItemID>
<ListingDetails>
<StartTime>10:30:10.000Z</StartTime>
<!-- Start difference from case 1 -->
<averages>
<AverageTime>value1</AverageTime>
<category type="TX">9823</category>
<category type="TY">9112</category>
<AveragePrice>value2</AveragePrice>
</averages>
<!-- End difference from case 1 -->
<EndTime>11:00:10.000Z</EndTime>
<ViewItemURL>http://url</ViewItemURL>
....
</item>
</root>
I borrowed this XML from google, anyways my objects are not always the same, sometimes there are extra elements like in case2. Now I'd like to produce CSV like this from both cases:
我从谷歌借用了这个 XML,反正我的对象并不总是相同的,有时会有额外的元素,比如 case2。现在我想从两种情况下生成这样的 CSV:
ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice
4504216603,10:00:10.000Z,10:00:30.000Z,http://url
4504216604,10:30:10.000Z,11:00:10.000Z,http://url,value1,value2
This 1st line is header it should also be included in csv. I got some useful links to stax today, I don't really don't know what is the right/optimal approach for this, I'm struggling with this for 3 days now, not really willing to give up yet.
第一行是标题,它也应该包含在 csv 中。我今天得到了一些有用的 stax 链接,我真的不知道什么是正确/最佳方法,我现在为此挣扎了 3 天,还不太愿意放弃。
Tell me what you think how would you solve this
告诉我你的想法你会如何解决这个问题
I forgot to mention this is very huge xml file up to 1gb
我忘了提到这是一个非常大的 xml 文件,最大 1gb
BOUNTY UPDATE :
赏金更新:
I'm looking for more Generic approach, meaning that this should work for any number of nodes with any depth, and sometimes as in the example xml, it can happen that one item
object has greater number of nodes than the next/previous one so there should be also case for that(so all columns and values match in CSV).
我正在寻找更通用的方法,这意味着这应该适用于任何深度的任意数量的节点,有时就像在示例 xml 中一样,可能会发生一个item
对象比下一个/上一个对象具有更多节点的情况,所以有也应该是这种情况(因此所有列和值都在 CSV 中匹配)。
Also it can happen that nodes have the same name/localName but different values and attributes, if that is the case then new column should appear in CSV with appropriate value. (I added example of this case inside <averages>
tag called category
)
此外,节点可能具有相同的名称/本地名称但具有不同的值和属性,如果是这种情况,则新列应以适当的值出现在 CSV 中。(我在<averages>
名为 的标签中添加了这种情况的示例category
)
采纳答案by Mark McLaren
The code provided should be considered a sketch rather than the definitive article. I am not an expert on SAX and the implementation could be improved for better performance, simpler code etc. That said SAX should be able to cope with streaming large XML files.
提供的代码应被视为草图,而不是最终的文章。我不是 SAX 方面的专家,可以改进实现以获得更好的性能、更简单的代码等。也就是说,SAX 应该能够处理流大型 XML 文件。
I would approach this problem with 2 passes using the SAX parser. (Incidentally, I would also use a CSV generating library to create the output as this would deal with all the fiddly character escaping that CSV involves but I haven't implemented this in my sketch).
我会使用 SAX 解析器通过 2 次来解决这个问题。(顺便说一句,我还会使用 CSV 生成库来创建输出,因为这将处理 CSV 涉及的所有繁琐的字符转义,但我还没有在我的草图中实现这一点)。
First pass:Establish number of header columns
第一遍:建立标题列的数量
Second pass:Output CSV
第二遍:输出CSV
I assume that the XML file is well formed. I assume that we don't have a scheme/DTD with a predefined order.
我假设 XML 文件格式正确。我假设我们没有带有预定义顺序的方案/DTD。
In the first pass I have assumed that a CSV column will be added for every XML element containing text content or for any attribute (I have assumed attributes will contain something!).
在第一遍中,我假设将为包含文本内容或任何属性的每个 XML 元素添加一个 CSV 列(我假设属性将包含某些内容!)。
The second pass, having established the number of target columns, will do the actual CSV output.
第二遍,确定了目标列的数量,将执行实际的 CSV 输出。
Based on your example XML my code sketch would produce:
根据您的示例 XML,我的代码草图将生成:
ItemID,StartTime,EndTime,ViewItemURL,AverageTime,category,category,type,type,AveragePrice
4504216603,10:00:10.000Z,10:00:30.000Z,http://url,,,,,,
4504216604,10:30:10.000Z,11:00:10.000Z,http://url,value1,9823,9112,TX,TY,value2
Please note I have used the google collections LinkedHashMultimap as this is helpful when associating multiple values with a single key. I hope you find this useful!
请注意,我使用了 google 集合 LinkedHashMultimap,因为这在将多个值与单个键关联时很有帮助。希望这个对你有帮助!
import com.google.common.collect.LinkedHashMultimap;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
public class App {
public static void main(String[] args) throws SAXException, FileNotFoundException, IOException {
// First pass - to determine headers
XMLReader xr = XMLReaderFactory.createXMLReader();
HeaderHandler handler = new HeaderHandler();
xr.setContentHandler(handler);
xr.setErrorHandler(handler);
FileReader r = new FileReader("test1.xml");
xr.parse(new InputSource(r));
LinkedHashMap<String, Integer> headers = handler.getHeaders();
int totalnumberofcolumns = 0;
for (int headercount : headers.values()) {
totalnumberofcolumns += headercount;
}
String[] columnheaders = new String[totalnumberofcolumns];
int i = 0;
for (Entry<String, Integer> entry : headers.entrySet()) {
for (int j = 0; j < entry.getValue(); j++) {
columnheaders[i] = entry.getKey();
i++;
}
}
StringBuilder sb = new StringBuilder();
for (String h : columnheaders) {
sb.append(h);
sb.append(',');
}
System.out.println(sb.substring(0, sb.length() - 1));
// Second pass - collect and output data
xr = XMLReaderFactory.createXMLReader();
DataHandler datahandler = new DataHandler();
datahandler.setHeaderArray(columnheaders);
xr.setContentHandler(datahandler);
xr.setErrorHandler(datahandler);
r = new FileReader("test1.xml");
xr.parse(new InputSource(r));
}
public static class HeaderHandler extends DefaultHandler {
private String content;
private String currentElement;
private boolean insideElement = false;
private Attributes attribs;
private LinkedHashMap<String, Integer> itemHeader;
private LinkedHashMap<String, Integer> accumulativeHeader = new LinkedHashMap<String, Integer>();
public HeaderHandler() {
super();
}
private LinkedHashMap<String, Integer> getHeaders() {
return accumulativeHeader;
}
private void addItemHeader(String headerName) {
if (itemHeader.containsKey(headerName)) {
itemHeader.put(headerName, itemHeader.get(headerName) + 1);
} else {
itemHeader.put(headerName, 1);
}
}
@Override
public void startElement(String uri, String name,
String qName, Attributes atts) {
if ("item".equalsIgnoreCase(qName)) {
itemHeader = new LinkedHashMap<String, Integer>();
}
currentElement = qName;
content = null;
insideElement = true;
attribs = atts;
}
@Override
public void endElement(String uri, String name, String qName) {
if (!"item".equalsIgnoreCase(qName) && !"root".equalsIgnoreCase(qName)) {
if (content != null && qName.equals(currentElement) && content.trim().length() > 0) {
addItemHeader(qName);
}
if (attribs != null) {
int attsLength = attribs.getLength();
if (attsLength > 0) {
for (int i = 0; i < attsLength; i++) {
String attName = attribs.getLocalName(i);
addItemHeader(attName);
}
}
}
}
if ("item".equalsIgnoreCase(qName)) {
for (Entry<String, Integer> entry : itemHeader.entrySet()) {
String headerName = entry.getKey();
Integer count = entry.getValue();
//System.out.println(entry.getKey() + ":" + entry.getValue());
if (accumulativeHeader.containsKey(headerName)) {
if (count > accumulativeHeader.get(headerName)) {
accumulativeHeader.put(headerName, count);
}
} else {
accumulativeHeader.put(headerName, count);
}
}
}
insideElement = false;
currentElement = null;
attribs = null;
}
@Override
public void characters(char ch[], int start, int length) {
if (insideElement) {
content = new String(ch, start, length);
}
}
}
public static class DataHandler extends DefaultHandler {
private String content;
private String currentElement;
private boolean insideElement = false;
private Attributes attribs;
private LinkedHashMultimap dataMap;
private String[] headerArray;
public DataHandler() {
super();
}
@Override
public void startElement(String uri, String name,
String qName, Attributes atts) {
if ("item".equalsIgnoreCase(qName)) {
dataMap = LinkedHashMultimap.create();
}
currentElement = qName;
content = null;
insideElement = true;
attribs = atts;
}
@Override
public void endElement(String uri, String name, String qName) {
if (!"item".equalsIgnoreCase(qName) && !"root".equalsIgnoreCase(qName)) {
if (content != null && qName.equals(currentElement) && content.trim().length() > 0) {
dataMap.put(qName, content);
}
if (attribs != null) {
int attsLength = attribs.getLength();
if (attsLength > 0) {
for (int i = 0; i < attsLength; i++) {
String attName = attribs.getLocalName(i);
dataMap.put(attName, attribs.getValue(i));
}
}
}
}
if ("item".equalsIgnoreCase(qName)) {
String data[] = new String[headerArray.length];
int i = 0;
for (String h : headerArray) {
if (dataMap.containsKey(h)) {
Object[] values = dataMap.get(h).toArray();
data[i] = (String) values[0];
if (values.length > 1) {
dataMap.removeAll(h);
for (int j = 1; j < values.length; j++) {
dataMap.put(h, values[j]);
}
} else {
dataMap.removeAll(h);
}
} else {
data[i] = "";
}
i++;
}
StringBuilder sb = new StringBuilder();
for (String d : data) {
sb.append(d);
sb.append(',');
}
System.out.println(sb.substring(0, sb.length() - 1));
}
insideElement = false;
currentElement = null;
attribs = null;
}
@Override
public void characters(char ch[], int start, int length) {
if (insideElement) {
content = new String(ch, start, length);
}
}
public void setHeaderArray(String[] headerArray) {
this.headerArray = headerArray;
}
}
}
回答by Uri
I am not convinced that SAX is the best approach for you. There are different ways you could use SAX here, though.
我不相信 SAX 是最适合您的方法。不过,您可以通过不同的方式在此处使用 SAX。
If element order is not guaranteed within certain elements, like ListingDetails, then you need to be proactive.
如果在某些元素中不能保证元素顺序,例如 ListingDetails,那么您需要积极主动。
When you start a ListingDetails, initialize a map as a member variable on the handler. In each subelement, set the appropriate key-value in that map. When you finish a ListingDetails, examine the map and explicitly mock values such as nulls for the missing elements. Assuming you have one ListingDetails per item, save it to a member variable in the handler.
当您启动 ListingDetails 时,将映射初始化为处理程序上的成员变量。在每个子元素中,在该映射中设置适当的键值。完成 ListingDetails 后,检查地图并显式模拟值,例如缺失元素的空值。假设每个项目有一个 ListingDetails,请将其保存到处理程序中的成员变量中。
Now, when your item element is over, have a function that writes the line of CSVs based on the map in the order you wanted.
现在,当您的 item 元素结束时,有一个函数可以按照您想要的顺序根据地图写入 CSV 行。
The risk with this is if you have corrupted XML. I would strongly consider setting all these variables to null when an item starts, and then checking for errors and announcing them when the item ends.
这样做的风险在于您是否损坏了 XML。我会强烈考虑在项目开始时将所有这些变量设置为 null,然后检查错误并在项目结束时通知它们。
回答by pampanet
You could use XStream (http://x-stream.github.io/) or JOX (http://www.wutka.com/jox.html) to recognize xml and then convert it to a Java Bean. I think you can convert the Beans to CSV automatically once you get the bean.
您可以使用 XStream ( http://x-stream.github.io/) 或 JOX ( http://www.wutka.com/jox.html) 来识别 xml,然后将其转换为 Java Bean。我认为您可以在获得 bean 后自动将 Beans 转换为 CSV。
回答by A. Ionescu
The best way to code based on your described requirement is to use the easy feature of FreeMarker and XML processing. See the docs.
根据您描述的要求进行编码的最佳方法是使用 FreeMarker 和 XML 处理的简单功能。请参阅文档。
In this case you will only need the template that will produce a CSV.
在这种情况下,您将只需要生成 CSV 的模板。
An alternative to this is XMLGen, but very similar in approach. Just look at that diagram and examples, and instead of SQL statements, you will output CSV.
另一种方法是XMLGen,但在方法上非常相似。只需查看该图和示例,您将输出 CSV,而不是 SQL 语句。
These two similar approaches are not "conventional" but do the job very quickly for your situation, and you don't have to learn XSL (quite hard to master I think).
这两种类似的方法不是“传统的”,而是针对您的情况非常快速地完成工作,而且您不必学习 XSL(我认为很难掌握)。
回答by mdma
Here some code that implements the conversion of the XML to CSV using StAX. Although the XML you gave is only an example, I hope that this shows you how to handle the optional elements.
这里有一些代码使用 StAX 实现了 XML 到 CSV 的转换。虽然您提供的 XML 只是一个示例,但我希望这向您展示了如何处理可选元素。
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
public class App
{
public static void main( String[] args ) throws XMLStreamException, FileNotFoundException
{
new App().convertXMLToCSV(new BufferedInputStream(new FileInputStream(args[0])), new BufferedOutputStream(new FileOutputStream(args[1])));
}
static public final String ROOT = "root";
static public final String ITEM = "Item";
static public final String ITEM_ID = "ItemID";
static public final String ITEM_DETAILS = "ListingDetails";
static public final String START_TIME = "StartTime";
static public final String END_TIME = "EndTime";
static public final String ITEM_URL = "ViewItemURL";
static public final String AVERAGES = "averages";
static public final String AVERAGE_TIME = "AverageTime";
static public final String AVERAGE_PRICE = "AveragePrice";
static public final String SEPARATOR = ",";
public void convertXMLToCSV(InputStream in, OutputStream out) throws XMLStreamException
{
PrintWriter writer = new PrintWriter(out);
XMLStreamReader xmlStreamReader = XMLInputFactory.newInstance().createXMLStreamReader(in);
convertXMLToCSV(xmlStreamReader, writer);
}
public void convertXMLToCSV(XMLStreamReader xmlStreamReader, PrintWriter writer) throws XMLStreamException {
writer.println("ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice");
xmlStreamReader.nextTag();
xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ROOT);
while (xmlStreamReader.hasNext()) {
xmlStreamReader.nextTag();
if (xmlStreamReader.isEndElement())
break;
xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ITEM);
String itemID = nextValue(xmlStreamReader, ITEM_ID);
xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, ITEM_DETAILS);
String startTime = nextValue(xmlStreamReader, START_TIME);
xmlStreamReader.nextTag();
String averageTime = null;
String averagePrice = null;
if (xmlStreamReader.getLocalName().equals(AVERAGES))
{
averageTime = nextValue(xmlStreamReader, AVERAGE_TIME);
averagePrice = nextValue(xmlStreamReader, AVERAGE_PRICE);
xmlStreamReader.nextTag();
xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, AVERAGES);
xmlStreamReader.nextTag();
}
String endTime = currentValue(xmlStreamReader, END_TIME);
String url = nextValue(xmlStreamReader,ITEM_URL);
xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ITEM_DETAILS);
xmlStreamReader.nextTag(); xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ITEM);
writer.append(esc(itemID)).append(SEPARATOR)
.append(esc(startTime)).append(SEPARATOR)
.append(esc(endTime)).append(SEPARATOR)
.append(esc(url));
if (averageTime!=null)
writer.append(SEPARATOR).append(esc(averageTime)).append(SEPARATOR)
.append(esc(averagePrice));
writer.println();
}
xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, ROOT);
writer.close();
}
private String esc(String string) {
if (string.indexOf(',')!=-1)
string = '"'+string+'"';
return string;
}
private String nextValue(XMLStreamReader xmlStreamReader, String name) throws XMLStreamException {
xmlStreamReader.nextTag();
return currentValue(xmlStreamReader, name);
}
private String currentValue(XMLStreamReader xmlStreamReader, String name) throws XMLStreamException {
xmlStreamReader.require(XMLStreamConstants.START_ELEMENT, null, name);
String value = "";
for (;;) {
int next = xmlStreamReader.next();
if (next==XMLStreamConstants.CDATA||next==XMLStreamConstants.SPACE||next==XMLStreamConstants.CHARACTERS)
value += xmlStreamReader.getText();
else if (next==XMLStreamConstants.END_ELEMENT)
break;
// ignore comments, PIs, attributes
}
xmlStreamReader.require(XMLStreamConstants.END_ELEMENT, null, name);
return value.trim();
}
}
回答by Robert Diana
This looks like a good case for using XSL. Given your basic requirements it may be easier to get at the right nodes with XSL as compared to custom parsers or serializers. The benefit would be that your XSL could target "//Item//AverageTime" or whatever nodes you require without worrying about node depth.
这看起来是使用 XSL 的好例子。鉴于您的基本要求,与自定义解析器或序列化器相比,使用 XSL 可能更容易获得正确的节点。好处是您的 XSL 可以针对“//Item//AverageTime”或您需要的任何节点,而无需担心节点深度。
UPDATE: The following is the xslt I threw together to make sure this worked as expected.
更新:以下是我拼凑在一起的 xslt,以确保它按预期工作。
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
ItemID,StartTime,EndTime,ViewItemURL,AverageTime,AveragePrice
<xsl:for-each select="//Item">
<xsl:value-of select="ItemID"/><xsl:text>,</xsl:text><xsl:value-of select="//StartTime"/><xsl:text>,</xsl:text><xsl:value-of select="//EndTime"/><xsl:text>,</xsl:text><xsl:value-of select="//ViewItemURL"/><xsl:text>,</xsl:text><xsl:value-of select="//AverageTime"/><xsl:text>,</xsl:text><xsl:value-of select="//AveragePrice"/><xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
回答by Pascal Thivent
I'm not sure I understand how generic the solution should be. Do you really want to parse a 1 GB file twice for a generic solution? And if you want something generic, why did you skipped the <category>
element in your example? How much different format do you need to handle? Do you really not know what the format can be (even if some element can be ommited)? Can you clarify?
我不确定我是否理解解决方案应该有多通用。您真的想为通用解决方案解析 1 GB 文件两次吗?如果您想要一些通用的东西,为什么要跳过<category>
示例中的元素?您需要处理多少不同的格式?你真的不知道格式可以是什么(即使可以省略某些元素)?你能澄清一下吗?
To my experience, it's generally preferable to parse specific files in a specific way (this doesn't exclude using a generic API though). My answer will go in this direction (and I'll update it after the clarification).
根据我的经验,通常最好以特定方式解析特定文件(但这并不排除使用通用 API)。我的答案将朝着这个方向发展(澄清后我会更新)。
If you don't feel comfortable with XML, you could consider using some existing (commercial) libraries, for example Ricebridge XML Managerand CSV Manager. See How to convert CSV into XML and XML into CSV using Javafor a full example. The approach is pretty straightforward: you define the data fields using XPath expressions (which is perfect in your case since you can have "extra" elements), parse the the file and then pass the result List
to the CSV component to generate the CSV file. The API looks simple, the code tested (the source code of their test casesis available under a BSD-style license), they claim supporting gigabyte-sized files.
如果您对 XML 不满意,可以考虑使用一些现有的(商业)库,例如 Ricebridge XML Manager和CSV Manager。有关完整示例,请参阅如何使用 Java 将 CSV 转换为 XML 和 XML 转换为 CSV。该方法非常简单:您使用 XPath 表达式定义数据字段(这在您的情况下是完美的,因为您可以拥有“额外”元素),解析文件,然后将结果传递List
给 CSV 组件以生成 CSV 文件。API 看起来很简单,经过测试的代码(他们的测试用例的源代码在 BSD 风格的许可下可用),他们声称支持千兆字节大小的文件。
You can get a Single Developer license for $170 which is not very expensive compared to developer daily rates.
您可以获得 170 美元的单一开发人员许可证,与开发人员的日常费率相比,这并不昂贵。
They offer 30 days trial versions, have a look.
他们提供 30 天的试用版,看看吧。
Another option would be to use Spring Batch. Spring batch offers everything required to work with XML filesas inputor output (using StAX and the XML binding framework of your choice) and flat filesas input or output. See:
另一种选择是使用Spring Batch。Spring batch 提供了将XML 文件用作输入或输出(使用 StAX 和您选择的 XML 绑定框架)和平面文件作为输入或输出所需的一切。看:
- the Spring Batch Documentation
- the Samples(especially the tradesample)
- A first look at Spring-Batch, part 2
- 在Spring Batch的文档
- 该样品(尤其是贸易样品)
- 初识 Spring-Batch,第 2 部分
You could also use Smooksto do XML to CSV transformations. See also:
您还可以使用Smooks进行 XML 到 CSV 的转换。也可以看看:
Another option would be to roll your own solution, using a StAX parser or, why not, using VTD-XMLand XPath. Have a look at:
另一种选择是推出您自己的解决方案,使用 StAX 解析器,或者为什么不使用VTD-XML和 XPath。看一下:
回答by Thorbj?rn Ravn Andersen
Note that this would be a prime example of using XSLT except that most XSLT processors read in the whole XML file into memory which is not an option as it is large. Note, however, that the enterprise version of Saxon can do streaming XSLT processing (if the XSLT script adheres to the restrictions).
请注意,这将是使用 XSLT 的一个主要示例,只是大多数 XSLT 处理器将整个 XML 文件读入内存,这不是一个选项,因为它很大。但是请注意,Saxon 的企业版可以进行流式 XSLT 处理(如果 XSLT 脚本遵守限制)。
You may also want to use an external XSLT processor outside your JVM instead, if applicable. This opens up for several more options.
如果适用,您可能还希望在 JVM 之外使用外部 XSLT 处理器。这为更多选项打开了大门。
Streaming in Saxon-EE: http://www.saxonica.com/documentation/sourcedocs/serial.html
在 Saxon-EE 中流式传输:http: //www.saxonica.com/documentation/sourcedocs/serial.html