@Before可能会有一些重复的问题建议,我不认为是这种情况可能先读一下,我会尽量做到尽可能简短。标题给出了基本的想法。
以下是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>
这是一个示例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>
我从谷歌借用了这个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
第一行是标题,它也应该包含在csv中。今天我得到了一些有用的链接到stax,我真的不知道什么是正确的/最佳的方法,我现在正在努力3天,不是真的愿意放弃。
告诉我你的想法如何解决这个问题
我忘了提到这是一个非常庞大的xml文件,最大1gb
BOUNTY UPDATE:
我正在寻找更多的通用方法,这意味着这应该适用于任何数量的任何深度的节点,有时在示例xml中,可能会发生一个item
对象具有更多节点比下一个/上一个,所以也应该有这种情况(因此所有列和值都匹配CSV)。
也可能发生节点具有相同名称/ localName但不同的值和属性,如果是这种情况,则新列应以CSV显示,并具有适当的值。 (我在名为<averages>
)的category
标记中添加了此案例的示例
答案 0 :(得分:11)
提供的代码应该被视为草图而不是权威性文章。我不是SAX的专家,可以改进实现以获得更好的性能,更简单的代码等。那就是说SAX应该能够处理流式传输大型XML文件。
我会使用SAX解析器进行2次传递来解决这个问题。 (顺便说一句,我也会使用CSV生成库来创建输出,因为这会处理CSV涉及的所有可疑角色,但我没有在草图中实现这一点。)
第一遍: 建立标题列数
第二遍: 输出CSV
我认为XML文件格式正确。我假设我们没有具有预定义顺序的方案/ DTD。
在第一遍中,我假设将为包含文本内容或任何属性的每个XML元素添加CSV列(我假设属性将包含某些内容!)。
第二次通过,确定了目标列的数量,将执行实际的CSV输出。
根据您的示例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
请注意我使用了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;
}
}
}
答案 1 :(得分:8)
这看起来像是使用XSL的好例子。根据您的基本要求,与自定义解析器或序列化器相比,使用XSL获取正确的节点可能更容易。好处是您的XSL可以定位“// Item // AverageTime”或您需要的任何节点,而不必担心节点深度。
更新:以下是我联合起来的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>
答案 2 :(得分:5)
我不确定我是否理解解决方案的通用性。您是否真的想要为通用解决方案解析1 GB文件两次?如果你想要通用的东西,你为什么跳过你的例子中的<category>
元素?你需要处理多少不同的格式?你真的不知道格式是什么(即使一些元素可以被省略)?你能澄清一下吗?
根据我的经验,通常最好以特定方式解析特定文件(但这并不排除使用通用API)。我的答案将朝着这个方向发展(我会在澄清之后对其进行更新)。
如果您对XML不满意,可以考虑使用一些现有的(商业)库,例如Ricebridge XML Manager和CSV Manager。有关完整示例,请参阅How to convert CSV into XML and XML into CSV using Java。这种方法非常简单:使用XPath表达式定义数据字段(在您的情况下这是完美的,因为您可以使用“额外”元素),解析文件,然后将结果List
传递给CSV组件生成CSV文件。 API看起来很简单,测试的代码(他们的test cases的源代码在BSD风格的许可下可用),他们声称支持千兆字节大小的文件。
您可以获得170美元的单一开发人员许可证,与开发者每日费率相比并不是非常昂贵。
他们提供30天试用版,看看。
另一种选择是使用Spring Batch。 Spring批处理提供了使用XML files作为input或输出(使用StAX和您选择的XML绑定框架)和flat files作为输入或output所需的一切。参见:
您还可以使用Smooks将XML转换为CSV transformations。另见:
另一种选择是使用StAX解析器推送自己的解决方案,或者为什么不使用VTD-XML和XPath。看看:
答案 3 :(得分:2)
根据您描述的要求进行编码的最佳方式是使用FreeMarker和XML处理的简单功能。 See the docs
在这种情况下,您只需要生成CSV的模板。
另一种选择是XMLGen,但方法非常相似。只需查看该图表和示例,而不是SQL语句,您将输出CSV。
这两种类似的方法不是“传统的”,但可以很快地完成你的工作,而且你不必学习XSL(我认为很难掌握)。
答案 4 :(得分:2)
这里有一些代码实现了使用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();
}
}
答案 5 :(得分:1)
我不相信SAX是最适合您的方法。 但是,有不同的方法可以在这里使用SAX。
如果在某些元素(如ListingDetails)中无法保证元素顺序,那么您需要主动。
当您启动ListingDetails时,将地图初始化为处理程序上的成员变量。在每个子元素中,在该映射中设置适当的键值。完成ListingDetails后,检查映射并显式模拟缺失元素的值,例如null。假设每个项目有一个ListingDetails,将其保存到处理程序中的成员变量。
现在,当你的item元素结束时,有一个函数可以按你想要的顺序根据地图写入CSV行。
这样做的风险是您的XML已损坏。我强烈考虑在项目开始时将所有这些变量设置为null,然后检查错误并在项目结束时通知它们。
答案 6 :(得分:1)
请注意,这将是使用XSLT的一个主要示例,除了大多数XSLT处理器将整个XML文件读入内存,因为它很大而不是一个选项。但请注意,Saxon的企业版可以进行流式XSLT处理(如果XSLT脚本遵守限制)。
如果适用,您可能还希望在JVM外部使用外部XSLT处理器。这开辟了更多选择。
Saxon-EE中的流媒体:http://www.saxonica.com/documentation/sourcedocs/serial.html
答案 7 :(得分:0)
您可以使用XStream(http://x-stream.github.io/)或JOX(http://www.wutka.com/jox.html)来识别xml,然后将其转换为Java Bean。我想你可以在获得bean后自动将Beans转换为CSV。