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
Obrigado pela classe Julio…
E de brinde descobri uma outra classe padrão que nem sabia que existia (FWFileReader) !!!
CurtirCurtido por 1 pessoa
Opa, perfeitamente. Tudo o que o FrameWork do AdvPL está criando de novo do P11 em diante está sendo documentado na TDN 😀
CurtirCurtir
Caso queira melhorar a performance da FWFileReader, basta usar o método oFile:setBufferSize(4096)
No mesmo arquivo que o Julio testou, cai aqui da minha maquina de 3.501s para 1.922s. Ainda não é tão rápido quanto mais já melhora bem.
Abs
CurtirCurtido por 1 pessoa
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
CurtirCurtir
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
CurtirCurtido 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 😀
CurtirCurtir
[…] post “Acelerando o AdvPL – Lendo arquivos TXT”, foi corrigido um erro que causava um mau comportamento da rotina, fazendo a leitura de linhas […]
CurtirCurtir
Tentei usar a classe FWFileReader(), usando para oFile:GetLine(), mas só lê uma linha do arquivo e já vai para EOF.
CurtirCurtido por 1 pessoa
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 😀
CurtirCurtir
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!
CurtirCurtido por 1 pessoa
* Correção:
If nRead < DEFAULT_FILE_BUFFER
CurtirCurtido por 1 pessoa
Bacana campeão !!! Não previ esta situação 😀 😉 Mandou bem !!!
CurtirCurtir
[…] sobre desempenho em AdvPL, criando uma classe em AdvPL para leitura de arquivos de texto simples (Acelerando o AdvPL – Lendo arquivos TXT). Porém, o exemplo em si já parte da premissa que o programador conhece o que e como funcionam as […]
CurtirCurtir