Lendo arquivos texto em Java com Scanner

É frequente precisarmos ler arquivos textos para processá-los em lote. Eles estão nos mais variados formatos. Há muitos onde determinada coluna representa o fim de um campo (posicionais), ou cada campo é demarcado com um separador especial, como por barras, vírgulas, espaços ou tabs. Por exemplo, um arquivo que lista o nome, data de nascimento, cidade e número de compras de cada cliente:

Natanael Pantoja|Fortaleza|27
Paulo Silveira|São Paulo|35

Como fazer isso em Java? Se você buscar rapidamente, vai acabar caindo nas antigas classes BufferedReader e InputStreamReader, além de lotar seu código com String.indexOf, String.substring e os demais métodos utilitários de String e StringBuilder. Por exemplo:

FileInputStream stream = new FileInputStream("arquivo.txt");
InputStreamReader reader = new InputStreamReader(stream);
BufferedReader br = new BufferedReader(reader);
String linha = br.readLine();
while(linha != null) {
   String nome = linha.substring(0, linha.indexOf('|'));
   String cidade = linha.substring(linha.indexOf('|') + 1, linha.lastIndexOf('|'));
   String compras = linha.substring(linha.lastIndexOf('|') + 1, linha.length());
   System.out.println(nome);
   System.out.println(cidade);
   System.out.println(compras);
   linha = br.readLine();
}

Sim, um código como esse funciona, mas não é tão sucinto. Há ainda o problema de converter as compras para Integer. O código completo ficaria bastante verboso, com um Integer.parseInt(compras) por aí. Claro, ainda falta o correto tratamento de erros e alguns casos particulares, mas o código ilustra bem uma forma clássica de abordar esse problema. Como simplificar?

A classe java.util.Scanner, que existe desde o Java 5, acaba sendo preterida muitas vezes por falta de conhecimento. Ela permite trabalharmos com textos (não só de arquivos), de uma maneira diferente. Para começar, podemos ler todos os campos como String de maneira simples:

Scanner scanner = new Scanner(new FileReader("arquivo.txt"))
                       .useDelimiter("\\||\\n");
while (scanner.hasNext()) {
	String nome = scanner.next();
	String cidade = scanner.next();
	String compras = scanner.next();
	System.out.println(nome);
	System.out.println(cidade);
	System.out.println(compras);
}

Note que o código ficou mais curto. Usando o método Scanner.useDelimiter() podemos passar uma expressão regular para dizer qual caractere deve ser considerado o divisor de campos. Chamamos de token o que há entre dois delimitadores. Nesse caso dissemos que pode ser tanto a nova linha (\\n) quanto o pipe (\\|). Com isso podemos usar o método hasNext() para iterar em cada token e o next() para recuperar o token corrente.

Outra grande vantagem da classe Scanner é a possibilidade de conversão de tipo de forma menos verbosa. Por exemplo, podemos extrair a quantidade de compras fazendo um simples int compras = scanner.nextInt(). Claro, podemos ter um problema de conversão aqui.

Além disso, há metodos análogos. Como existe o nextInt, existe também o hasNextInt para saber se há um inteiro no próximo token. Existem muitos outros, como o scanner.nextBigDecimal(), scanner.nextBoolean(), etc, como você pode ver no JavaDoc da classe Scanner.

O recurso mais poderoso é poder utilizá-la em conjunto com as expressões regulares no Java. Há os métodos next(...) e hasNext(...) que recebem expressões regulares como parâmetros (sendo Strings ou Patterns), não precisando ficar preso ao useDelimiter. Outros métodos trabalham como expressões regulares e com a API do java.util.regex, como o match(), o findInLine() e o findWithinHorizon().

Dá até para ler um arquivo inteiro na memória com um simples código como esse:

String arquivo = 
  new Scanner(new File("compras.txt"))
             .useDelimiter("\\Z").next();

É claro você precisa tomar muito cuidado se não souber o quão grande esse arquivo pode ser. O melhor era já ir consumindo o arquivo através dos campos e quebras que for utilizar dentro da sua aplicação. Há o outro conselho óbvio: tomar cuidado com o fechamento dos recursos, fazendo o close() do Scanner.

Quais ferramentas você utiliza para trabalhar com textos e arquivos em Java? Quais seus problemas mais comuns?

22 Comentários

  1. João Víctor Rocon Maia 26/11/2012 at 14:56 #

    Opa, faz um tempo que não uso Java mas me lembro de ter testado o Scanner e BufferedReader antes de começar a usar um dos dois e percebi que o desempenho com o BufferedReader era bem melhor, isso mudou?

  2. Paulo Silveira 26/11/2012 at 15:01 #

    Realmente há um custo de performance, em especial se você trocar várias vezes de delimitador ou abusar de diferentes expressões regulares. Caso seja algo simples e com regras bem claras, a variação não é significativa.

    Além disso, como está escrito no artigo, você deve tomar cuidado com a memória. Next()s descuidados podem trazer Strings gigantes pra memória. Mesmo algumas tentativas inocentes, como tentar ver se determinado caractere está presente, faz com que o Scanner bufferize todo o arquivo até o final, caso ele não exista.

  3. João Víctor Rocon Maia 26/11/2012 at 15:21 #

    Show Paulo! O meu caso era sempre entrada até que simples e formatação bem definida, eu geral eu usava ou o split ou StringTokenizer para parsear tudo. Conhecendo o seu histórico e seu irmão vão entender isso pq usava em alguns problemas na Maratona de Programação, achava mais tranquilo resolver problemas que envolviam Strings e BigInt em Java do que C++.

  4. JP 26/11/2012 at 19:08 #

    Qual você recomenda para se trabalhar com regex?

  5. Bruno Laturner 27/11/2012 at 09:39 #

    Só um adendo sobre um erro muito comum, que já devo ter visto centenas de vezes no GUJ:

    A combinação de Scanner, System.in, Windows e mais de um nextInt em sequência, também conhecido como o modo clássico de entrada de dados via teclado no prompt do DOS.

    O problema é o seguinte:

    Ao entrar os dados de três números, A, B, C, em sequência, o programa vai ignorar a entrada de B, pulando direto para C, ou dando algum NumberFormatException.

    Isto acontece por que no Windows uma nova linha é denominada por um Carriage Return + Line Feed, que em Java é \r\n. Em Unix e Unix-like, o formato é \n, e nos Mac OSs antigos era \r.

    O que acontece é que o Scanner, por padrão usa somente um desses caracteres para reconhecer um inteiro no nextInt, e como o Windows escreve 2 caracteres, a 1ª linha com o número é processada até o \r, e a 2ª processa o \n, deixando a linha sem conteúdo.

    Para tratar esses casos, eu prefiro sempre usar o next, e parsear manualmente a String para o formato que quero.

  6. Fernando Boaglio 27/11/2012 at 17:21 #

    Para ler arquivos texto para fazer carga com algumas validações eu uso o http://supercsv.sourceforge.net/ , que é bem tranquilo de trabalhar.

  7. José Leitão 28/11/2012 at 14:53 #

    Valeu Panta!!!

  8. Raphael Almeida 28/11/2012 at 15:28 #

    @Paulo recentemente estou usando o Guava. Acabei precisando fazer um parser de arquivo para uma entidade e usei o Files .realines com processor. Ficou mto bacana.

    T com.google.common.io.Files.readLines(File file, Charset charset, LineProcessor callback)

  9. Paulo Silveira 28/11/2012 at 15:30 #

    Legal Raphael. Boa ideia de ter um LineProcessor. Da pra usar o Scanner dentro dele, ou um simples String.split dependendo do caso.

  10. Paulo Silveira 28/11/2012 at 15:31 #

    @Bruno, valeu os detalhes!

  11. Raphael Almeida 28/11/2012 at 15:35 #

    Files.readLines(arquivo, Charsets.UTF_8, new ProventoProcessor() );

    public class ProventoProcessor implements LineProcessor<List> {

    List result = new ArrayList();

    @Override
    public List getResult() {
    return result;
    }

    @Override
    public boolean processLine(String linha) throws IOException {

    return result.add(
    new Provento.Builder()
    .comMatricula(linha.substring(0, 5))
    .comNome(linha.substring(5, 24))
    .comCargo(linha.substring(24, 32))
    .comSituacao(linha.substring(32, 37))
    .comLotacao(linha.substring(37, 41))
    .build()
    );
    }

    }

  12. Luca Bastos 28/11/2012 at 15:42 #

    Usei Scanner(new BufferedInputStream(System.in)) no post http://blog.concretesolutions.com.br/2012/11/mapreduce-%E2%80%93-parte-ii/ para ler arquivo texto com < e pelos comentários fiquei até com medo. Mas aqui no OSX deu certo.

  13. Santos 29/11/2012 at 16:09 #

    Bacana a solução do Guava.

    Já usei um semelhante da Apache com o IOUtils

    LineIterator it = IOUtils.lineIterator(reader);
    while (it.hasNext()) {
    String line = it.nextLine();
    /// do something with line
    }

  14. Márcio Ferreira 05/12/2012 at 01:54 #

    estou trabalhando mais proximamente com leitura de arquivos csv agora e posso acrescentar tbm que o groovy para leitura de arquivos tbm é poderoso.:

    def file = new File(‘/home/foo/arquivo.csv’).splitEachLine(‘;’){
    }

    com esse metodo , eu ja posso definir qual vai ser o separador de campos do meu arquivo…

  15. Gustavo 22/01/2013 at 09:45 #

    Srs,

    Alguém poderia postar o código completo.

    Obrigado.

  16. Erivando 16/02/2013 at 02:10 #

    tentei seguir essa sua ideia, e o scanner.next() sempre vai me trazer o ultimo registro do arquivo que no caso são 2 um em cada linha do arquivo.

    tem como capturar todas as linhas?

  17. Leandro Prates 26/02/2013 at 15:30 #

    Nao vejo problema nenhum no primeiro modelo usar bufferReader ate porque voce pega a linha inteira do arquivo da um split e tem todos os elementos separados tranquilamente em vetor. Muito menos porco que no segundo exemplo.

  18. Paulo Silveira 26/02/2013 at 15:48 #

    Pois é, tem gente que ainda prefere indexOf do que expressões regulares e Scanner… estranho, mas válido.

  19. Rogério Koglin 24/03/2015 at 19:27 #

    Achei bem interessante a ideia, também li os comentários.
    Aproveito a oportunidade para deixar uma pergunta.

    Como criar uma rotina para parser de arquivos CSV com celulas vazias?

    Exemplo
    Nome; Tel; Celular; Endereço
    Fulano;;(xx) xxxx-xxxx;Rua a n°20
    Sicrano;(xx) xxxx-xxx;;Rua b n°50

  20. Vagner José 10/03/2016 at 03:13 #

    Muito boa essa ideia de utilizar as expressões regulares. No calor do momento a gente não pensa nessas coisas, apenas nas soluções clássicas.

  21. Halisson 17/03/2016 at 10:49 #

    Funcionou fácil e limpo.
    Obrigado

  22. Sandro 18/03/2019 at 12:54 #

    Aproveitando a ideia de leitura de arquivos, tenho uma duvida. Eu tenho um arquivo txt com o seguinte conteudo:
    2 2
    34 78
    89 -12
    @
    2 2
    67 76
    123 5
    Isso representa duas matrizes onde o “2 2” mostra o rank da matriz (2×2). O @ separa a primeira matriz da segunda. Eu preciso utilizar essas matrizes para fazer calculos. No meu codigo no Eclipse, eu ja consigo separar as matrizes mas nao consigo deixar de ler o rank delas (2×2) para fazer o calculo apenas com os elementos. Conseguem me ajudar?

Deixe uma resposta