如何从Java中漂亮地打印XML?

499

我有一个包含XML代码的Java字符串,其中没有换行或缩进。我想将其转换为格式良好的XML字符串。如何做到这一点?

String unformattedXml = "<tag><nested>hello</nested></tag>";
String formattedXml = new [UnknownClass]().format(unformattedXml);
注意:我的输入是一个字符串(String)。我的输出也是一个字符串(String)。
(基础)模拟结果:
<?xml version="1.0" encoding="UTF-8"?>
<root>
  <tag>
    <nested>hello</nested>
  </tag>
</root>

请查看这个问题:https://dev59.com/lXM_5IYBdhLWcg3wslfs - dfa
10
只是好奇,你是将这个输出发送到XML文件或其他需要缩进很重要的地方吗?之前我非常关注格式化我的XML以便正确显示它...但在花费了大量时间后,我意识到我必须将输出发送到一个Web浏览器,并且任何相对现代的Web浏览器都可以以漂亮的树形结构显示XML,所以我可以忘记这个问题并继续前进。我提到这个只是为了防止你(或其他有同样问题的用户)可能会忽略同样的细节。 - Abel Morelos
4
@Abel,将数据保存到文本文件中,插入到HTML文本区域中,并将其倒出到控制台以进行调试。 - Steve McLeod
6
“put on hold as too broad” 的意思是“因问题过于广泛而被搁置”,目前很难比问题更加精确明了! - Steve McLeod
34个回答

307
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
// initialize StreamResult with File object to save to file
StreamResult result = new StreamResult(new StringWriter());
DOMSource source = new DOMSource(doc);
transformer.transform(source, result);
String xmlString = result.getWriter().toString();
System.out.println(xmlString);

注意:结果可能会因Java版本而异。请搜索与您平台特定的解决方法。


1
如何使输出不包含 <?xml version="1.0" encoding="UTF-8"?> - Thang Pham
28
为了省略 <?xml ...> 声明,请添加 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")。要求翻译内容保持原意,通俗易懂。 - rustyx
5
一般读者可能会发现这里描述的解决方案(https://dev59.com/sV8e5IYBdhLWcg3wVJEC#33541820)的改进版本很有用。 - Stephan
1
这个解决方案移除了 CDATA 包装器。是否有一个标志可以防止它? - Marinos An

145

基于这个答案的一个更简单的解决方案

public static String prettyFormat(String input, int indent) {
    try {
        Source xmlInput = new StreamSource(new StringReader(input));
        StringWriter stringWriter = new StringWriter();
        StreamResult xmlOutput = new StreamResult(stringWriter);
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        transformerFactory.setAttribute("indent-number", indent);
        transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
        transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
        Transformer transformer = transformerFactory.newTransformer(); 
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.transform(xmlInput, xmlOutput);
        return xmlOutput.getWriter().toString();
    } catch (Exception e) {
        throw new RuntimeException(e); // simple exception handling, please review it
    }
}

public static String prettyFormat(String input) {
    return prettyFormat(input, 2);
}

测试用例:

prettyFormat("<root><child>aaa</child><child/></root>");

返回:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <child>aaa</child>
  <child/>
</root>

//忽略:原始编辑只需要在代码中的类名中添加缺失的s。冗余的六个字符添加是为了通过SO上的6个字符验证。


1
这是我一直使用的代码,但在这家公司它不起作用,我猜他们正在使用另一个XML转换库。我将工厂创建为一个单独的行,然后执行factory.setAttribute("indent-number", 4);,现在它可以工作了。 - Adrian Smith
4
@Harry: transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - jjmontes
6
嗨,我正在使用完全相同的代码,我的格式化正常,除了第一个元素。所以这个:<?xml version="1.0" encoding="UTF-8"?><root> 全部在一行上。有任何想法为什么会这样吗? - CodyK
@dfa:我喜欢这个评论//简单的异常处理,请审核它。你能指出一些推荐这种类型异常处理的资源吗?谢谢。 - John
5
@Codemiester:似乎是一个bug(参见https://dev59.com/5XXYa4cB1Zd3GeqP9bVV#18251901)。对我来说,添加 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); 是有效的。 - jansohn
显示剩余3条评论

140

这是我自己问题的答案。我结合了各种结果的答案,编写了一个能够漂亮打印XML的类。

无法保证它在处理无效XML或大型文档时的响应。

package ecb.sdw.pretty;

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Pretty-prints xml, supplied as a string.
 * <p/>
 * eg.
 * <code>
 * String formattedXml = new XmlFormatter().format("<tag><nested>hello</nested></tag>");
 * </code>
 */
public class XmlFormatter {

    public XmlFormatter() {
    }

    public String format(String unformattedXml) {
        try {
            final Document document = parseXmlFile(unformattedXml);

            OutputFormat format = new OutputFormat(document);
            format.setLineWidth(65);
            format.setIndenting(true);
            format.setIndent(2);
            Writer out = new StringWriter();
            XMLSerializer serializer = new XMLSerializer(out, format);
            serializer.serialize(document);

            return out.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Document parseXmlFile(String in) {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            InputSource is = new InputSource(new StringReader(in));
            return db.parse(is);
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        String unformattedXml =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><QueryMessage\n" +
                        "        xmlns=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/message\"\n" +
                        "        xmlns:query=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/query\">\n" +
                        "    <Query>\n" +
                        "        <query:CategorySchemeWhere>\n" +
                        "   \t\t\t\t\t         <query:AgencyID>ECB\n\n\n\n</query:AgencyID>\n" +
                        "        </query:CategorySchemeWhere>\n" +
                        "    </Query>\n\n\n\n\n" +
                        "</QueryMessage>";

        System.out.println(new XmlFormatter().format(unformattedXml));
    }

}

13
请注意,这个答案需要使用Xerces。如果您不想添加此依赖项,则可以直接使用标准jdk库和javax.xml.transform.Transformer(请参见下面的答案)。 - khylo
45
回到2008年,这是一个很好的答案,但现在所有的操作都可以使用标准JDK类完成,而不需要使用Apache类。请参考http://xerces.apache.org/xerces2-j/faq-general.html#faq-6。是的,这是Xerces的常见问题解答,但答案涵盖了标准JDK类。最初1.5版本的这些类存在许多问题,但从1.6版本开始一切都运行良好。在FAQ中复制LSSerializer示例,删除“...”部分,并在`LSSerializer writer = ...行之后添加writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);`。 - George Hawkins
2
我使用@GeorgeHawkins提供的链接给出的Apache示例创建了一个小类。它缺少变量document的初始化方式,所以我想添加声明并快速制作一个示例。如果需要更改内容,请告诉我,链接为http://pastebin.com/XL7932aC。 - samwell
你只使用JDK是不可靠的,这并不是真实情况。这取决于一些内部注册表实现,而我的JDK7u72默认情况下并没有激活它。因此,你最好直接使用Apache的工具。 - user1050755
这里有一个没有任何依赖的解决方案:https://dev59.com/sV8e5IYBdhLWcg3wVJEC#33541820。 - Stephan
显示剩余3条评论

107

现在已经是2012年了,Java可以比以前更多地使用XML,我想给我的被接受的答案添加一个替代方案。这个方案没有Java 6以外的依赖。

import org.w3c.dom.Node;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;

/**
 * Pretty-prints xml, supplied as a string.
 * <p/>
 * eg.
 * <code>
 * String formattedXml = new XmlFormatter().format("<tag><nested>hello</nested></tag>");
 * </code>
 */
public class XmlFormatter {

    public String format(String xml) {

        try {
            final InputSource src = new InputSource(new StringReader(xml));
            final Node document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
            final Boolean keepDeclaration = Boolean.valueOf(xml.startsWith("<?xml"));

        //May need this: System.setProperty(DOMImplementationRegistry.PROPERTY,"com.sun.org.apache.xerces.internal.dom.DOMImplementationSourceImpl");


            final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
            final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
            final LSSerializer writer = impl.createLSSerializer();

            writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); // Set this to true if the output needs to be beautified.
            writer.getDomConfig().setParameter("xml-declaration", keepDeclaration); // Set this to true if the declaration is needed to be outputted.

            return writer.writeToString(document);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        String unformattedXml =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><QueryMessage\n" +
                        "        xmlns=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/message\"\n" +
                        "        xmlns:query=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/query\">\n" +
                        "    <Query>\n" +
                        "        <query:CategorySchemeWhere>\n" +
                        "   \t\t\t\t\t         <query:AgencyID>ECB\n\n\n\n</query:AgencyID>\n" +
                        "        </query:CategorySchemeWhere>\n" +
                        "    </Query>\n\n\n\n\n" +
                        "</QueryMessage>";

        System.out.println(new XmlFormatter().format(unformattedXml));
    }
}

我尝试了这个解决方案,发现返回的XML字符串总是UTF-16。当文档被序列化回字符串时,有这样一行代码:ser._format.setEncoding("UTF-16");。这可能是现在的标准,但对于我正在使用的系统,使用的是UTF-8。有人知道如何保持原始XML字符串的编码吗? - Dan Temple
2
@DanTemple 看起来你需要使用LSOutput来控制编码。请参考http://www.chipkillmar.net/2009/03/25/pretty-print-xml-from-a-dom/。 - Joshua Davis
2
我尝试在安卓中使用这个,但是我找不到DOMImplementationRegistry包。我正在使用Java 8。 - Chintan Soni
1
设置编码方式: LSOutput output = impl.createLSOutput(); output.setEncoding("UTF-8"); output.setByteStream(new ByteArrayOutputStream()); writer.write(document, output); return output.getByteStream().toString(); - Ali Cheaito
3
感谢您也包含了导入清单,否则很难理解所需组合的这么多冲突的软件包。 - Leon
显示剩余8条评论

54
只是要注意,最佳答案需要使用xerces。
如果您不想添加这个外部依赖项,那么您可以简单地使用标准的jdk库(实际上是在内部使用xerces构建的)。
注意:jdk版本1.5存在一个错误,请参见https://bugs.java.com/bugdatabase/view_bug?bug_id=6296446,但现在已经解决了。
(注意,如果发生错误,将返回原始文本)
package com.test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;

import org.xml.sax.InputSource;

public class XmlTest {
    public static void main(String[] args) {
        XmlTest t = new XmlTest();
        System.out.println(t.formatXml("<a><b><c/><d>text D</d><e value='0'/></b></a>"));
    }

    public String formatXml(String xml){
        try{
            Transformer serializer= SAXTransformerFactory.newInstance().newTransformer();
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            //serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            //serializer.setOutputProperty("{http://xml.customer.org/xslt}indent-amount", "2");
            Source xmlSource=new SAXSource(new InputSource(new ByteArrayInputStream(xml.getBytes())));
            StreamResult res =  new StreamResult(new ByteArrayOutputStream());            
            serializer.transform(xmlSource, res);
            return new String(((ByteArrayOutputStream)res.getOutputStream()).toByteArray());
        }catch(Exception e){
            //TODO log error
            return xml;
        }
    }

}

在这种情况下,不使用左制表符。所有标签都从行的第一个符号开始,就像普通文本一样。 - Ruslan
在字节和字符串之间相互转换时,您需要指定字符集吗? - Will Glass
2
不需要从字节数组/字符串进行转换。至少在这样做时,您必须指定字符集。更好的选择是使用StringReader和StringWriter类,包装在InputSource和StreamResult中。 - maximdim
无法工作。您需要在一些内部注册表实现上进行调整。 - user1050755
这里有一个更简单的解决方案变体:https://dev59.com/sV8e5IYBdhLWcg3wVJEC#33541820 - Stephan

33

过去我曾使用org.dom4j.io.OutputFormat.createPrettyPrint()方法进行漂亮的打印。

public String prettyPrint(final String xml){  

    if (StringUtils.isBlank(xml)) {
        throw new RuntimeException("xml was null or blank in prettyPrint()");
    }

    final StringWriter sw;

    try {
        final OutputFormat format = OutputFormat.createPrettyPrint();
        final org.dom4j.Document document = DocumentHelper.parseText(xml);
        sw = new StringWriter();
        final XMLWriter writer = new XMLWriter(sw, format);
        writer.write(document);
    }
    catch (Exception e) {
        throw new RuntimeException("Error pretty printing xml:\n" + xml, e);
    }
    return sw.toString();
}

4
被接受的解决方案在我的情况下没有正确缩进嵌套标签,而这个解决方案则可以。 - Chase Seibert
3
我配合将行尾的空格全部删除使用这段代码:prettyPrintedString.replaceAll("\\s+\n", "\n")。它可以使格式更加美观,同时保持原有含义。 - jediz

19

以下是使用dom4j的方法:

导入:

import org.dom4j.Document;  
import org.dom4j.DocumentHelper;  
import org.dom4j.io.OutputFormat;  
import org.dom4j.io.XMLWriter;

代码:

String xml = "<your xml='here'/>";  
Document doc = DocumentHelper.parseText(xml);  
StringWriter sw = new StringWriter();  
OutputFormat format = OutputFormat.createPrettyPrint();  
XMLWriter xw = new XMLWriter(sw, format);  
xw.write(doc);  
String result = sw.toString();

1
这对我没用。它只是给了一行类似于 <?xml version... 的东西,另一行则是其他的内容。 - sixtyfootersdude
添加 format.setSuppressDeclaration(true); 以移除 <?xml version.. 标签。 - mrboieng

16

由于您的起点是一个 String,在使用 Transformer 之前,可以转换为 DOM 对象(例如 Node)。但是,如果您知道 XML 字符串有效,并且不想承担将字符串解析成 DOM 的内存开销,然后再运行转换以获取字符串的开销,那么您可以使用老式的逐个字符解析方法。在每个 </...> 字符后插入换行符和空格,保持缩进计数器(以确定空格数),对于每个看到的 <...> 减少并对每个 </...> 增加。

免责声明 - 我对以下功能进行了剪切/粘贴/文本编辑,因此它们可能无法直接编译。

public static final Element createDOM(String strXML) 
    throws ParserConfigurationException, SAXException, IOException {

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setValidating(true);
    DocumentBuilder db = dbf.newDocumentBuilder();
    InputSource sourceXML = new InputSource(new StringReader(strXML));
    Document xmlDoc = db.parse(sourceXML);
    Element e = xmlDoc.getDocumentElement();
    e.normalize();
    return e;
}

public static final void prettyPrint(Node xml, OutputStream out)
    throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException {
    Transformer tf = TransformerFactory.newInstance().newTransformer();
    tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    tf.setOutputProperty(OutputKeys.INDENT, "yes");
    tf.transform(new DOMSource(xml), new StreamResult(out));
}

1
"然而,如果您知道您的XML字符串是有效的......" 这是一个很好的观点。请参见下面基于这种方法的解决方案。 - David Easley
这个答案是不正确的。并不是必须要转换为DOM。其他答案展示了如何直接从StreamSource转换到StreamResult;这些解决方案可以在不需要在内存中构建DOM的情况下实现所需的效果。 - Michael Kay

15

Kevin Hakanson说道: "然而,如果您知道您的XML字符串是有效的,并且不想承担将字符串解析为DOM所需的内存开销,然后在DOM上运行转换以获取一个字符串-您可以只进行一些老式字符逐个解析。在每个“<...>”之后插入换行符和空格,保留缩进计数器(用于确定空格数),您增加每个“<...>”,并减少看到每个“</...>”。"

同意。这种方法要快得多,依赖性也要少得多。

示例解决方案:

/**
 * XML utils, including formatting.
 */
public class XmlUtils
{
  private static XmlFormatter formatter = new XmlFormatter(2, 80);

  public static String formatXml(String s)
  {
    return formatter.format(s, 0);
  }

  public static String formatXml(String s, int initialIndent)
  {
    return formatter.format(s, initialIndent);
  }

  private static class XmlFormatter
  {
    private int indentNumChars;
    private int lineLength;
    private boolean singleLine;

    public XmlFormatter(int indentNumChars, int lineLength)
    {
      this.indentNumChars = indentNumChars;
      this.lineLength = lineLength;
    }

    public synchronized String format(String s, int initialIndent)
    {
      int indent = initialIndent;
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < s.length(); i++)
      {
        char currentChar = s.charAt(i);
        if (currentChar == '<')
        {
          char nextChar = s.charAt(i + 1);
          if (nextChar == '/')
            indent -= indentNumChars;
          if (!singleLine)   // Don't indent before closing element if we're creating opening and closing elements on a single line.
            sb.append(buildWhitespace(indent));
          if (nextChar != '?' && nextChar != '!' && nextChar != '/')
            indent += indentNumChars;
          singleLine = false;  // Reset flag.
        }
        sb.append(currentChar);
        if (currentChar == '>')
        {
          if (s.charAt(i - 1) == '/')
          {
            indent -= indentNumChars;
            sb.append("\n");
          }
          else
          {
            int nextStartElementPos = s.indexOf('<', i);
            if (nextStartElementPos > i + 1)
            {
              String textBetweenElements = s.substring(i + 1, nextStartElementPos);

              // If the space between elements is solely newlines, let them through to preserve additional newlines in source document.
              if (textBetweenElements.replaceAll("\n", "").length() == 0)
              {
                sb.append(textBetweenElements + "\n");
              }
              // Put tags and text on a single line if the text is short.
              else if (textBetweenElements.length() <= lineLength * 0.5)
              {
                sb.append(textBetweenElements);
                singleLine = true;
              }
              // For larger amounts of text, wrap lines to a maximum line length.
              else
              {
                sb.append("\n" + lineWrap(textBetweenElements, lineLength, indent, null) + "\n");
              }
              i = nextStartElementPos - 1;
            }
            else
            {
              sb.append("\n");
            }
          }
        }
      }
      return sb.toString();
    }
  }

  private static String buildWhitespace(int numChars)
  {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < numChars; i++)
      sb.append(" ");
    return sb.toString();
  }

  /**
   * Wraps the supplied text to the specified line length.
   * @lineLength the maximum length of each line in the returned string (not including indent if specified).
   * @indent optional number of whitespace characters to prepend to each line before the text.
   * @linePrefix optional string to append to the indent (before the text).
   * @returns the supplied text wrapped so that no line exceeds the specified line length + indent, optionally with
   * indent and prefix applied to each line.
   */
  private static String lineWrap(String s, int lineLength, Integer indent, String linePrefix)
  {
    if (s == null)
      return null;

    StringBuilder sb = new StringBuilder();
    int lineStartPos = 0;
    int lineEndPos;
    boolean firstLine = true;
    while(lineStartPos < s.length())
    {
      if (!firstLine)
        sb.append("\n");
      else
        firstLine = false;

      if (lineStartPos + lineLength > s.length())
        lineEndPos = s.length() - 1;
      else
      {
        lineEndPos = lineStartPos + lineLength - 1;
        while (lineEndPos > lineStartPos && (s.charAt(lineEndPos) != ' ' && s.charAt(lineEndPos) != '\t'))
          lineEndPos--;
      }
      sb.append(buildWhitespace(indent));
      if (linePrefix != null)
        sb.append(linePrefix);

      sb.append(s.substring(lineStartPos, lineEndPos + 1));
      lineStartPos = lineEndPos + 1;
    }
    return sb.toString();
  }

  // other utils removed for brevity
}

2
这就是正确的做法。在字符串级别上即时格式化。这是唯一能够格式化无效或不完整XML的解决方案。 - Florian F

12
如果使用第三方XML库是可以接受的,那么可以比当前最高票 答案建议的方法简单得多。已经声明输入和输出都应该是字符串,因此这里提供一个使用XOM库实现的实用方法:
import nu.xom.*;
import java.io.*;

[...]

public static String format(String xml) throws ParsingException, IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    Serializer serializer = new Serializer(out);
    serializer.setIndent(4);  // or whatever you like
    serializer.write(new Builder().build(xml, ""));
    return out.toString("UTF-8");
}

我测试了它的工作原理,结果与您的JRE版本或其他任何东西无关。要了解如何自定义输出格式,请查看Serializer API。
实际上,这比我想象的要长 - 因为 Serializer 需要一个 OutputStream 来写入。但请注意,这里实际上很少有用于XML操作的代码。

(这个回答是我对XOM的评估的一部分,它被建议作为我的有关最佳Java XML库的问题中替换dom4j的一个选项。值得一提的是,使用dom4j,您可以使用类似的方式使用XMLWriterOutputFormat轻松实现此目标。 编辑:...如mlo55的答案所示。)


2
谢谢,这就是我要找的内容。 如果您已经使用XOM解析了XML并获得了“Document”对象,您可以直接将其传递给serializer.write(document); - Thibault D.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接