¿La mejor manera de codificar datos de texto para XML en Java?


93

Muy similar a esta pregunta , excepto para Java.

¿Cuál es la forma recomendada de codificar cadenas para una salida XML en Java? Las cadenas pueden contener caracteres como "&", "<", etc.

Respuestas:


41

Muy simple: use una biblioteca XML. De esa manera, será realmente correcto en lugar de requerir un conocimiento detallado de los bits de la especificación XML.


25
¿Puedes recomendar una biblioteca así? (Me sorprende que esta no sea una parte estándar de la edición 5 de Java ... una tarea tan común).
Tim Cooper

4
XML es parte del marco estándar de Java; busque en org.w3c.sax y org.w3c.dom. Sin embargo, también hay un marco más fácil de usar, como JDom. Tenga en cuenta que puede que no exista un método de "codificación de cadenas para salida XML"; estaba recomendando más que toda la tarea XML se hiciera con una biblioteca en lugar de simplemente hacer bits a la vez con manipulación de cadenas.
Jon Skeet

1
Este no es un consejo tan útil cuando se genera XHTML: FlyingSaucer requiere XML, pero no hay forma de que esté usando una plantilla a través de una biblioteca XML :). Afortunadamente, StringTemplate me permite escapar rápidamente de todos los objetos String.
Stephen

4
@mice: la pregunta está etiquetada como Java, y Java tiene muchas bibliotecas XML. De hecho, hay API XML integradas en Java, por lo que no habría necesidad de agregar nada más ... pero incluso si lo hiciera, unos cientos de K rara vez son un problema fuera de los dispositivos móviles en estos días. Incluso si no fuera Java, desconfiaría mucho de desarrollar en una plataforma que no tuviera ninguna API XML ...
Jon Skeet

2
@mice: La API DOM es perfectamente capaz de generar XML. O hay bibliotecas de terceros bastante pequeñas. (El archivo jar de JDom es 114K, por ejemplo.) El uso de una API XML sigue siendo la forma recomendada de crear XML.
Jon Skeet

124

Como han mencionado otros, usar una biblioteca XML es la forma más sencilla. Si desea escapar, puede buscar en StringEscapeUtilsla biblioteca Apache Commons Lang .


Este podría ser el camino a seguir si no le importa la corrección absoluta, por ejemplo, si está armando un prototipo.
Chase Seibert

2
Utilizar StringEscapeUtils.escapeXml(str)desde commons-lang. Lo uso en la aplicación App Engine: funciona como un encanto. Aquí está el documento de Java para esta función:
Oleg K

El método escapeXml de StringEscapeUtils parece ser un poco costoso. ¿Existe un método más eficiente que opere en un StringBuffer en lugar de un String?
CKing

¿Este método funciona tanto para contenido XML como para atributos? A mí me parece que no funciona con los atributos. No parece escapar \t, \ny \r.
Lii

@Lii y \t, ¿ \no \res necesario escapar?
Betlista

20

Solo usa.

<![CDATA[ your text here ]]>

Esto permitirá que cualquier personaje excepto el final

]]>

Por lo tanto, puede incluir caracteres que serían ilegales como & y>. Por ejemplo.

<element><![CDATA[ characters such as & and > are allowed ]]></element>

Sin embargo, los atributos deberán escaparse ya que los bloques CDATA no se pueden usar para ellos.


11
En la mayoría de los casos, eso no es lo que debe hacer. Demasiadas personas abusan de las etiquetas CDATA. La intención del CDATA es decirle al procesador que no lo procese como XML y simplemente lo pase. Si está intentando crear un archivo XML, entonces debería crear XML, no solo pasar bytes a través de algún elemento de ajuste.
Mads Hansen

2
@Mads, el uso de CDATA da como resultado un archivo XML válido, por lo que es tan bueno como hacerlo de la "manera correcta". Si no le gusta, analícelo después, transforme su identidad e imprímalo.
Thorbjørn Ravn Andersen

24
Si envuelve texto en un elemento CDATA, debe escapar del marcador de cierre CDATA: "]]>" ... excepto que no puede escapar de eso. Entonces, en su lugar, debe dividir su código en partes donde coloca la mitad de los datos en un elemento CDATA y la otra mitad en un segundo: <! [CDATA [Estos datos contienen un marcador de cierre CDATA: "]]]]> <! [CDATA [> "es por eso que tuvo que dividirse.]]> ... Al final, puede ser mucho más sencillo simplemente escapar de '<', '>' y '&' en su lugar. Por supuesto, muchas aplicaciones ignoran el problema potencial con los marcadores de cierre CDATA en los datos. La ignorancia es una bendición, supongo. :)
Stijn de Witt

3
@StijndeWitt es absolutamente correcto. CDATA no es una panacea para escapar de caracteres especiales.
dnault

Esta es una mala idea. CDATA no permite ningún carácter fuera de la codificación XML.
Florian F

14

Esto me ha funcionado bien para proporcionar una versión de escape de una cadena de texto:

public class XMLHelper {

/**
 * Returns the string where all non-ascii and <, &, > are encoded as numeric entities. I.e. "&lt;A &amp; B &gt;"
 * .... (insert result here). The result is safe to include anywhere in a text field in an XML-string. If there was
 * no characters to protect, the original string is returned.
 * 
 * @param originalUnprotectedString
 *            original string which may contain characters either reserved in XML or with different representation
 *            in different encodings (like 8859-1 and UFT-8)
 * @return
 */
public static String protectSpecialCharacters(String originalUnprotectedString) {
    if (originalUnprotectedString == null) {
        return null;
    }
    boolean anyCharactersProtected = false;

    StringBuffer stringBuffer = new StringBuffer();
    for (int i = 0; i < originalUnprotectedString.length(); i++) {
        char ch = originalUnprotectedString.charAt(i);

        boolean controlCharacter = ch < 32;
        boolean unicodeButNotAscii = ch > 126;
        boolean characterWithSpecialMeaningInXML = ch == '<' || ch == '&' || ch == '>';

        if (characterWithSpecialMeaningInXML || unicodeButNotAscii || controlCharacter) {
            stringBuffer.append("&#" + (int) ch + ";");
            anyCharactersProtected = true;
        } else {
            stringBuffer.append(ch);
        }
    }
    if (anyCharactersProtected == false) {
        return originalUnprotectedString;
    }

    return stringBuffer.toString();
}

}

1
stringBuffer.append ("& #" + (int) ch + ";"); Esto no funcionará para caracteres multibyte. Me estoy encontrando con esto ahora mismo con un personaje emoji, secuencia UTF8 F0 9F 98 8D.
Kylar

14

Prueba esto:

String xmlEscapeText(String t) {
   StringBuilder sb = new StringBuilder();
   for(int i = 0; i < t.length(); i++){
      char c = t.charAt(i);
      switch(c){
      case '<': sb.append("&lt;"); break;
      case '>': sb.append("&gt;"); break;
      case '\"': sb.append("&quot;"); break;
      case '&': sb.append("&amp;"); break;
      case '\'': sb.append("&apos;"); break;
      default:
         if(c>0x7e) {
            sb.append("&#"+((int)c)+";");
         }else
            sb.append(c);
      }
   }
   return sb.toString();
}

8
Tienes al menos dos errores que puedo ver. Uno es sutil, el otro no lo es. No tendría ese error, porque no reinventaría la rueda en primer lugar.
Jon Skeet

1
Y la iteración a través de cadenas Unicode es un poco más complicada. Vea aquí: stackoverflow.com/q/1527856/402322
ceving

1
No estoy seguro de que sea sutil, pero es mejor considerar el caso t==null.
Myobis

1
@ user1003916: el escape XML está diseñado para convertir cualquier & ocurrencia en & amp; así es como tiene que funcionar. Si escapa a una cadena que ya escapó, es su culpa.
Puntero nulo

3
Estoy contento con la versión final. Java SE es compacto, rápido y eficiente. En mi opinión, siempre es mejor hacer lo que hay que hacer en lugar de descargar otros 100 MB de bloatware.
Roger

11

¡Esta pregunta tiene ocho años y aún no es una respuesta completamente correcta! No, no debería tener que importar una API de terceros completa para realizar esta sencilla tarea. Mal consejo.

El siguiente método:

  • Manejar correctamente personajes fuera del plano multilingüe básico.
  • caracteres de escape necesarios en XML
  • escapar de cualquier carácter no ASCII, que es opcional pero común
  • reemplace los caracteres ilegales en XML 1.0 con el carácter de sustitución Unicode. No existe la mejor opción aquí: eliminarlos es igualmente válido.

Intenté optimizar para el caso más común, sin dejar de asegurarme de que podría canalizar / dev / random a través de esto y obtener una cadena válida en XML.

public static String encodeXML(CharSequence s) {
    StringBuilder sb = new StringBuilder();
    int len = s.length();
    for (int i=0;i<len;i++) {
        int c = s.charAt(i);
        if (c >= 0xd800 && c <= 0xdbff && i + 1 < len) {
            c = ((c-0xd7c0)<<10) | (s.charAt(++i)&0x3ff);    // UTF16 decode
        }
        if (c < 0x80) {      // ASCII range: test most common case first
            if (c < 0x20 && (c != '\t' && c != '\r' && c != '\n')) {
                // Illegal XML character, even encoded. Skip or substitute
                sb.append("&#xfffd;");   // Unicode replacement character
            } else {
                switch(c) {
                  case '&':  sb.append("&amp;"); break;
                  case '>':  sb.append("&gt;"); break;
                  case '<':  sb.append("&lt;"); break;
                  // Uncomment next two if encoding for an XML attribute
//                  case '\''  sb.append("&apos;"); break;
//                  case '\"'  sb.append("&quot;"); break;
                  // Uncomment next three if you prefer, but not required
//                  case '\n'  sb.append("&#10;"); break;
//                  case '\r'  sb.append("&#13;"); break;
//                  case '\t'  sb.append("&#9;"); break;

                  default:   sb.append((char)c);
                }
            }
        } else if ((c >= 0xd800 && c <= 0xdfff) || c == 0xfffe || c == 0xffff) {
            // Illegal XML character, even encoded. Skip or substitute
            sb.append("&#xfffd;");   // Unicode replacement character
        } else {
            sb.append("&#x");
            sb.append(Integer.toHexString(c));
            sb.append(';');
        }
    }
    return sb.toString();
}

Editar: para aquellos que continúan insistiendo en que es una tontería escribir su propio código para esto cuando hay API de Java perfectamente buenas para tratar con XML, es posible que desee saber que la API de StAX incluida con Oracle Java 8 (no he probado otras ) no codifica correctamente el contenido CDATA: no escapa]]> secuencias en el contenido. Una biblioteca de terceros, incluso una que sea parte del núcleo de Java, no siempre es la mejor opción.


+1 para código independiente. Simplemente comparando su código con la implementación de guayaba , me pregunto ¿qué pasa con '\ t', '\ n', '\ r'? Ver también notas en guava docs
jschnasse

2
No hay necesidad de escapar \ n, \ r y \ t, son válidos, aunque hacen que el formateo sea un poco feo. Modifiqué el código para mostrar cómo escsapearlos si eso es lo que quieres.
Mike B

1
No hay forma de "escapar]]>" en CDATA.
kmkaplan

1
Entonces debería rechazar el contenido lanzando una IllegalArgumentException. Bajo ninguna circunstancia debe pretender tener éxito pero aún generar XML no válido.
Mike B

En lugar de reemplazar los caracteres ilegales en XML 1.0 con el carácter de sustitución Unicode, puede usar mis métodos aquí stackoverflow.com/a/59475093/3882565 .
stonar96

8

StringEscapeUtils.escapeXml()no escapa a los caracteres de control (<0x20). XML 1.1 permite caracteres de control; XML 1.0 no lo hace. Por ejemplo,XStream.toXML() felizmente serializará los caracteres de control de un objeto Java en XML, que un analizador XML 1.0 rechazará.

Para escapar de los caracteres de control con Apache commons-lang, use

NumericEntityEscaper.below(0x20).translate(StringEscapeUtils.escapeXml(str))

7
public String escapeXml(String s) {
    return s.replaceAll("&", "&amp;").replaceAll(">", "&gt;").replaceAll("<", "&lt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
}

5
El encadenamiento de replaceAllllamadas es muy ineficaz, especialmente para cadenas grandes. Cada llamada da como resultado la creación de un nuevo objeto String, que permanecerá hasta que se recoja la basura. Además, cada llamada requiere volver a recorrer la cadena. Esto podría consolidarse en un solo bucle manual con comparaciones con cada carácter objetivo en cada iteración.
daiscog

Esta debería ser la respuesta aceptada, incluso si es ineficiente. Resuelve el problema en una sola línea.
Stimpson Cat

Y tiene muchos errores. Vea este comentario arriba
David Balažic

Para corregir estos errores, también puede usar mi método aquí stackoverflow.com/a/59475093/3882565 . Tenga en cuenta que esto no es un reemplazo, pero se puede usar adicionalmente.
stonar96

6

Si bien el idealismo dice que use una biblioteca XML, en mi humilde opinión, si tiene una idea básica de XML, el sentido común y el rendimiento lo dicen todo el camino. Podría decirse que también es más legible. Aunque usar las rutinas de escape de una biblioteca probablemente sea una buena idea.

Considere esto: XML estaba destinado a ser escrito por humanos.

Utilice bibliotecas para generar XML cuando tenga su XML como un "objeto" que modele mejor su problema. Por ejemplo, si los módulos conectables participan en el proceso de construcción de este XML.

Editar: en cuanto a cómo escapar de XML en plantillas, el uso de CDATA o escapeXml(string)de JSTL son dos buenas soluciones, escapeXml(string)se pueden usar así:

<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

<item>${fn:escapeXml(value)}</item>

6

El comportamiento de StringEscapeUtils.escapeXml () ha cambiado de Commons Lang 2.5 a 3.0. Ahora ya no escapa a los caracteres Unicode superiores a 0x7f.

Esto es algo bueno, el método antiguo era estar un poco ansioso por escapar de las entidades que podrían simplemente insertarse en un documento utf8.

Los nuevos escapers que se incluirán en Google Guava 11.0 también parecen prometedores: http://code.google.com/p/guava-libraries/issues/detail?id=799


1
Aquí está el escalador XML de Guava: code.google.com/p/guava-libraries/source/browse/guava/src/com/… . En general, he descubierto que Guava tiene una mejor arquitectura que Apache Commons.
jhclark



5

Nota: Su pregunta es sobre escapar , no codificar . Escapar es usar <, etc. para permitir que el analizador distinga entre "esto es un comando XML" y "esto es un texto". La codificación es lo que especifica en el encabezado XML (UTF-8, ISO-8859-1, etc.).

En primer lugar, como todos los demás dijeron, use una biblioteca XML. XML parece simple pero la codificación + el material de escape es un vudú oscuro (que notarás tan pronto como encuentres diéresis y japonés y otras cosas raras como " dígitos de ancho completo " (& # FF11; es 1)). Mantener XML legible por humanos es una tarea de Sisyphus.

Sugiero que nunca intentes ser inteligente sobre la codificación de texto y el escape en XML. Pero no dejes que eso te impida intentarlo; solo recuerda cuando te muerda (y lo hará).

Dicho esto, si usa solo UTF-8, para hacer las cosas más legibles, puede considerar esta estrategia:

  • Si el texto contiene '<', '>' o '&', envuélvalo en <![CDATA[ ... ]]>
  • Si el texto no contiene estos tres caracteres, no lo deforme.

Estoy usando esto en un editor de SQL y permite a los desarrolladores cortar y pegar SQL desde una herramienta SQL de terceros en el XML sin preocuparse por escapar. Esto funciona porque el SQL no puede contener diéresis en nuestro caso, así que estoy a salvo.


5

Si bien estoy de acuerdo con Jon Skeet en principio, a veces no tengo la opción de usar una biblioteca XML externa. Y me parece peculiar que las dos funciones para escapar / no escapar de un valor simple (atributo o etiqueta, no documento completo) no están disponibles en las bibliotecas XML estándar incluidas con Java.

Como resultado y en base a las diferentes respuestas que he visto publicadas aquí y en otros lugares, aquí está la solución que terminé creando (nada funcionó como una simple copia / pegado):

  public final static String ESCAPE_CHARS = "<>&\"\'";
  public final static List<String> ESCAPE_STRINGS = Collections.unmodifiableList(Arrays.asList(new String[] {
      "&lt;"
    , "&gt;"
    , "&amp;"
    , "&quot;"
    , "&apos;"
  }));

  private static String UNICODE_NULL = "" + ((char)0x00); //null
  private static String UNICODE_LOW =  "" + ((char)0x20); //space
  private static String UNICODE_HIGH = "" + ((char)0x7f);

  //should only be used for the content of an attribute or tag      
  public static String toEscaped(String content) {
    String result = content;
    
    if ((content != null) && (content.length() > 0)) {
      boolean modified = false;
      StringBuilder stringBuilder = new StringBuilder(content.length());
      for (int i = 0, count = content.length(); i < count; ++i) {
        String character = content.substring(i, i + 1);
        int pos = ESCAPE_CHARS.indexOf(character);
        if (pos > -1) {
          stringBuilder.append(ESCAPE_STRINGS.get(pos));
          modified = true;
        }
        else {
          if (    (character.compareTo(UNICODE_LOW) > -1)
               && (character.compareTo(UNICODE_HIGH) < 1)
             ) {
            stringBuilder.append(character);
          }
          else {
            //Per URL reference below, Unicode null character is always restricted from XML
            //URL: https://en.wikipedia.org/wiki/Valid_characters_in_XML
            if (character.compareTo(UNICODE_NULL) != 0) {
              stringBuilder.append("&#" + ((int)character.charAt(0)) + ";");
            }
            modified = true;
          }
        }
      }
      if (modified) {
        result = stringBuilder.toString();
      }
    }
    
    return result;
  }

Lo anterior se adapta a varias cosas diferentes:

  1. evita el uso de lógica basada en caracteres hasta que sea absolutamente necesario: mejora la compatibilidad con Unicode
  2. intenta ser lo más eficiente posible dada la probabilidad es la segunda condición "si" es probablemente la vía más utilizada
  3. es una función pura; es decir, seguro para subprocesos
  4. optimiza muy bien con el recolector de basura al devolver solo el contenido del StringBuilder si algo realmente cambió; de lo contrario, se devuelve la cadena original

En algún momento, escribiré la inversión de esta función, toUnescaped (). Simplemente no tengo tiempo para hacer eso hoy. Cuando lo haga, actualizaré esta respuesta con el código. :)


Se ve muy bien para mí. No deseo agregar otro frasco a mi proyecto para un solo método. Si me concedes permiso, ¿puedo copiar y pegar tu código en el mío?
RuntimeException

1
@SatishMotwani Por supuesto, puede tomar el código anterior y hacer con él lo que quiera. Tengo entendido que se supone que cualquier código publicado en StackOverflow está libre de derechos de autor (no está cubierto como un trabajo en su totalidad). Por otro lado, sería extremadamente difícil para alguien presionar cualquier tipo de reclamo de derechos de autor y esperar una especie de resultado para sí mismo.
chaotic3quilibrium

1
Gracias por permitirme :-) Lo usaré.
RuntimeException

Olvidaste manejar los caracteres NUL. Y quizás otras cosas también.
David Balažic

@ DavidBalažic Está bien, por favor explique con más detalle lo que podría haberme perdido. Lea el código más detenidamente. Manejé CADA carácter Unicode (de los 1.111.998), incluido el nullcarácter. ¿Puede explicar la definición de los dos valores UNICODE_LOWy UNICODE_HIGH? Vuelva a leer el ifque usa esos dos valores. Observe null( \u0000cuál es (int)0) no cae entre estos dos valores. Lea cómo se "escapa" correctamente al igual que TODOS los caracteres Unicode que existen fuera del rango UNICODE_LOWy UNICODE_HIGH, utilizando la &#técnica.
chaotic3quilibrium


3

Si está buscando una biblioteca para hacer el trabajo, intente:

  1. Guava 26.0 documentado aquí

    return XmlEscapers.xmlContentEscaper().escape(text);

    Nota: también hay una xmlAttributeEscaper()

  2. Apache Commons Text 1.4 documentado aquí

    StringEscapeUtils.escapeXml11(text)

    Nota: también hay un escapeXml10()método


1

Esta es una solución fácil y también es ideal para codificar caracteres acentuados.

String in = "Hi Lârry & Môe!";

StringBuilder out = new StringBuilder();
for(int i = 0; i < in.length(); i++) {
    char c = in.charAt(i);
    if(c < 31 || c > 126 || "<>\"'\\&".indexOf(c) >= 0) {
        out.append("&#" + (int) c + ";");
    } else {
        out.append(c);
    }
}

System.out.printf("%s%n", out);

Salidas

Hi L&#226;rry &#38; M&#244;e!

¿No debería el "31" en la primera línea del "si" ser "32"? es decir, menos que el carácter de espacio? Y si debe permanecer "31", ¿no debería corregirse para que lea "si (c <= 31 || ..." (signo igual adicional después del signo menor que)?
caotic3quilibrium


1

Solo reemplaza

 & with &amp;

Y para otros personajes:

> with &gt;
< with &lt;
\" with &quot;
' with &apos;

0

Use JAXP y olvídese del manejo de texto, se hará automáticamente.


Su enlace está en español, lo que no es tan útil para la mayoría de nosotros. Mejor es este .
Vivit

0

Intente codificar el XML usando el serializador Apache XML

//Serialize DOM
OutputFormat format    = new OutputFormat (doc); 
// as a String
StringWriter stringOut = new StringWriter ();    
XMLSerializer serial   = new XMLSerializer (stringOut, 
                                          format);
serial.serialize(doc);
// Display the XML
System.out.println(stringOut.toString());

0

Esto es lo que encontré después de buscar en todas partes buscando una solución:

Obtenga la biblioteca Jsoup:

<!-- https://mvnrepository.com/artifact/org.jsoup/jsoup -->
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.12.1</version>
</dependency>

Luego:

import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Entities
import org.jsoup.parser.Parser

String xml = '''<?xml version = "1.0"?>
<SOAP-ENV:Envelope
   xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
   SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding">

   <SOAP-ENV:Body xmlns:m = "http://www.example.org/quotations">
      <m:GetQuotation>
         <m:QuotationsName> MiscroSoft@G>>gle.com </m:QuotationsName>
      </m:GetQuotation>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>'''



Document doc = Jsoup.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")), "UTF-8", "", Parser.xmlParser())
doc.outputSettings().charset("UTF-8")
doc.outputSettings().escapeMode(Entities.EscapeMode.base)

println doc.toString()

Espero que esto ayude a alguien

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.