Manipulação de arquivos em AdvPL – Parte 01

Introdução

Faz algum tempo, eu publiquei um post 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 funções de manipulação de arquivos funcionam. Neste post, vamos ver o que elas fazem “por dentro”, para entendermos como podemos utilizá-las.

Um arquivo no disco

Qualquer arquivo gravado em uma unidade de disco ou em um dispositivo com sistema de arquivos (Drive USB, CD-ROM,etc…) não passa de uma sequência finita de bytes, que representam um conteúdo. Um arquivo (no Windows) normalmente possui um nome e uma extensão, separados pelo caractere “.” (ponto), onde a extensão indica ao sistema operacional qual é o tipo do conteúdo deste arquivo. O arquivo possui um tamanho alocado na unidade na qual ele está gravado — quantos bytes têm o arquivo — além da data e hora da última gravação feita no arquivo, e ainda pode conter atributos especiais.

Uma imagem gravada em disco possui uma representação digital da imagem real, em uma determinada resolução (ou grid de pontos em linhas x colunas), onde o formato utilizado indica ao programa como as sequencias de bytes devem ser tratadas para representar a imagem que o arquivo contém.

Arquivos TEXTO

Um arquivo de texto simples — por exemplo com a extensão “.txt”, nada mais é do que a representação em bytes de todas as letras, números, espaços, pontuação, letras acentuadas e quebras de linha. Normalmente um arquivo texto simples utiliza a representação da tabela ASCII — onde uma faixa de bytes com os valores de 0 a 31 representam caracteres de controle ( quebra de linha, tabulação e outros), do 32 as 127 representam letras maiúsculas e minúsculas, números — representação do número como texto –, espaço em branco, caracteres e símbolos gráficos comuns (exclamação, arroba, dólar, percentual, asterisco, hífen, colchetes, aspas simples e duplas, parênteses, e afins; e por fim a faixa de 128 a 255 é utilizada para representar letras acentuadas, caracteres gráficos e especiais. Esta representação é conhecida como TABELA ASCII.

Existe uma outra padronização mundialmente conhecida de conversão de valores de bytes para caracteres. Chama-se EBCDIC — vide mais detalhes nas referências no final do post — criada pela IBM na década de 60, praticamente junto da padronização do ASCII. Esta tabela foi usada nos equipamentos IBM a partir do Mainframe System/360, usada ainda hoje em Mainframes e Mid-Range Computers da IBM.

Outros formatos de arquivo

Existem vários outros formatos de arquivo, cada formato na verdade é uma especificação de como o seu conteúdo deve ser tratado. A Tabela ASCII é convenção de representação de códigos numéricos para caracteres, criada para padronizar a troca de informações entre computadores. Na prática, dentro do arquivo, eu tenho uma sequência de bytes com valores de 0 a 255.

Texto e Bytes

Vamos partir de alguns exemplos, para deixar mais claro o que tem dentro dos arquivos, e das variáveis do tipo “C” Caractere do AdvPL, usando algumas funções. Por exemplo, se eu quero criar em AdvPL uma variável caractere com as letras A, B e C (maiúsculas, sem acento), eu posso simplesmente fazer uma atribuição direta a uma variável, usando um conteúdo literal.

cTexto := "ABC"

Na área de memória usada para armazenar estes três caracteres, é usado um byte para cada caractere. O valor guardado dentro do Byte é o valor numérico da representação da letra na TABELA ASCII. Por exemplo, a letra “A” é o número 65“B” é 66 e “C” é 67.

Eu poderia escrever isso de outra forma. Por exemplo, usando a função CHR(), que recebe o número do byte e retorna a representação em string do mesmo. Por exemplo:

cTexto := chr(65) + chr(66) + chr(67)

O conteúdo da variável cTexto será exatamente igual nas duas formas de atribuição. Um arquivo texto pode ter algumas diferenças entre sistemas operacionais. Por exemplo, no Windows uma quebra de linha do arquivo é representada pela sequência de bytes chr(13)+chr(10). Já em Linux, é usado apenas chr(10). Logo, eu posso montar a representação de uma linha de um arquivo texto na memória usando:

cLinha := "Uma linha de texto." + chr(13) + chr(10)

Funções de baixo nível de manipulação de arquivo

Existem algumas funções da linguagem AdvPL, projetadas para ler e gravar bytes de qualquer arquivo. Porém, nenhuma destas funções fazem qualquer crítica ao tipo de conteúdo do arquivo — isto é, não importa se o arquivo tem uma imagem, um texto ou um vídeo: Para estas funções, o arquivo é simplesmente uma sequência de bytes. São elas:

  • FCreate – Cria um arquivo.
  • FOpen – Abre um arquivo.
  • FSeek – Reposiciona o ponteiro de dados do arquivo.
  • FRead – Lê bytes do arquivo.
  • FWrite – Grava bytes no arquivo.
  • FClose – Fecha o arquivo
  • FError – Retorna o status de execução da última instrução de arquivo

As funções FCreate() e FOpen() retornam um número identificador, chamado de “Handler de Arquivo”, e todas as demais funções, enquanto o handler não foi fechado pela função FCLose(), recebem este identificador como parâmetro para realizar as operações descritas.

Ponteiro de Dados do Arquivo

O ponteiro de dados é uma espécie de marcador que aponta para um determinado byte do conteúdo arquivo, em uma determinada posição — que também pode ser chamado de “Offset“. O Offset inicia em 0 (zero), correspondendo ao primeiro byte do arquivo. Em um arquivo de tamanho N bytes, o ponteiro de dados pode apontar para uma determinada posição dentro do arquivo, de 0 a N-1, ou pode apontar para o final do arquivo (EOF), correspondendo a posição N.

Qualquer operação de leitura ou gravação sempre é iniciada na posição onde o ponteiro de dados está apontando, e a operação é sempre feito da posição atual em direção ao final do arquivo. Imagine um arquivo no disco como sendo uma sequência de bytes, iniciando da esquerda para a direita.

Função FCreate()

Primeiro vamos criar um arquivo. Usamos para isso a função FCreate(), onde informamos no primeiro parâmetro o caminho e nome do arquivo a ser criado. Em caso de sucesso, a função FCreate() retorna um identificador numérico, chamado de “Handler” do arquivo. Agora, vamos ver alguns detalhes sobre os comportamentos da função FCreate().

  • Caso o arquivo informado como parâmetro contenha uma unidade de disco (drive), o Application Server entende que a criação da tabela será feita pelo cliente AdvPL — SmartClient.
  • Caso o arquivo não contenha o drive ou unidade de disco, o arquivo será criado na máquina onde o Application Server está sendo executado, em uma pasta ou sub-pasta a partir do RootPath (pasta raiz de arquivos do ambiente).
  • Caso o arquivo a ser criado já exista no disco, e não esteja sendo aberto e/ou usado por outro processo, o conteúdo anterior do arquivo é perdido — ele torna-se um arquivo vazio, com 0 (zero) bytes.
  • Caso o arquivo a ser criado não exista, ele é criado vazio — com 0 byte(s).
  • Uma vez que o arquivo tenha sido criado (ou recriado) com sucesso, ele será mantido aberto em modo exclusivo, para leitura e escrita. Isto é, enquanto este programa não fechar o arquivo usando a função FClose(), o arquivo permanece aberto para o processo atual ler e gravar bytes no arquivo, e nenhum outro processo vai conseguir abrir este arquivo, mesmo que seja apenas para leitura.
  • Em caso de falha na criação (ou recriação) do arquivo, o handler retornado é o valor -1 (menos um) , indicando erro. Podemos recuperar a causa do erro chamando a função FError() após executar o FCreate(). A função FError() retorna o código do erro de criação/recriação da tabela, e retorna 0 (zero) em caso de sucesso.
User Function TSTFILE()
Local nHnd 
Local cFile := '\temp\teste.txt'
Local cLine
nHnd := FCreate(cFile)
If nHnd == -1
  MsgStop("Falha ao criar arquivo ["+cFile+"]","FERROR "+cValToChar(fError()))
  Return
Endif

Com o fonte acima, criamos um arquivo vazio, na pasta “\temp\”, que deve ser criada a partir do RootPath do ambiente atual, Esta configuração está presente nos ambientes do Protheus Server, na chave “RootPath” do ambiente.

Como  o arquivo está vazio, o ponteiro de dados do arquivo neste momento não aponta para lugar algum . Agora, vamos gravar uma linha de texto em formato Windows dentro desse arquivo, com quebra de linha.

cLine := "Olá sistema de arquivos" + CRLF

Para quem não conhece, CRLF escrito desta forma no AdvPL — junto e com letras maiúsculas — é um #translate, que retorna os dois bytes ( chr(13) e chr(10) que indicam em um arquivo texto do Windows que deve ser realizada uma quebra de linha. Este recurso está disponível no AdvPL desde que seja utilizado o #include “protheus.ch” — ou o seu equivalente, “totvs.ch”.

Agora, vamos gravar esta informação três vezes no arquivo, em três linhas, uma embaixo da outra, e em seguida fechar o arquivo

FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FClose(nHnd)

Uma operação de escrita no arquivo é feita sempre a partir de onde está apontando o ponteiro de dados. Como o arquivo não tem nada após ser criado, o ponteiro de dados está em EOF (Final de Arquivo), a operação de escrita no arquivo insere os bytes/caracteres informados no final do arquivo, aumentando o seu tamanho. Após gravar os caracteres, o ponteiro de dados aponta novamente para EOF,

Feito isto, vamos ver exatamente o que foi gravado. Cada linha contém 23 caracteres (ou bytes), mais dois caracteres de controle ( chr(13)  e chr(10) ). Logo, cada linha possui na verdade 25 bytes de informação. Após gravar as três linhas e as respectivas quebras, o arquivo resultante deve ter exatamente 75 byte(s).

Função FOPEN

A função fOpen() é usada para abrir um arquivo. Podemos especificar parâmetros opcionais para definir se o arquivo será aberto para leitura, escrita, ou ambos (leitura e escrita), e ainda é possível especificar o modo de compartilhamento do arquivo — exclusivo, compartilhado, bloqueio de leitura ou bloqueio de escrita. Por default, o arquivo é aberto para leitura exclusiva — nenhum outro processo pode abrir o arquivo para fazer nada, e o processo atual pode apenas ler dados do arquivo.

Seu tratamento de erro é idêntico ao FCreate() — em caso de retorno -1 (menos um), a função FError() contém um código com mais detalhes da falha encontrada.

Veja os detalhes da documentação da função FOpen na TDN. O modo de abertura e compartilhamento da tabela é definido pela soma de dois números das listas de modo de acesso e modo de compartilhamento — apenas um elemento de cada lista. Para usarmos as constantes — que são mais “elegantes” para ler o fonte, devemos fazer um #include “fileio.ch” no início do nosso fonte AdvPL. No nosso exemplo, vamos abrir a tabela no modo default.

nHnd := FOpen(cFile)
If nHnd == -1
   MsgStop("Falha ao abrir ["+cFile+"]","Ferror " + cValToChar(fError()) )
Endif

Caso eu queira, por exemplo, abrir o arquivo em modo compartilhado de leitura e escrita, podemos usar os seguintes parâmetros:

nHnd := FOpen(cFile,FO_READWRITE + FO_SHARED)

Vale lembrar que, ao usar um modo compartilhado que permita escrita no arquivo, dois processos podem tentar escrever na mesma área de dados do arquivo. Se isto acontecer, a última gravação é a que vale. Cabe a aplicação gerenciar os acessos concorrentes ao mesmo Offset do arquivo.

Função FSEEK

Usamos a função FSeek() para mover o ponteiro de Offset de dados do arquivo. Esta função não mexe no arquivo, apenas movimenta o ponteiro de dados do arquivo o parâmetro nOffset informado, e a partir de que ponto do arquivo — ou origem — este Offset deve ser considerado. Veja a documentação oficial completa da FSeek no TDN.

Para entender melhor seu funcionamento, vamos das um exemplo de cada movimentação a partir de uma origem distinta. A origem padrão / default é 0 (zero), indicando que o nOffSet informado é considerado a partir do início do arquivo — posição 0 (zero). Veja abaixo os parâmetros a serem informados para a função.

FSeek( < nHandle >, < nOffSet >, [ nOrigem ] )

O parâmetro nOrigem é opcional. Ele indica a partir de que ponto do arquivo o parâmetro nOffset deve ser considerado. Seu valor default é 0 (zero). Ele pode ser:

  • 0 ( zero) — DEFAULT — Movimenta o ponteiro de dados para a posição informada em nOffSet, a partir do início do arquivo.
  • 1 (um) – Considera o nOffSet como sendo o número de bytes para movimenta o ponteiro de dados a partir de sua posição atual. Neste caso, nOffSet pode ser positivo ( mover o ponteiro para frente) ou negativo (mover o ponteiro para traz).
  • 2 (dois) – Movimenta o ponteiro de dados considerando nOffSet como sendo a quantidade de bytes para deslocar o ponteiro de dados a partir do final do arquivo. Neste caso, nOffSet deve ser 0 (zero) ou um valor negativo.

O retorno da função, independente dos parâmetros utilizados, será a nova posição (OffSet) do arquivo, a partir do início do arquivo. Vamos a alguns exemplos

FSeek( nHnd , 0  [ ,0 ] ) – Posiciona no primeiro byte do arquivo. Retorna o OffSet após o movimento  (zero).

FSeek( nHnd , 3 [ ,0 ]) – Posicional no quarto byte do arquivo. Retorna o OffSet após o movimento (três).

FSeek( nHnd , 0 , 2 ) – Posiciona o ponteiro de dados no final do arquivo (EOF). Neste caso o offset atual corresponde ao tamanho do arquivo — pois para um arquivo de N bytes, o final de arquivo (EOF) é justamente a posição N.

FSeek( nHnd , 5 , 1 ) – Avança o ponteiro de dados 5 bytes para frente da posição atual.

FSeek( nHnd , -5 , 1 )  – Volta o ponteiro de dados 5 bytes para traz da posição atual.

Quando utilizamos o #include “fileio.ch”, ao invés de usar os números de 0 a 2 como terceiro parâmetro da função fSeek(), podemos usar as constantes abaixo:

Número 0       Constante FS_SET         
Número 1       Constante FS_RELATIVE    
Número 2       Constante FS_END

Continuando o programa de exemplo, vamos determinar o tamanho do arquivo e mover o ponteiro de dados para a primeira posição (Offset 0) do arquivo novamente.

nTamFile := FSeek(nHnd,0,FS_END)
FSeek(nHnd,0,FS_SET)

Função FRead()

Agora, vamos ler os primeiros 25 bytes do arquivo. Para isso, usamos a função FRead(). A Documentação oficial está no TDN – FRead. Veja abaixo os parâmetros da função:

FRead( < nHandle >, < cBufferVar >, < nQtdBytes > )

A função recebe em nHandle o identificador do arquivo obtido através da função FCreate() ou FOpen(), uma variável string onde os bytes lidos do arquivo a partir da posição atual do ponteiro de dados, e a quantidade de bytes que devem ser lidas.

Por exemplo, logo após determinar o tamanho do arquivo, vamos ler os primeiros 25 bytes do arquivo:

cBuffer := ""
nRead := FRead( nHnd, @cBuffer, 25 )
conout("Bytes Lidos ... "+cValToChar(nRead))

Primeiro inicializamos uma variável do tipo “C” Caractere no AdvPL com uma string vazia, depois a passamos por referência na função FRead(), seguido do número de bytes que eu quero ler a partir da posição atual do ponteiro de dados do arquivo. A função retorna o número de bytes que efetivamente foram lidos. Caso a leitura termine devido ao final do arquivo ter sido atingido, ou caso tenha havido algum erro, o valor de retorno de FRead() pode ser 0 ou um valor menor que o tamanho que foi solicitado para ler. Em caso de erro na leitura, a função FError() deve conter maiores informações. Se ferror() retornar zero, e o conteúdo lido for menor que o solicitado, então o arquivo efetivamente chegou ao final.

Como eu gravei três linhas de 25 bytes no arquivo, inserindo as quebras de linha, eu seu que ao carregar os primeiros 25 bytes ao arquivo, eu li a primeira linha de dados do arquivo inclusive com a quebra de linha. E, se eu fizer mais duas leituras da mesma forma, eu vou ler fatalmente as linhas 2 e 3 inteiras, respectivamente.

Função FWrite()

Caso eu queira escrever alguma informação no arquivo, eu posso utilizar a função FWrite(), desde que eu tenha aberto o arquivo para escrita. A função FWrite() recebe o identificador ou Handler do arquivo como parâmetro, seguido de uma variável “C” Caractere do AdvPL, contendo uma sequência de caracteres / bytes a serem gravados no arquivo, e pode ainda receber um último parâmetro opcional, permitindo informar quantos bytes do buffer de caracteres informado deve ser salvo. Caso este último parâmetro não seja informado, a string inteira é gravada no arquivo.

De forma similar a FRead(), que começa a leitura a partir da posição atual do ponteiro de dados do arquivo, a função FWrite() começa a gravar os dados a partir da posição atual do ponteiro de dados, em direção ao final do arquivo. Caso o ponteiro de dados esteja situado em um ponto do arquivo com informações já gravadas, estas informações serão perdidas, pois deste ponto em diante a nova string informada como parâmetro será salva. Caso o ponteiro de dados atinga EOF, ou já esteja em EOF, os dados são acrescentados a partir de então, sempre no final do arquivo, aumentando o tamanho do arquivo.

Não há função ou API direta que permita inserir novos dados no meio do arquivo deslocando os demais dados para a frente. E não há função ou API direta que diminua o tamanho de um arquivo. Vamos ao exemplo, continuando o fonte anterior:

nGravado := fWrite( nHnd , replicate('-',23) )

Após ler a primeira linha do arquivo, o ponteiro de dados estará apontando para o primeiro caractere da segunda linha, que é igual a primeira? 23 bytes de dados e os dos bytes finais de quebra de linha. Da forma feita acima, eu literalmente passo por cima da linha escrevendo vários “hifens” sobre a segunda linha, sem invadir o espaço ocupado pela quebra de linha. Se formos ver o arquivo após todas estas execuções, seu conteúdo deve ser:

Olá sistema de arquivos
-----------------------
Olá sistema de arquivos

Se eu escrever por exemplo mais 4 hífens, eu passaria por cima da quebra de linha, e por cima das duas primeiras letras da terceira linha. Sem a quebra de linha, o arquivo ficaria apenas com duas linhas, com o seguinte conteúdo:

Olá sistema de arquivos
---------------------------á sistema de arquivos

Observações finais

  • Se eu abrir um arquivo no modo de escrita, mesmo assim eu ainda consigo ler dados do arquivo.
  • Se eu abrir um arquivo apenas para leitura, a função FWrite() não vão conseguir gravar nada, vai retornar 0 (zero), e a função FError() retornará o código de erro correspondente.
  • Ao usar a função FOpen(), procure especificar o modo de abertura e o modo de acesso, sempre.

Conclusão

Por incrível que pareça, esta é apenas a primeira parte sobre as funções de baixo nível de arquivos em AdvPL. Ainda têm mais coisas para ver no próximo post desta sequência, porém com essa introdução já dá para se ter uma ideia do que é possível fazer em AdvPL.

Agradeço novamente a audiência, as curtidas, compartilhamentos, dúvidas e afins. Desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

Um comentário sobre “Manipulação de arquivos em AdvPL – Parte 01

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 )

Foto do Google

Você está comentando utilizando sua conta Google. 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 )

Conectando a %s