Acelerando o AdvPL – Lendo arquivos TXT

Introdução

Alguém me perguntou na sexta-feira, qual era o método mais rápido de ler um arquivo TXT, que utilizava apenas o código ASCII 13 (CR) como quebra de linha… Normalmente eu mesmo usaria as funções FT_Fuse() e FT_FReadLn() para ler o arquivo … mas estas funções não permitem especificar o caractere de quebra… elas trabalham com arquivos TXT onde a quebra pode ser CRLF ou apenas LF.

Como eu não lembrava quem tinha me perguntado, e não achei a pergunta no WhatsApp, e-mail, FaceBook, etc…. resolvi postar a resposta no Facebook mesmo, explicando duas alternativas rápidas de leitura, uma consumindo mais memória e lendo o arquivo inteiro, e outra para arquivos sem limite de tamanho, e consumindo menos memória. Como houve interesse no assunto e diversos comentários, resolvi fazer uma classe de exemplo da segunda abordagem, partindo das premissas do melhor desempenho versus o menor consumo de memória possível.

Fonte da Clase ZFWReadTXT

#include 'protheus.ch'
/* ======================================================================
 Classe ZFWReadTXT
 Autor Júlio Wittwer
 Data 17/10/2015
 Descrição Método de leitura de arquivo TXT
Permite com alto desempenho a leitura de arquivos TXT
 utilizando o identificador de quebra de linha definido
 ====================================================================== */
#define DEFAULT_FILE_BUFFER 4096
CLASS ZFWReadTXT FROM LONGNAMECLASS
  DATA nHnd as Integer
  DATA cFName as String
  DATA cFSep as String
  DATA nFerror as Integer
  DATA nOsError as Integer
  DATA cFerrorStr as String
  DATA nFSize as Integer
  DATA nFReaded as Integer
  DATA nFBuffer as Integer
  DATA _Buffer as Array
  DATA _PosBuffer as Integer
  DATA _Resto as String
  // Metodos Pubicos
  METHOD New()
  METHOD Open()
  METHOD Close()
  METHOD GetFSize()
  METHOD GetError()
  METHOD GetOSError()
  METHOD GetErrorStr()
  METHOD ReadLine()
  // Metodos privados
  METHOD _CleanLastErr()
  METHOD _SetError()
  METHOD _SetOSError()
ENDCLASS
METHOD New( cFName , cFSep , nFBuffer ) CLASS ZFWReadTXT
DEFAULT cFSep := CRLF
DEFAULT nFBuffer := DEFAULT_FILE_BUFFER
::nHnd := -1
::cFName := cFName
::cFSep := cFSep
::_Buffer := {}
::_Resto := ''
::nFSize := 0
::nFReaded := 0
::nFerror := 0
::nOsError := 0
::cFerrorStr := ''
::_PosBuffer := 0
::nFBuffer := nFBuffer
Return self

METHOD Open( iFMode ) CLASS ZFWReadTXT
DEFAULT iFMode := 0
::_CleanLastErr()
If ::nHnd != -1
 _SetError(-1,"Open Error - File already open")
 Return .F.
Endif
// Abre o arquivo
::nHnd := FOpen( ::cFName , iFMode )
If ::nHnd < 0
 _SetOSError(-2,"Open File Error (OS)",ferror())
Return .F.
Endif
// Pega o tamanho do Arquivo
::nFSize := fSeek(::nHnd,0,2)
// Reposiciona no inicio do arquivo
fSeek(::nHnd,0)
Return .T.
METHOD Close() CLASS ZFWReadTXT
::_CleanLastErr()
If ::nHnd == -1
 _SetError(-3,"Close Error - File already closed")
Return .F.
Endif
// Close the file
fClose(::nHnd)
// Clean file read cache 
aSize(::_Buffer,0)
::_Resto := ''
::nHnd := -1
::nFSize := 0
::nFReaded := 0
::_PosBuffer := 0
Return .T.
METHOD ReadLine( /*@*/ cReadLine ) CLASS ZFWReadTXT
Local cTmp := ''
Local cBuffer
Local nRPos
Local nRead
// Incrementa o contador da posição do Buffer
::_PosBuffer++
If ( ::_PosBuffer <= len(::_Buffer) )
 // A proxima linha já está no Buffer ...
 // recupera e retorna
 cReadLine := ::_Buffer[::_PosBuffer]
 Return .T.
Endif
If ( ::nFReaded < ::nFSize )
  // Nao tem linha no Buffer, mas ainda tem partes
  // do arquivo para ler. Lê mais um pedaço
  nRead := fRead(::nHnd , @cTmp, ::nFBuffer)
  if nRead < 0
    _SetOSError(-5,"Read File Error (OS)",ferror())
    Return .F.
  Endif
  // Soma a quantidade de bytes lida no acumulador
  ::nFReaded += nRead
  // Considera no buffer de trabalho o resto
  // da ultima leituraa mais o que acabou de ser lido
  cBuffer := ::_Resto + cTmp
  // Determina a ultima quebra
  nRPos := Rat(::cFSep,cBuffer)
  If nRPos > 0
    // Pega o que sobrou apos a ultima quegra e guarda no resto
    ::_Resto := substr(cBuffer , nRPos + len(::cFSep))
    // Isola o resto do buffer atual
    cBuffer := left(cBuffer , nRPos-1 )
  Else
    // Nao tem resto, o buffer será considerado inteiro
    // ( pode ser final de arquivo sem o ultimo separador )
    ::_Resto := ''
  Endif
 // Limpa e Recria o array de cache
 // Por default linhas vazias são ignoradas
 // Reseta posicionamento de buffer para o primeiro elemento 
 // E Retorna a primeira linha do buffer 
 aSize(::_Buffer,0)
 ::_Buffer := StrTokArr2( cBuffer , ::cFSep )
 ::_PosBuffer := 1
 cReadLine := ::_Buffer[::_PosBuffer]
 Return .T.
Endif
// Chegou no final do arquivo ...
::_SetError(-4,"File is in EOF")
Return .F.
METHOD GetError() CLASS ZFWReadTXT
Return ::nFerror
METHOD GetOSError() CLASS ZFWReadTXT
Return ::nOSError
METHOD GetErrorStr() CLASS ZFWReadTXT
Return ::cFerrorStr
METHOD GetFSize() CLASS ZFWReadTXT
Return ::nFSize
METHOD _SetError(nCode,cStr) CLASS ZFWReadTXT
::nFerror := nCode
::cFerrorStr := cStr
Return
METHOD _SetOSError(nCode,cStr,nOsError) CLASS ZFWReadTXT
::nFerror := nCode
::cFerrorStr := cStr
::nOsError := nOsError
Return
METHOD _CleanLastErr() CLASS ZFWReadTXT
::nFerror := 0
::cFerrorStr := ''
::nOsError := 0
Return

Como funciona

No construtor da classe (método NEW), devemos informar o nome do arquivo a ser aberto, e opcionalmente podemos informar quais são os caracteres que serão considerados como “quebra de linha”. Caso não especificado, o default é CRLF — chr(13)+chr(10). Mas pode ser especificado também apenas Chr(13) (CR) ou Chr(10) (LF). E, como terceiro parâmetro, qual é o tamanho do cache em memória que a leitura pode utilizar. Caso não informado, o valor default são 4 KB (4096 bytes). Caso as linhas de dados do seu TXT tenha em média 1 KB, podemos aumentar seguramente este número para 8 ou 16 KB, para ter um cache em memória por evento de leitura em disco de pelo menos 8 ou 16 linhas.

A idéia é simples: Na classe de encapsulamento de leitura, a linha é lida por referência pelo método ReadLine(), que pode retornar .F. em caso de erros de acesso a disco, ou final de arquivo (todas as linhas já lidas). A abertura do arquivo apenas determina o tamanho do mesmo, para saber quanto precisa ser lido até o final do arquivo. A classe mantém um cache de linhas em um array, e uma propriedade para determinar o ponto atual do cache. Ele começa na primeira linha.

Na primeira leitura, o array de cache está vazio, bem como um buffer temporário chamado “_resto”. A primeira coisa que a leitura faz é incrementar o ponteiro do cache e ver se o ponteiro não passou o final do cache. Como na primeira leitura o cache está vazio, a próxima etapa é verificar se ainda falta ler alguma coisa do arquivo.

Caso ainda exista dados para ler do arquivo, um bloco de 4 KB é lido para a memória, em um buffer de trabalho montado com o resto da leitura anterior (que inicialmente está vazio) mais os dados lidos na operação atual. Então, eu localizo da direita para a esquerda do buffer a ultima quebra do buffer lido, guardo os dados depois da última quebra identificada no buffer “_resto” e removo estes dados do buffer atual de trabalho.

Desse modo, eu vou ter em memória um buffer próximo do tamanho máximo do meu cache (4 KB), considerando a última quebra encontrada neste buffer. Basta eu transformá-lo em um array usando a função StrTokArr2, guardar esse array na propriedade “_Buffer” da classe, e retornar a primeira linha lida.

Quando o método ReadLine() for chamado novamente, o cache vai estar alimentado com “N” linhas do arquivo, eu apenas movo o localizador do cache uma unidade pra frente, e se o localizador ainda está dentro do array, eu retorno a linha correspondente. Eu nem preciso me preocupar em limpar isso a cada leitura, afinal a quantidade de linhas em cache vai ocupar pouco mais de 4 KB mesmo … eu somente vou fazer acesso a disco, e consequente limpeza e realimentação desse cache quando o cache acabar, e ainda houver mais dados no arquivo a ser lido.

Desempenho

Peguei um arquivo TXT aleatório no meu ambiente, que continha um LOG de instalação de um software. O arquivo têm 1477498 bytes, com 12302 linhas de texto. O arquivo foi lido inteiro e todas as linhas identificadas entre 39 e 42 milissegundos (0,039 a 0,042 segundos). Resolvi fazer um outro fonte de testes, lendo este arquivo usando FT_FUSE e FT_FREADLN. Os tempos foram entre 51 e 55 milissegundos (0,051 a 0,055 segundos). E, usando a classe FWFileReader(), os tempos foram entre 101 e 104 milissegundos (0,101 e 0,104 segundos).

Repeti o mesmo teste com um arquivo suficientemente maior, 50 mil linhas e 210 MB .. os tempos foram:

FWFileReader ...... 2,937 s.
FT_FreadLN ........ 1,233 s.
ZFWReadTXT ........ 0,966 s.

Conclusão

Ganhar centésimos de segundo pode parecer pouco … mas centésimos de segundos podem significar uma largada na “pole position”, ou largar na 5a fila … Em uma corrida de 60 voltas, um segundo por volta torna-se um minuto. A máxima do “mais com menos” permanece constante. Quanto menos ciclos de máquina você consumir para realizar um processamento, mais rápido ele será. Quando falamos em aplicações com escalabilidade “estrelar”, para milhões de requisições por segundo, onde temos um limite natural de número de processos dedicados por máquina, quanto mais rápido cada processo for executado, maior “vazão” de processamento pode ser obtido com a mesma máquina. Principalmente quando entramos em ambientes “Cloud”, mesmo tendo grande disponibilidade de processamento, normalmente você será tarifado proporcionalmente ao que consumir. Chegar ao mesmo resultado com menos consumo de recurso fará a diferença 😀

Nos próximos tópicos (aceito sugestões), vou procurar abordar as questões de “como fazer mais rápido” determinada tarefa ou processo, acredito que esta abordagem ajuda a aproximar todos os conceitos de escalabilidade e desempenho que foram abordados apenas na teoria nos posts iniciais sobre este tema !

Agradeço a todos pela audiência do Blog, e não sejam tímidos em dar sugestões, basta enviar a sua sugestão de post siga0984@gmail.com 😀 Para dúvidas pertinentes ao assunto do post, basta postar um comentário no post mesmo 😉

E, pra finalizar, segue abaixo o fonte de teste de desempenho utilizado:

#include 'protheus.ch'
#define TEXT_FILE '\meuarquivo.txt'
/* ======================================================================
Função U_LeFile1, 2 e 3()
Autor Júlio Wittwer
Data 17/10/2015
Descrição Fontes de teste comparativo de desempenho de leitura de arquivo TEXTO
U_LeFile1() - Usa ZFWReadTXT
U_LeFile2() - Usa FT_FREADLN
U_LeFile3() - Usa FWFileReader
====================================================================== */
User Function LeFile1()
Local oTXTFile
Local cLine := ''
Local nLines := 0
Local nTimer
nTimer := seconds()
oTXTFile := ZFWReadTXT():New(TEXT_FILE)
If !oTXTFile:Open()
  MsgStop(oTXTFile:GetErrorStr(),"OPEN ERROR")
  Return
Endif
While oTXTFile:ReadLine(@cLine)
  nLines++
Enddo
oTXTFile:Close()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using ZFWReadTXT")
Return
User Function LeFile2()
Local nTimer
Local nLines := 0
nTimer := seconds()
FT_FUSE(TEXT_FILE)
While !FT_FEOF()
  cLine := FT_FReadLN()
  FT_FSkip()
  nLines++
Enddo
FT_FUSE()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using FT_FReadLN")
Return
User Function LeFile3()
Local nTimer
Local nLines := 0
Local oFile
nTimer := seconds()
oFile := FWFileReader():New(TEXT_FILE)
If !oFile:Open()
  MsgStop("File Open Error","ERROR")
  Return
Endif
While (!oFile:Eof())
  cLine := oFile:GetLine()
  nLines++
Enddo
oFile:Close()
MsgInfo("Read " + cValToChar(nLines)+" line(s) in "+str(seconds()-nTimer,12,3)+' s.',"Using FWFileReader")
Return
Anúncios

12 comentários sobre “Acelerando o AdvPL – Lendo arquivos TXT

    • Perfeito 😀 Com o fonte alterado, setando o buffer de arquivo para 4KB, o tempo baixou no meu ambiente para 1,295 segundos ! A classe trabalha com o buffer default de 1 KB. Vide fonte abaixo com o código inserido antes de fazer a leitura da tabela.

      User Function LeFile3()
      Local nTimer
      Local nLines := 0
      Local oFile
      nTimer := seconds()
      oFile := FWFileReader():New(TEXT_FILE)
      If !oFile:Open()
      MsgStop(“File Open Error”,”ERROR”)
      Return
      Endif
      // Aumenta o buffer default de leitur de arquivo de 1K para 4K
      oFile:setBufferSize(4096)
      While (!oFile:Eof())
      cLine := oFile:GetLine()
      nLines++
      Enddo
      oFile:Close()
      MsgInfo(“Read ” + cValToChar(nLines)+” line(s) in “+str(seconds()-nTimer,12,3)+’ s.’,”Using FWFileReader”)
      Return

      Curtir

  1. Eu estava usando as funções FT_* para fazer a leitura de um CSV quando me falaram da classe FWFileReader. Dei uma lida no Post e já estava quase desistindo de usar a classe, quando vi no TDN que ela também tem o método getAllLines(), que lê o arquivo e joga tudo para um array.
    Embora eu não tenha parado para medir a diferença de tempo, fiz o teste usando este método e senti que o desempenho ficou bem melhor que com os tradicionais FT_* 😀
    Abs

    Curtido por 1 pessoa

    • Legal !! Realmente, as funções FT_F* lêem um pedaço de 1 KB do arquivo, localizam a quebra de linha e desprezam o resto, lendo sempre de 1 em 1 KB. Usando a FWFileReader(), a leitura é feita em blocos maiores, sem desprezar nada 😀

      Curtir

    • Opa, este comportamento não é esperado …rs.. pode ter algum ASCII 0 no arquivo, ou o arquivo tem uma quebra de linha diferenciada … Me envie mais detalhes por e-mail (siga0984@gmail.com) que eu dou uma olhada 😀

      Curtir

  2. Olá Júlio!

    Eu criei uma classe para meu sistema baseado nesta sua implementação, porém me deparei com casos onde a última linha do arquivo não era considerada se a mesma não possuía o caractere separador no final (no meu caso o CRLF).

    Consegui corrigir com o código abaixo:

    [..]
    // Considera no BUFFER de trabalho o resto da última leitura e mais o
    // que acabou de ser lido.
    cBuffer := ::cRemaining + cTmp

    // Verifica se o final do arquivo possui o caractere de delimitação.
    If nRead <= DEFAULT_FILE_BUFFER
    cBuffer += If(Right(cBuffer,Len(::cFSep)) == ::cFSep,"",::cFSep)
    EndIf
    [..]

    Não sei se é a maneira mais correta, mas deu certo.
    Obrigado!

    Curtido por 1 pessoa

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s