Página 1 de 2

Função para pesquisar e trocar texto qquer tipo de arquivo

Enviado: 10 Fev 2008 16:30
por Clipper
Prezados Colegas

Fiz uma funçãozinha para ajudar num post e acabei fazendo essa genérica para disponibilizar para os colegas.

Essa função faz uma pesquisa Byte a Byte por um cadeia de caracteres e com a opção de efetuar a troca por outra cadeia, pode ser usada em arquivo com qualquer extensão.

Dependendo do tamanho do arquivo essa pesquisa pode demorar um pouco, no teste que fiz com um arquivo de 4 MB demorou 43 segundos, em pesquisas com arquivos grandes é interessante colocar uma barra de progresso ou algo parecido.

Um exemplo clássico onde ela pode ser aplicada :

Digamos que existam vários arquivos PRG dentro de um diretório e que se deseja pesquisar em todos eles e trocar a cadeia DBFNTX por DBFCDX, o uso seria assim :

AEVAL(DIRECTORY("*.PRG"),{|AFILE|PESQBYTE(AFILE[F_NAME],"DBFNTX","DBFCDX")})

Lembrando que como é uma pesquisa byte a byte é Case sensitive (faz a diferença entre letras maiusculas e minusculas)

Segue abaixo o código :

Código: Selecionar todos

*** Pesquisa a existencia de um determinado texto dentro de um arquivo
*** O arquivo pode ter qualquer extensao
***
*** Parametros
*** cArq   = Nome do arquivo a ser pesquisado
*** cPesq  = Cadeia a ser pesquisada 
*** cTroca = Caso seja informado ao encontrar a sequencia sera feita a troca 
***
*** Retorno :
*** Retorna a quantidade de ocorrencias
*** Em caso de erro retorna -1, -2, -3 ou -4 dependendo do erro
***
*** Modificado em 10.02.2007 as 21:37 Horário de Brasilia
*** Adicionada Verificação de duplicidade de parametros
*** Por sugestão do Colega Eolo

*------------------------------------*
 Function PesqByte(cArq,cPesq,cTroca)
*------------------------------------*
if cArq=nil .or. cPesq=nil // Falta de parametros
   return(-1)
endif
if cTroca<>nil
   if len(cPesq)<>len(cTroca) // Tamanhos diferentes
      return(-2)
   endif
endif
if .not. file(alltrim(cArq)) // Arquivo nao encontrado
   return(-3)
endif
if alltrim(cPesq)=alltrim(cTroca) // Igualdade de parametros
   return(-4)
endif

hand=fopen(cArq,2)
Byte=1
Tamanho=fseek(hand,0,2)
posiciona=fseek(hand,0,0)
conta=0
do whil .T.
   leu=freadstr(hand,len(cPesq))
   if leu=cPesq
      conta++
      if cTroca<>nil
         fseek(hand,-len(cPesq),1)
         fwrite(hand,cTroca,len(cTroca))
      endif
   endif
   byteatual=fseek(hand,-len(cPesq)+1,1)
   byte++
   if byte>=tamanho-len(cPesq)+1
      fclose(hand)
      return(conta)
   endif
enddo
Até logo.

Marcelo

Enviado: 10 Fev 2008 21:00
por Eolo
Marcelo, faltou uma checagem, no começo: se cPesq e cTroca forem iguais, o programa não deveria continuar...

Também, a limitação do mesmo tamanho para cPesq e cTroca é ruim. Você tentou fazer sem essa lmitação pra ver o acréscimo de tempo?

Enviado: 10 Fev 2008 21:24
por Clipper
Eolo escreveu:Marcelo, faltou uma checagem, no começo: se cPesq e cTroca forem iguais, o programa não deveria continuar...

Também, a limitação do mesmo tamanho para cPesq e cTroca é ruim. Você tentou fazer sem essa lmitação pra ver o acréscimo de tempo?
Prezado Eolo

Realmente está certo quanto a igualdade de cPesq e cTroca, não existe motivo para continuar neste caso.

cPesq e cTroca não podem ter tamanhos diferentes pois dependendo do arquivo a ser mudado isso causaria erros imprevisiveis e provavelmente irrecuperaveis, imagine o estrago que isso causaria num EXE, OBJ ou DLL.

Gostaria de salientar que essa função apesar de funcionar para manipulação de dados foi feita pensando em arquivos geralmente não editaveis com o Clipper, como por exemplo : EXE, DLL, OBJ, COM, INI.

Para uso com DBF ela só tem uso apropriado para corrigir erros de estrutura ou coisa parecida.

Já alterado !

Valeu pela dica !

Até logo.

Marcelo

Enviado: 11 Fev 2008 09:24
por Maligno
Clipper escreveu:Essa função faz uma pesquisa Byte a Byte por um cadeia de caracteres e com a opção de efetuar a troca por outra cadeia, pode ser usada em arquivo com qualquer extensão.
Você disse que o desempenho da pesquisa pode demorar um pouco em arquivos maiores. Acho que o motivo da lentidão se explica pelo fato de você fazer uma leitura por FReadStr(), testar e depois voltar o ponteiro Len(cPesq)-1. Quer dizer, você vai pra frente e depois volta quase tudo. Aí a performance cai mesmo.

Uma sugestão: a forma que se costuma fazer é ler um bloco de dados para um buffer em memória, de certo tamanho X arbitrário, até o limite do tamanho do arquivo. Então a pesquisa passa a ser em memória. Mas ao invés de se buscar a string inteira, busca-se apenas a primeira letra, seja ela maiúscula ou minúscula. Uma vez encontrada a primeira letra, é só pesquisar o resto. Só se deve prestar um pouco mais de atenção quando a pesquisa chegar ao limite do buffer, já que a leitura pode não ter trazido a string inteira do arquivo. O código todo, neste caso, além de conseguir uma melhor performance, também resolveria o problema da "caixa" da string.

Um pouco mais de código e você conseguiria também incluir uma pesquisa binária e a troca de strings por outras de tamanhos diferentes. Mas como você disse que esse não é o objetivo do código, não comento nada.

Enviado: 11 Fev 2008 09:48
por Clipper
Prezado Maligno

A lentidão realmente é pelo motivo que você mencionou, disso eu já sabia, óbviamente que se puder carregar o arquivo ou partes dele para a memória e então fazer a busca ganharia um tempo significativo, creio eu que o tempo então passaria apenas ser a transferrência do bloco ou do arquivo inteiro para a memória, o que mesmo em arquivos grandes é rápido, o problema é que não sei como fazer da forma que você sugeriu, foge aos meus conhecimentos.

Quanto a questão da "caixa", não sei se entendi bem, mas me parece que a priori não sanaria o problema, pois a busca seria pela 1ª letra e esta poderia ser maiuscula ou minuscula tudo bem, mas e as outra letras da string a ser pesquisada ?

Até logo.

Marcelo

Enviado: 11 Fev 2008 10:47
por Maligno
Clipper escreveu:o problema é que não sei como fazer da forma que você sugeriu, foge aos meus conhecimentos.
Codificar isso nem é tanto o problema. Você só precisa da lógica:

Defina um buffer de tamanho apropriado para a operação. Digamos uns 32KB. Mas não use esse buffer antes de saber o tamanho do arquivo. Se ele for menor, use o tamanho dele. Assim, ele será totalmente carregado, sem desperdício de memória.

Leia os blocos no tamanho definido. Mas use FRead(). A função FReadStr() só lê strings. E strings são terminadas por zero. Se futuramente você for adaptar a função para pesquisa de valores binários, poderá acontecer de ler um byte zero. Daí FReadStr() não teria utilidade. Aliás, eu próprio nunca usei essa função.

Faça a pesquisa na variável. Ao encontrar algo, tente perceber se você está no limite do buffer. Se o fim dele for "...AKJSHKAJHDBF" e estiver procurando pela string "DBFNTX", você perderá a string. O ponteiro precisará voltar um pouco. Mas você também pode continuar fazendo do jeito que faz. Se não encontrar nada, forçosamente volte o ponteiro "Len(cPesq)-1" bytes antes de cada leitura.
Quanto a questão da "caixa", não sei se entendi bem, mas me parece que a priori não sanaria o problema, pois a busca seria pela 1ª letra e esta poderia ser maiuscula ou minuscula tudo bem, mas e as outra letras da string a ser pesquisada ?
Em linguagens mais poderosas, o costume é ler o buffer um byte por vez e convertê-lo para maiúscula ou minúscula. Aí se faz uma comparação. Parece ser algo que fica lento, mas não fica, já que a linguagem foi feita pra gerar um código bem otimizado. No caso do Clipper a coisa se complica um pouco, já que a linguagem XBase não foi feita pra esse tipo de tarefa. Neste caso, até se poderia tentar converter o buffer todo para maiúscula, por exemplo, e depois fazer uma busca simples. Mas se o arquivo for binário, a função Upper() pode falhar. Portanto, o ideal a meu ver, é fazer dessa forma mesmo: ler byte a byte, converter para maíscula e comparar com a primeira letra de cPesq, já convertida para maiúscula. Se iguais (chamemos esse byte de "X"), a rotina entra numa porção separada do código para analisar o restante de cPesq contra o buffer. Mesma coisa: converte-se letra por letra e se compara com o buffer. Se falhar, deve-se voltar o "ponteiro" de pesquisa para o primeiro byte após o byte "X" e recomeçar a partir daí. Após percorrer todo o buffer, se precisar de nova carga, antes volte o ponteiro "Len(cPesq)-1" bytes, para o caso de haver apenas uma parte de cPesq justo no final do buffer. Acredite: coincidências desse tipo nunca acontecem nos testes. Só depois do projeto pronto. :)

Você também poderia fazer uma pesquisa dupla por meio da função At(), mas o esquema de controle pode ficar até mais complicado, porque obrigatoriamente você terá de procurar pela primeira letra de cPesq convertida para maiúscula, e analisar o buffer até o fim, se for o caso. Terminado este processo, você precisa voltar seu "ponteiro" de pesquisa para o início e refazer o mesmo processo para a primeira letra de cPesq, agora convertida em minúscula.

Note que são muitos pequenos detalhes. Você tem que pensar onde o esquema de busca pode falhar pra poder ir "tapando" esses buracos. Em pesquisa de texto a coisa fica bem mais complicada do que em pesquisa de valores binários.

Enviado: 11 Fev 2008 11:24
por Clipper
Prezado Maligno

Obrigado pelas dicas !

Assim que tiver um tempinho vou tentar fazer conforme indicado, primeiro vou pensar em resolver o caso da lentidão, depois penso no problema do "case sensitive" que me parece ser bem mais complexo, minha intenção era só fornecer mais uma ferramenta para os colegas, mas vai valer como estudo também.

Até logo.

Marcelo

Enviado: 11 Fev 2008 15:05
por Clipper
Prezados Colegas

Baseado nas observações do Maligno segue abaixo a nova versão, ainda não é o ideal, mas já melhorou bastante.

Já não existe mais o problema do "Case sensitive" a função agora consegue encontrar a string independente de estar com caracteres maiúsculos e ou minúsculos.

A pesquisa agora é feita apenas em uma direção, segue sempre em frente, o ganho não foi tão grande cerca de 30%, aqui no meu micro antes um arquivo com 4 MB demorava 45 segundos, agora caiu para 35.

Vamos ao que interessa.

Código: Selecionar todos

*** Pesquisa a existencia de um determinado texto dentro de um arquivo
*** O arquivo pode ter qualquer extensao
***
*** Parametros
*** cArq   = Nome do arquivo a ser pesquisado
*** cPesq  = Cadeia a ser pesquisada 
*** cTroca = Caso seja informado ao encontrar a sequencia sera feita a troca 
*** Obs : Se a nova string for menor que a pesquisada entao a diferenca
***     : sera preenchida com espacos
***
*** Retorno :
*** Retorna a quantidade de ocorrencias
*** Em caso de erro retorna -1, -2 ou -3 dependendo do erro
***
*** Alterada em : 11.02.2008 as 14:29 horario de Brasilia
*** A pesquisa agora e feita do 1§ byte em diante
*** A funcao agora nao diferencia maisusculas de minusculas
***
*** Obs : Na pesquisa tanto faz se e maiuscula ou minuscula
***       porem no caso da nova string vale como ela foi passada
***       Ex : Se a nova string for "BaRtOlomEU" assim ela sera gravada
***
*** Contribuicoes : Maligo e Eolo
*** 
*------------------------------------*
 Function PesqByte(cArq,cPesq,cTroca)
*------------------------------------*
if cArq=nil .or. cPesq=nil // Falta de parametros
   return(-1)
endif
cPesq=alltrim(cPesq)
if cTroca<>nil
   cTroca=Alltrim(cTroca)
   if len(cPesq)<len(cTroca) // String pesquisada menor que nova string
      return(-2)
   endif

   if len(cPesq)>len(cTroca)  // a nova string e menor que pesquisada entao
      espacos=len(cPesq)-len(cTroca) // a diferenca sera preenchida com espacos
      cTroca=cTroca+space(espacos)
   endif
endif
if .not. file(cArq) // Arquivo nao encontrado
   return(-3)
endif
pl=upper(substr(cPesq,1,1))
hand=fopen(cArq,2)
Byte=1
Tamanho=fseek(hand,0,2)
posiciona=fseek(hand,0,0)
conta=0
do whil .T.
   lebyte=freadstr(hand,1)
   if upper(lebyte)=pl
      posiciona=fseek(hand,-1,1)
      leu=freadstr(hand,len(cPesq))
      if upper(leu)=upper(cPesq)
         conta++
         if cTroca<>nil
            fseek(hand,-len(cPesq),1)
            fwrite(hand,cTroca,len(cTroca))
         endif
      endif
   endif
   byteatual=fseek(hand,1,-1)
   byte++
   if byte>=tamanho-len(cPesq)+1
      fclose(hand)
      return(conta)
   endif
enddo
Até logo.

Marcelo

Enviado: 26 Fev 2008 09:27
por Pablo César
Aproveitando carona aqui neste tópico, gostaria eu saber como faço para descobrir o CHR(13)+CHR(10) de cada linha em um arquivo texto. Talvez o mestre Maligno me dê uma luz. Passo explicar melhor a situação:

Tenho um arquivo texto, que em cada final de linha tem um CHR(13)+CHR(10), acho que até aí meu arquivo é como outro qualquer, só que cada linha tem um tamanho diferente e preciso pegar LINHA a LINHA do arquivo. Se fosse linha de tamanho FIXO, eu faria assim:

Código: Selecionar todos

cTXT:=MEMOREAD("TESTE.TXT")
nLINES:=MLCOUNT(cTXT,70) // aqui saberia quantas linhas tem o arquivo
FOR I=1 TO nLINES
    ? SUBSTR(MEMOLINE(cTXT,70,I),3) // iria mostra SUBSTR de cada linha do arquivo
NEXT
Mas como cada linha pode ter diferentes tamanho, cómo faria para eu não utilizar essas funções acima e passar a usar funções de baixo nível ?. Talvez essa procura não seja da mesma forma que é feita com STRIBG, e como você Maligno mencionou em fazer procura binária para o Marcelo, penso que deveria ser assim a minha localização de final de linha, mas confesso que não sei fazer e preciso ajuda.

Obs.: Preciso desta rotina para ser incrementado no utilitário para MP3 que irei disponibilizar na seção fontes junto com a primeira versão que o Marcelo disponibilizou.

Enviado: 26 Fev 2008 10:53
por Pablo César
Ja que ninguém respondeu (ao menos até o momento em que eu mesmo achei a solução), se bem que a procura é feito byte a byte (não sei se existe outra opção que possa ser mais rápida, não que esta demore, mas tenho a impressão que em arquivo bem grandes não irá ser nada ágil, de todas formas passo abaixo este exemplo para que dêm uma olhada:

Código: Selecionar todos

PARAMETERS cTextFile
IF cTextFile = NIL
   ? "Precisa informar o nome do arquivo"
   QUIT
ENDIF

nFileHandle = FOPEN( cTextFile )
nFileSize = FSEEK( nFileHandle, 0, 2 )
FSEEK( nFileHandle, 0, 0 )
nBytesRead = 0
vLinha = ReadLin( nFileSize )
FCLOSE( nFileHandle )
FOR I=1 TO LEN(vLinha)
    ? vLinha[i]
next

FUNCTION ReadLin(nFileSize)
Static v_L:={}
cAccumText:=""
DO WHILE nBytesRead<=nFileSize
   cSingle = FREADSTR( nFileHandle, 1 )
   nBytesRead = nBytesRead + 1
   IF cSingle = CHR(13)
      cSingle = FREADSTR( nFileHandle, 1 )
      nBytesRead = nBytesRead + 1
      IF cSingle = CHR(10)
         cSingle = FREADSTR( nFileHandle, 1 )
         nBytesRead = nBytesRead + 1
         IF !(cSingle = CHR(26) .OR. ASC(cSingle) = 0)
            FSEEK( nFileHandle, -1, 1 )
            nBytesRead = nBytesRead - 1
         ENDIF
      ELSE
         IF !(cSingle = CHR(26) .OR. ASC(cSingle) = 0)
           FSEEK( nFileHandle, -1, 1 )
           nBytesRead = nBytesRead - 1
         ENDIF
      ENDIF
      AADD(v_L,cAccumText)
      cSingle = ""
      cAccumText = ""
   ELSE
      cAccumText = cAccumText + cSingle
   ENDIF
ENDDO
RETURN v_L
Este exemplo irá colocar em um VECTOR o contéudo de cada linha, claro que vou limitar a quantidade de vetor para meu caso resolve, ora porque existe uma limitação de quantidade de elementos e que não deve estrapolar esse número. Mas enfim resolveu, fica aqui como experiência.

Enviado: 26 Fev 2008 16:06
por Maligno
Não é aconselhável você procurar texto lendo byte a byte. Cai a performance. A melhor maneira de encontrar o par 13/10 é ler um bom lote de dados e procurar o primeiro (13) usando a boa e velha função At(). Encontrado, confirme que o segundo (10) existe ao lado e separe para ter sua linha. Mas porque o primeiro e não procurar os dois de uma vez? Porque o primeiro (13) pode estar justamente na fronteira do buffer lido. Se procurar pelo par não vai encontrar. Ai você faz nova leitura e o segundo byte da linha (10) será o primeiro byte desta nova leitura. Aí a caca está feita. Lembre-se que Murphy adora esse tipo de coisa. :)

PS: Repare que você fez dois teste "IF cSingle = CHR(13)" seguidos.

Enviado: 26 Fev 2008 17:27
por Clipper
Prezado Pablo

Não sei se entendi direito o que voce quer fazer, mas creio que que neste caso é mais fácil exportar via COPY TO SDF para um DBF e pegar cada linha como se fosse um registro.

Até logo.

Marcelo

Enviado: 26 Fev 2008 17:49
por Pablo César
Não é aconselhável você procurar texto lendo byte a byte. Cai a performance.
Sim concordo, foi o que falei antes. Mas como o que eu preciso para o momento são umas poucas linhas, apenas para exibir na tela as suas primeiras linhas, nada que precise exibir tudo.
Repare que você fez dois teste "IF cSingle = CHR(13)" seguidos
Ihhhh é mesmo (vou corrigir o código acima).

Obrigado, Maligno pela sua costumeira atenção.

Enviado: 26 Fev 2008 17:54
por Pablo César
Clipper escreveu:neste caso é mais fácil exportar via COPY TO SDF para um DBF e pegar cada linha como se fosse um registro.
Ahhh sim Marcelo (obrigado pela sua dica), eu também acho que se eu tivesse necessidade de exibir muitas mais linhas, eu optaria por esse recurso, pois acredito que é mais seguro e no final das contas tornaria-se mais rápido. MAs são apenas 14 linhas que preciso exibir como máximo.

Enviado: 26 Fev 2008 18:42
por Maligno
Porque você não aproveita que está com a mão na massa e faz um sub-sistema de tratamento de texto, pra efeito didático? Algo que lhe permita abstrair todas essas funções, a ponto de poder "navegar" por um arquivo texto da mesma forma como se faz com DBF. Fiz um pra mim. Me deu uma boa ajuda. Uso inclusive no meu preview de relatório. Sua pesquisa ficaria mais ou menos assim:

Código: Selecionar todos

function PesqStr(cStr)
local nLine := 0
local cLine
txOpen("ARQ.TXT")
while nLine <= txLines()
   cLine := txGetLine() // não precisa dizer qual linha
   if cStr está nesta linha...
      exit // achou!
   end
end
txClose() // não precisa do handle
Mais moleza que isso só se você sentar no pudim. :)))