Problema de encoding criando arquivo CSV

Boa tarde Srs,

Estou com um problema de encoding que está tirando minhas noites de sono… já pesquisei pra caramba, mas ainda não consegui achar onde estou pecando.

O problema é o seguinte, tenho uma rotina que exporta alguns dados do banco de dados (DB2, UTF-8 ) e gera um arquivo CSV, só que este arquivo chega pro usuário baixar como UTF-8 e o Excel não o interpreta corretamente.

Como se dá a exportação:

Basicamente, é criado um StringBuilder com construtor sem parâmetros, depois são concatenado os títulos das colunas com tokens de intercionalização para pt_BR (verifiquei o arquivo de tokens e segundo o Notepad++ o encoding dele é ANSI).
Depois adicionando registro por registro do banco à este StringBuilder e retornado um toString deste StringBuilder.

Aí começa o vilão da história (ou pelo menos quem eu acho que é o vilão).

Essa String passa por um tratamento (um tanto quanto de choque hehehe).

Segue o código deste tratamento… :

A chamada para o método de tratamento:

String csv2 = AUtil.autoEncode(csv.getBytes());
    public static String autoEncode(byte[] b) {
    	
    	String encodeString = "";
    	if (b == null) {
    		return encodeString;
    	}
    	
    	try {
		CharsetToolkit toolkit = new CharsetToolkit(b);			
		toolkit.setEnforce8Bit(true);
			
		if ("UTF-8".equals(toolkit.guessEncoding().name())) {
    			encodeString = new String(b, "UTF-8");
    		} else if ("ISO-8859-1".equals(toolkit.guessEncoding().name())) {
    			encodeString = new String(b, "ISO-8859-1");
    		} else { 
    			encodeString = new String(b);
    		}
    		
    	} catch (Exception e) {
    		Logger.error("[autoEncode] " + e.getMessage());
    	}
    	
    	return encodeString;
    }

O construtor da CharsetToolkit não faz nada, só atribui b a uma variável cde instância da classe, e o método guessEncoding tenta adivinhar qual o encoding está sendo usado (avaliando os 5 primeiros bytes), no meu ambiente Windows(pt_BR) ele retorna windows-1252 e no meu ambiente Linux(en_US) ele retorna ISO-8859-1.

O System.getProperty(“file.encoding”) no Windows retorna CP1252 e no Linux retorna ISO-8859-1.

Bom, continuando o processo, depois do tratamento nesta string, temos o código responsável por enviar a mesma para o usuário:

        response.addHeader("Cache-Control", "no-cache");
        response.addHeader("Pragma", "no-cache");
        response.addIntHeader("Expires", 0);        
        response.setContentType("application/csv");
	response.setHeader("Content-Disposition", "filename=arquivo.csv;");
	response.setContentLength(csv2.length());
	
        PrintWriter writer = response.getWriter();
	writer.print(csv2);
        writer.flush();
	writer.close();

Fiz alguns testes:

  • Removi a chamada do método, deixando apenas
String csv2 = csv;
  • Coloquei mensagens de debug para saber o estado do writer, que retornaram o seguinte resultado no ambiente Windows:
    • response.getLocale() retornou: “pt_BR”;
    • response.getCharacterEncoding() retornou: “ISO-8859-1”.

Isto tudo roda debaixo do tomcat6.0.14.
A versão 1 deste sistema roda um código semelhante a esse, considerando que a String não passa pelo tratamento do método autoEncode e exporta um CSV com encoding ANSI segundo o Notepad++ e é exibido perfeitamento pelo Excel. Acho que a diferença gritante entre as duas, é que não é o tomcat quem responde diretamente as requisições, e sim um Apache.

Preciso exportar de forma que o Excel reconheça, mas não posso forçar nada porque esse sistema roda em diversas plataformas e atende clientes de diversas localidades (até ásia).

Alguma sugestão?

Qual o problema com o toString() do StringBuilder que faz você querer fazer isso tudo?

Renato,

Esse é um código que estou dando manutenção, não entendi o propósito de todo este tratamento também. A principío achei que eles tivessem convertendo, mas de acordo com os testes em ambientes onde caiu em todas possibilidades do if-elseif-else e não houve diferença no resultado retornado.

Como citei acima, eu testei eliminar a chamada ao método que faz todo este tratamento e apenas considerar a String retornada pelo métotod toString() do StringBuilder, mas o problema persistiu, o CSV continuou vindo com caracteres como “acentuação” no lugar de acentos…

“acentuação” acontece quando um conjunto de bytes UTF-8 é decodificado como ISO-8859-1. Pelo o que entendi, os bytes UTF-8 do banco estão sendo decodificados erroneamente com o encoding do sistema, por alguma camada do código, talvez sua biblioteca de acesso ao banco. Use um depurador para visualizar as strings logo após serem recuperadas do banco.

Caso você não consiga de jeito nenhum trazer as strings do banco corretamente, você pode tentar isso antes de escrever o CSV:

csv = new String(csv.getBytes(), "UTF-8");

Renato,

O problema é exatamente este (a conversão de carácteres UTF-8 para ISO-8859-1), já tinha constatado isso mas obrigado pela ajuda.

Sobre sua sugestão do new String, vale lembrar que aquele método autoEncode faz exatamente isso, já testei fazer isso de 3 formas (UTF-8, ISO-8859-1 e windows-1252). Mas não funcionou.

O response.getWriter() retorna por default um Writer em ISO-8859-1… coloquei uma mensagem de debug e olha lá:

Logger.debug(response.getCharacterEncoding());

Produz saída “ISO-8859-1”.

Então… alterei com qual encoding o Writer deveria vir, usando este método:
http://java.sun.com/products/servlet/2.2/javadoc/javax/servlet/ServletResponse.html#setContentType(java.lang.String)

Beleza, veio em UTF-8 o Writer… só que o parse continua a dar problema na hora que abre o arquivo pelo Excel, ele deve ser burro e tenta transformar UTF-8 em ANSI ou algo que o valha, ao invés de lê-lo como UTF-8 mesmo.

Aí lembrei que eu já tinha testado isso, mesmo o Writer retornando ISO-8859-1, o arquivo chegava como UTF-8 segundo meu Notepad++.

Tentei utilizar o método encode/decode do Charset, mas sem muito sucesso…

Charset cs = Charset.forName("ISO-8859-1");
ByteBuffer bb = cs.encode(csv2);
writer.print(new String(bb.array()));

Sempre chega como UTF-8 aqui…

A solução mesmo acho que está em como converter carácteres UTF-8 para ISO-8859-1 de forma válida (que todos carácteres fiquem consistente), o problema é: como?

Ou então, será que tem como dizer pro Excel que ele tem que abrir aquele arquivo como UTF-8 direito?

Sugestões? :stuck_out_tongue:

1 curtida

Ah claro, só uma observação.

Acho que isso diz muito do porque sempre chega como UTF-8. Na verdade então o que eu preciso é uma forma de mandar para download os bytes, e não um toString dos bytes…

Renato,

Problema resolvido!

Ao invés de usar

PrintWriter writer = response.getWriter();
writer.print(csv);

que imprime apenas a String, portanto, sempre imprimirá como Unicode (segundo meu Notepad++, UTF-8 (sem bom)), passei a usar o

OutputStream out = response.getOutputStream(); out.write(byteArray);

que imprime os bytes, ou seja, imprime no encoding especificado.

Vale lembrar que o Charset dele será definido ou pelo response.setContentType ou será ISO-8859-1 (o default), como o meu caso é um arquivo para download, o Charset dele não influencia (na verdade, pelo que entendo, especifícar o Charset é para avisar ao browser o que está sendo enviado, para ele poder renderizar de maneira correta, ou seja, no caso de arquivo para download o browser não mete a mão).

Eu converti os bytes usando o seguinte:

Charset cs = Charset.forName(encode); ByteBuffer bb = cs.encode(csv);

Obs1.: encode é uma String que vem de um arquivo de propriedades.
Obs2.: ByteBuffer tem um método array(), que retorna um byte [] (para imprimir no out).

Usei este tópico como referência, ajudou bastante:
http://www.guj.com.br/posts/list/12456.java

Valeu Renato pela boa vontade e as boas dicas :smiley:

Leandro Del Sole

1 curtida

Olá a todos,

Estou tendo um problema que não envolve o encoding mas sim o tamanho do arquivo csv que
o usuário irá fazer o download.

Fiz um pouco diferente do leandrodelsole, tal que pego os caracteres de um textFieldArea para salvar.
Usei como base esse post também: http://www.guj.com.br/posts/list/75335.java

O código é o sequinte:

HttpServletResponse response = (HttpServletResponse) getExternalContext().getResponse();

response.setContentType("text/plain");
response.addHeader("Content-disposition", "attachment;filename=arquivo.csv");

 //carrega o streaming de saida que recebe os dados 
ServletOutputStream os = response.getOutputStream();

os.flush();

BufferedWriter out = new BufferedWriter(new OutputStreamWriter(os, "ISO-8859-1"));

//imprimindo uma linha de exemplo 
out.write(textAreaResultado.getText().toString());

//finaliza a exportação do arquivo 
out.flush();
out.close();

o log do tomcat infoma o seguinte erro, por exemplo, quando tento salvar um arquivo com 2897 linhas e 1727419 caractes

02/05/2010 15:42:57 org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet Faces Servlet threw exception
java.lang.IllegalStateException: Cannot forward after response has been committed
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:312)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:302)
at com.sun.faces.context.ExternalContextImpl.dispatch(ExternalContextImpl.java:408)
at com.sun.faces.application.ViewHandlerImpl.executePageToBuildView(ViewHandlerImpl.java:439)
at com.sun.faces.application.ViewHandlerImpl.renderView(ViewHandlerImpl.java:114)
at com.sun.rave.web.ui.appbase.faces.ViewHandlerImpl.renderView(ViewHandlerImpl.java:320)
at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:106)
at com.sun.faces.lifecycle.LifecycleImpl.phase(LifecycleImpl.java:251)
at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:144)
at com.sun.faces.extensions.avatar.lifecycle.PartialTraversalLifecycle.render(PartialTraversalLifecycle.java:106)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:245)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at com.sun.webui.jsf.util.UploadFilter.doFilter(UploadFilter.java:267)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:849)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:454)
at java.lang.Thread.run(Thread.java:619)

Aguardo
Obrigado

Bom dia Marlon,

Primeiramente, por ser um erro diferente, acho que você deveria abrir um novo tópico.

Bom, o exceção me pareceu bastente descritiva.
Joguei-a no google e veio este tópico do guj: http://www.guj.com.br/posts/list/29284.java

atente-se ao trecho

dê uma olhada se esse seu .flush() não é o problema hehehe

Pessoal, boa tarde.

Segundo dia tentando resolver isso, seguindo passos aqui gente… e talvez, até pelo cansaço, não estou mais conseguindo resultados.

Bom, é bem o contexto que o leandro colocou ae.

Segue o código:

                String conteudo = "";
		
		conteudo += "PIS,CPF,NOME TRABALHADOR,CNPJ,RAZÃO SOCIAL,DATA DE ENTRADA,DATA DE SAÍDA,SITUAÇÃO,TIPO MOVIMENTO\n";
		
		for (VinculoTO x : this.lista) {
			
			conteudo += x.getNuNit() + ",";
			conteudo += x.getCpf() + ",";
			conteudo += x.getNomeTrabalhador() + ",";
			conteudo += x.getNuDocIdentEstab() + ",";
			conteudo += x.getRazaoSocial() + ",";
			conteudo += x.getDataAdmissao() + ",";
			conteudo += x.getDataDesligamento() + ",";
			conteudo += x.getSituacao() + ",";
			conteudo += x.getTipoMovimento() + ",";
			conteudo += "\n";
			
		}
		
		
		HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
		
		response.addHeader("Cache-Control", "no-cache");
		response.addHeader("Pragma", "no-cache");
		response.setContentType("text/csv");
		response.setHeader("Content-disposition", "attachment;filename=arquivo.csv;");
  
		
		OutputStream out;
		try {
			out = response.getOutputStream();
			
			out.write(conteudo.getBytes(), 0, conteudo.getBytes().length);  
			out.flush();  
			out.close();
		
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}  
		
		response.setContentLength(conteudo.getBytes().length);

No início, monto a string “conteudo” para depois ser jogada no arquivo e este arquivo ser disponibilizado para ABRIR ou BAIXAR.

E o erro é igual. Os nomes com acentos ae, por ex., Razão Social, Situação, etc… estão vindo no arquivo em forma de códigos

  • DATA DE SAÍDA
  • RAZÃO SOCIAL
  • SITUAÇÃO

Fora isso, a planilha gerada está vindo, além das informações que coloquei na string, vem também com o código HTML da pagina atual. Como se nesse meio de campo ae… o código HTML tivesse se juntado ao conteúdo da minha String.

Ja tente CharSet, ja tentei response.setContentType… colocando tudo para “UTF-8”… mas nada.

Alguma dica a mais, pessoal?

Agradeço.

Bom dia ugocavalcanti,

Bom, faz muito tempo que fiz o post original, mas vamos ver se consigo te ajudar.

Acho que o primeiro passo que você tem que dar, é conseguir entender o porque e eliminar o HTML que vai junto com seu arquivo.
Veja se resetando o response, escrevendo o arquivo e depois sinalizando ele como completo, funciona:

final String characterEncoding = externalContext.getResponseCharacterEncoding(); externalContext.responseReset(); externalContext.setResponseCharacterEncoding(characterEncoding); externalContext.getResponseOutputWriter().write(<conteudo_do_seu_arquivo>); FacesContext.getCurrentInstance().responseComplete();

Vejo que vc está usando Faces, então acredito que possa testar como sua aplicação se comporta usando a lib Omni Faces para enviar o arquivo:
http://wiki.omnifaces.googlecode.com/hg/javadoc/1.6/org/omnifaces/util/Faces.html#sendFile(byte[], java.lang.String, boolean)
(Se rolar para baixo, há 3 assinaturas diferentes do método sendFile).

No seu trecho de código não vi a conversão dos bytes da String, isto pode fazer diferença:

Charset cs = Charset.forName(encode); ByteBuffer bb = cs.encode(csv);

Outros pontos que valem a pena citar é:

  • vc consegue garantir que antes do envio do arquivo, a String montada em seu código está com o encoding correto? (vc pode garantir isso debugando ou printando a String);
  • o encoding do Sistema Operacional, do processo Java e do editar de texto que abre o arquivo também influenciam, então atente-se à isso também.

Boa sorte!

Leandro,

esse atributo “externatContext” é no tipo javax.faces.context.ExternalContext? (ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext():wink:

Pq aqui ele não reconheceu os método responseReset(), nem o getResponseOutputWriter().

Quanto aos outros questionamentos… eu cheguei a usar o ByteBuffer, setando o Charset.forName(“UTF-8”) antes… mas não tive sucesso.

Quando eu printo a String no console ANTES de declarar e manipular o response, a string está perfeita… com acentos, tudo normal.

Em um dos meus testes, coloquei tudo q podia(e que eu sabia) como UFT-8, tanto no response.setContentType(), quanto no Charset que vc sugeriu aí. Ao abrir no LibreOffice, ele ainda pergunta qual o conjunto de caracteres que eu quero importar… e eu escolho “UNICODE (UTF-8)”

Ainda assim… o mesmo problema. Que bronca!

Estou com esperança de testar essa sua ideia de sinalizar o response como completo e ver se pelo menos o HTML deixar de ser concatenado com a minha string de dados. Mas realmente não encontrei aqueles 2 métodos.

Qualquer sugestão, estou aqui, na luta.

Valeu cara.

Sim, é este tipo:
http://docs.oracle.com/javaee/6/api/javax/faces/context/ExternalContext.html
Porém, é Java EE 6 que uso, você deve estar usando o 5 :S

De qualquer forma, se o HTML estiver sendo appendado DEPOIS do seu CSV, você talvez não precise resetar o response. Esta era só uma forma de garantir.
Mas ainda assim, a sinalização do response como completo é indispensável. Para isso, você pode usar o método responseComplete do FacesContext.

Para o método getResponseOutputWriter(), tem o:
FacesContext.getCurrentInstance().getResponseWriter().getCharacterEncoding()

Ao abrir o arquivo no libre office, você tentou abrir como “ISO-8859-1” ? Se funcionar com este encoding, já é um indicador de como ele está sendo escrito.

Já tentou forçar o processo Java a usar UTF-8 com o parâmetro de inicialização da JVM -Dfile.encoding=utf8 ?

Abraços

Leandro,

Inacreditável… mas a unica linha de código que resolveu o problema foi essa:

FacesContext.getCurrentInstance().responseComplete();

Colo o código abaixo para que compare com o mesmo trecho de código que coloquei inicialmente e notem q apenas incluí o responseComplete() no fim. Outras mudanças foram apenas na apresentação dos dados, no foreach.

Tanto o texto HTML que vinha junto com a String, quanto os códigos que vinha no lugar dos acentos foram resolvidos.

Agradeço a você Leandro pela ajuda fundamental. Valeu


String conteudo = "";
		
		conteudo += "PIS,CPF,NOME TRABALHADOR,CNPJ,RAZÃO SOCIAL,DATA DE ENTRADA,DATA DE SAÍDA,SITUAÇÃO,TIPO MOVIMENTO\n";
		
		for (VinculoTO v : this.lista) {
			
			conteudo += v.getNuNit() == 0 ? "Não encontrado," : acertos.getNuNit() + ",";
			conteudo += v.getCpf() == null? "Não encontrado," : acertos.getCpf() + ",";
			conteudo += v.getNomeTrabalhador() == null? "Não encontrado," : acertos.getNomeTrabalhador() + ",";
			conteudo += v.getNuDocIdentEstab() == null? "Não encontrado," : acertos.getNuDocIdentEstab() + ",";
			conteudo += v.getRazaoSocial() == null? "Não encontrado," : acertos.getRazaoSocial() + ",";
			conteudo += v.getDataAdmissao() == null? "Não encontrado," : acertos.getDataAdmissao() + ",";
			conteudo += v.getDataDesligamento() == null? "," : acertos.getDataDesligamento() + ",";
			conteudo += v.getSituacao() == null? "Não encontrado," : acertos.getSituacao() + ",";
			conteudo += v.getTipoMovimento() == 1? "Admissão," : "Desligamento,";
			conteudo += "\n";
			
		}
		
		HttpServletResponse response = (HttpServletResponse) FacesContext.getCurrentInstance().getExternalContext().getResponse();
		
		response.addHeader("Cache-Control", "no-cache");
		response.addHeader("Pragma", "no-cache");
		response.setContentType("text/csv");
		response.setHeader("Content-disposition", "attachment;filename=arquivo.csv;");
		OutputStream out;
		try {
			
			out = response.getOutputStream();
			
			out.write(conteudo.getBytes(), 0, conteudo.getBytes().length);
			out.flush();  
			out.close();
			
			FacesContext.getCurrentInstance().responseComplete();
		
		} catch (IOException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}