2012-01-10 15 views
6

Escribí una herramienta para reparar algunos archivos XML (es decir, inserto algunos atributos/valores que faltaban) usando C# y Linq-to-XML. La herramienta carga un archivo XML existente en un objeto XDocument. Luego, analiza a través del nodo para insertar los datos faltantes. Después de eso, llama a XDocument.Save() para guardar los cambios en otro directorio.XDocument.Save() elimina mis entidades

Todo eso está bien excepto por una cosa: cualquier #xA; las entidades que están en el texto en el archivo XML se reemplazan con un nuevo carácter de línea. La entidad representa una nueva línea, por supuesto, pero necesito preservar la entidad en el XML porque otro consumidor lo necesita allí.

¿Hay alguna manera de guardar el XDocument modificado sin perder el & #xA; entidades?

Gracias.

+1

¿Se reemplaza cuando carga el documento anterior o cuando guarda el nuevo? –

+0

@Arnold: cuando guardo el nuevo. – mahdaeng

+0

La solución ideal sería corregir al consumidor de su XML, para que maneje XML correctamente. – svick

Respuesta

10

Las entidades 
 se denominan técnicamente "referencias de caracteres numéricos" en XML, y se resuelven cuando el documento original se carga en el XDocument. Esto hace que su problema sea problemático de resolver, ya que no hay forma de distinguir entidades de espacio en blanco resueltas de espacios en blanco insignificantes (normalmente se utiliza para formatear documentos XML para lectores de texto sin formato) después de cargar el XDocument. Por lo tanto, lo siguiente solo aplica si su documento no tiene espacios en blanco insignificantes.

La biblioteca System.Xml le permite a uno para conservar espacio en blanco entidades estableciendo la propiedad NewLineHandling de la clase XmlWriterSettings a Entitize. Sin embargo, dentro de los nodos de texto, esto solo daría derecho a \r a 
, y no a \n a 
.

La solución más fácil es derivar de la clase XmlWriter y anular su método WriteString para reemplazar manualmente los caracteres de espacios en blanco con sus entidades de caracteres numéricos. El método WriteString también pasa a ser el lugar donde .NET entitizes caracteres que no están permitidos para aparecer en los nodos de texto, tales como los marcadores de sintaxis &, <, y >, que están entitized respectivamente a &amp;, &lt;, y &gt;.

Dado que XmlWriter es abstracto, derivaremos de XmlTextWriter para evitar tener que implementar todos los métodos abstractos de la clase anterior. Aquí es una implementación rápida y sucia-:

public class EntitizingXmlWriter : XmlTextWriter 
{ 
    public EntitizingXmlWriter(TextWriter writer) : 
     base(writer) 
    { } 

    public override void WriteString(string text) 
    { 
     foreach (char c in text) 
     { 
      switch (c) 
      { 
       case '\r': 
       case '\n': 
       case '\t': 
        base.WriteCharEntity(c); 
        break; 
       default: 
        base.WriteString(c.ToString()); 
        break; 
      } 
     } 
    } 
} 

Si diseñado para su uso en un entorno de producción, que querría acabar con la parte c.ToString(), ya que es muy ineficiente. Puede optimizar el código agrupando subcadenas del text original que no contienen ninguno de los caracteres que desea titularizar y alimentarlas juntas en una sola llamada base.WriteString.

Una palabra de advertencia: La siguiente aplicación ingenua no funcionará, ya que la base WriteString método reemplazaría cualquier & caracteres con &amp;, causando con ello \r se expanda a &amp;#xA;.

public override void WriteString(string text) 
    { 
     text = text.Replace("\r", "&#xD;"); 
     text = text.Replace("\n", "&#xA;"); 
     text = text.Replace("\t", "&#x9;"); 
     base.WriteString(text); 
    } 

Finalmente, para salvar su XDocument en un archivo de destino o un arroyo, sólo tiene que utilizar el siguiente fragmento:

using (var textWriter = new StreamWriter(destination)) 
using (var xmlWriter = new EntitizingXmlWriter(textWriter)) 
    document.Save(xmlWriter); 

Espero que esto ayude!

Editar: Para referencia, aquí es una versión optimizada de la WriteString método anulado:

public override void WriteString(string text) 
{ 
    // The start index of the next substring containing only non-entitized characters. 
    int start = 0; 

    // The index of the current character being checked. 
    for (int curr = 0; curr < text.Length; ++curr) 
    { 
     // Check whether the current character should be entitized. 
     char chr = text[curr]; 
     if (chr == '\r' || chr == '\n' || chr == '\t') 
     { 
      // Write the previous substring of non-entitized characters. 
      if (start < curr) 
       base.WriteString(text.Substring(start, curr - start)); 

      // Write current character, entitized. 
      base.WriteCharEntity(chr); 

      // Next substring of non-entitized characters tentatively starts 
      // immediately beyond current character. 
      start = curr + 1; 
     } 
    } 

    // Write the trailing substring of non-entitized characters. 
    if (start < text.Length) 
     base.WriteString(text.Substring(start, text.Length - start)); 
} 
+0

Esta es una de las respuestas más exhaustivas que he visto. Voy a probar esto. Incluso si no funciona (y probablemente lo hará), obtendrá mi voto. Gracias, Douglas! – mahdaeng

+0

De nada :-) No olvide que lo anterior solo funcionará si no tiene espacios en blanco insignificantes en su XML fuente. Si tiene espacios en blanco insignificantes, le sugiero que use el código de la otra respuesta (abajo). – Douglas

0

Si el documento contiene espacios en blanco insignificantes que desea distinguir de sus &#xA; entidades, puede utilizar el siguiente (mucho más simple) solución: convierta temporalmente las referencias de caracteres &#xA; a otro carácter (que no esté ya presente en su documento), realice su procesamiento XML y luego conviértalo de nuevo en el resultado de salida. En el ejemplo siguiente, utilizaremos el carácter privado U+E800.

static string ProcessXml(string input) 
{ 
    input = input.Replace("&#xA;", "&#xE800;"); 
    XDocument document = XDocument.Parse(input); 
    // TODO: Perform XML processing here. 
    string output = document.ToString(); 
    return output.Replace("\uE800", "&#xA;"); 
} 

Tenga en cuenta que, dado que XDocument resuelve las referencias numéricas de caracteres a sus caracteres Unicode correspondientes, las entidades "&#xE800;" se habría resuelto a '\uE800' en la salida.

Normalmente, puede usar cualquier punto de código del "Área de uso privado" de Unicode (U+E000 - U+F8FF). Si desea estar más seguro, realice una comprobación de que el personaje ya no está presente en el documento; si es así, elige otro personaje de dicho rango. Como solo usarás el personaje de forma temporal e interna, no importa cuál uses. En el escenario muy poco probable de que todos los caracteres de uso privado ya estén presentes en el documento, genere una excepción; sin embargo, dudo que eso ocurra en la práctica.

Cuestiones relacionadas