En este artículo no voy a contar nada muy puntero ni novedoso, además seguramente mucho de vosotros ya conocéis, pero yo no conocía hasta hace unos días. El objetivo es compartir mi experiencia con la marca de orden de bytes y poder ayudar a alguién que se encuentre en la misma situación que yo hace unos días.
Por unas cosas o por otras, pocas veces desde que llevo programando, y van ya 13 años, he tenido la necesidad de tener que enfrentarme a creación de ficheros manualmente utilizando c#. Esta semana ha tocado y me he encontrado con algo que desconocia hasta ahora, la marca de orden de bytes.
En este artículo voy a contar los problemas que me he encontrado y lo que he aprendido en el camino.
Requerimientos y problema detectado
Sin entrar en muchos detalles, que no son necesarios para entender el problema, en mi empresa teniamos la necesidad de generar un fichero separado por tabuladores que después se iba a importar en una herramienta de terceros.Clase que se encargará de crear los bytes
En nuestro proyecto tenemos una clase que se encarga de crear los bytes. Tiene métodos para añadir campos, lineas y para generar los bytes del fichero.public class DelimitedFile : IDelimitedFile { private string output = string.Empty; private Queue<byte> fifo = null; private string delimitedCharacter; public DelimitedFile(string delimitedCharacter) { fifo = new Queue<byte>(); this.delimitedCharacter = delimitedCharacter; } public void Empty() { fifo = new Queue<byte>(); } public void InsertField(string field) { if(!String.IsNullOrEmpty(field)) { // First, sanitize the data to clean characters // new line field = Regex.Replace(field, @"\r\n?|\n", String.Empty); // delimitator field = Regex.Replace(field, delimitedCharacter, String.Empty); } output = string.Concat(output, field + delimitedCharacter); } public void InsertLine() { output = string.Concat(output, "\n"); Enqueue(); } public void Enqueue() { byte[] bytes = Encoding.UTF8.GetBytes(output); foreach (var b in bytes) fifo.Enqueue(b); output = string.Empty; } public byte[] CreateFile() { if (output != string.Empty) Enqueue(); byte[] bytes = fifo.ToArray(); Empty(); return bytes; } }
Problemas con herramienta externa y excel
El problema detectado es que uno de los campos tiene una longitud máxima en la herramienta de terceros de 120 caracteres. Nos estaba dando error por superar esta longitud, pero aparentemente estaba bien controlado esta longitud en nuestro código antes de generar el fichero. Una de las pruebas que hice fue abrir el fichero con excel mediante el asistente de ficheros de texto y mi sorpresa fue ver que dentro de ese campo habia una palabra que debería ser nüvi que el asistente interpretaba como nüvi, añadiendo así un carácter más y superando la longitud. Además me doy cuenta de que en el origen del fichero en el asistente indica Windows (Ansi) y no UTF-8 como debería.Probando con otras herramientas
Decido probar a abrir el fichero con otras herramientas para ver si entendían la códificación utf-8 correctamente. Pruebo con Notepad++ y con UltraEdit. Con estás herramientas se interpreta correctamente la palabra nüvi y reconoce la codificación como utf-8. Por lo tanto la herramienta de terceros debe utilizar excel o algo que se comporta de forma similar.Analizando código heredado
Viendo la clase que se encarga de generar el fichero se observa claramente que la codificación utilizada para generar los bytes es utf-8, sin embargo se producía este problema. Así que me decido a investigar por la red acabando como siempre en el querido Stackoverflow. Es en esta web donde empiezo a leer en algunos post y respuestas la palabra BOM, que significa marca de orden de bytes.Creando test que me ayudan a entender el problema
Me decido a crear un test sobre DelimitedFile que genere los bytes, para después verificar la codificación. Fijaros que en el test me creo un StreamReader y en el constructor le indico al segundo parámetro Encoding.Unicode y al segundo true. Con esto le estoy indicando que quiero que busque la marca de orden de bytes y si no la encuentra que me devuelva unicode como codificación, para después verificar que sea utf-8.[TestMethod] public void CreateFileBytes_EncodingShouldBeUTF8() { DelimitedFile delimitedFile = new DelimitedFile("\t"); delimitedFile.InsertField("nüvi"); byte[] fileBytes = delimitedFile.CreateFile(); Stream stream = new MemoryStream(fileBytes); StreamReader sr = new StreamReader(stream, Encoding.Unicode,true); //detect encoding sr.ReadToEnd(); Encoding currentEncoding = sr.CurrentEncoding; Assert.AreEqual(Encoding.UTF8, currentEncoding); }
El test no pasa.
Entendiendo la marca de bytes
Después de que el test me falle me centro en entender mejor el BOM, son unos bytes que deben preceder a los bytes del fichero y ayuda al que lee los bytes a interpretar mejor la codificación. Buscando de nuevo por internet acabo en este artículo de la documentación de Microsoft donde ya entiendo los que debo de hacer. La marca de orden de bytes no se inclue por defecto, debo incluirla yo y la clase encoding tiene una propiedad GetPreamble que da esos bytes que necesito.Modificando la clase que se encargará de crear los bytes
Modifico la clase que crea los bytes para que al inicio escriba el preamble de bytes (BOM).public class DelimitedFile : IDelimitedFile { private string output = string.Empty; private Queue<byte> fifo = null; private string delimitedCharacter; private Encoding encoding = new UTF8Encoding(true); public DelimitedFile(string delimitedCharacter) { fifo = new Queue<byte>(); EnqueueBOM(); this.delimitedCharacter = delimitedCharacter; } public void Empty() { fifo = new Queue<byte>(); } public void InsertField(string field) { if(!String.IsNullOrEmpty(field)) { // First, sanitize the data to clean characters // new line field = Regex.Replace(field, @"\r\n?|\n", String.Empty); // delimitator field = Regex.Replace(field, delimitedCharacter, String.Empty); } output = string.Concat(output, field + delimitedCharacter); } public void InsertLine() { output = string.Concat(output, "\n"); Enqueue(); } public void Enqueue() { byte[] bytes = encoding.GetBytes(output); foreach (var b in bytes) fifo.Enqueue(b); output = string.Empty; } public void EnqueueBOM() { //https://en.wikipedia.org/wiki/Byte_order_mark byte[] bytes = encoding.GetPreamble(); foreach (var b in bytes) fifo.Enqueue(b); output = string.Empty; } public byte[] CreateFile() { if (output != string.Empty) Enqueue(); byte[] bytes = fifo.ToArray(); Empty(); return bytes; } }
No hay comentarios:
Publicar un comentario