[Rio-pm] Isso não deveria estar certo???

breno breno em rio.pm.org
Sexta Dezembro 7 21:40:15 PST 2012


Oi Aureliano,

cara, antes de tudo, parabéns. O exemplo que vc colou no email mostra
que vc está prestando atenção no que é dito aqui na lista. Seus
códigos estão agora com strict (faltou só o 'warnings', hein?), usando
autodie, open com 3 argumentos, muito bom mesmo! Agora, pra ficar
perfeito, falta só uma coisa: por favor, antes de mandar dúvidas,
lembre que as pessoas daqui da lista não estão sentadas aí do seu lado
olhando o seu problema, nem vivendo o seu dia-a-dia para saber os
motivos e objetivos do código, tampouco possuem (eu pelo menos não
possuo) poder de ler mente :-)

Por exemplo, quando vc pergunta algo do tipo:

> Monges, cade o erro???

Fica faltando o clássico grupo: "o que vc está tentando fazer, de modo
geral? No código específico, o que deveria estar acontecendo mas não
está? Ainda no código específico, o que está de fato acontecendo em
vez do que deveria?"

Quando, mesmo depois do Junior Moraes matar a charada, vc continua com:

> Mas me diz uma coisa, como editar um arquivo.

A gente tem que se esforçar até pra saber que é uma pergunta, mais
ainda qual é a real pergunta por trás da pergunta. "Como editar um
arquivo" é quase tão vago quanto "Preciso saber o que estou errando
aqui" (retirado de outra thread que vc começou). Percebe o padrão?

Isso não é uma bronca. Mas, pensa comigo: se der a impressão que vc
não está se esforçando nem pra fazer a pergunta, pq alguém se
esforçaria pra te dar a resposta? A atividade na lista é voluntária e
não remunerada, feita por pessoas com um interesse em comum (Perl) e
que se dispõem a ajudar umas às outras em seu tempo livre. Por isso
mesmo, se vc quer respostas melhores e mais rápidas, precisa nos
ajudar a te ajudar! Por exemplo, experimente ler suas perguntas em voz
alta antes de enviar o email, e veja se elas deixam realmente claro o
que você está perguntando. Existe um texto muito bom sobre isso
chamado "Como fazer perguntas inteligentes" mas também é grande e tem
muitas coisas lugar-comum que você certamente já sabe. Se me permite a
sugestão, vá direto nessas aqui, que são onde (eu pelo menos) sinto
mais dificuldade:

http://www.istf.com.br/perguntas/#writewell
http://www.istf.com.br/perguntas/#beprecise
http://www.istf.com.br/perguntas/#symptoms
http://www.istf.com.br/perguntas/#goal
http://www.istf.com.br/perguntas/#examples

Não estou falando isso pra vc ficar chateado, pelo contrário! Estou só
tentando te ajudar a nos ajudar, e assim tirar maior proveito das
listas e melhorar cada vez mais. Os parágrafos acima são uma leitura
super rápida, pense como um investimento: vc vai gastar menos de 5
minutos pra ler (e está traduzido em português!) e se prestar atenção
nesses pontos, tenho certeza que daqui pra frente você vai conseguir
respostas muito mais rapidamente nessa e em qualquer outra lista!

Dito isso, deixa eu tentar te ajudar. Eu *acho*, depois de perder um
bom tempo olhando pro seu exemplo, que a sua pergunta é:

"Pessoal, como eu faço pra abrir o mesmo arquivo tanto para leitura
quanto para escrita?"

ou

"Pessoal, como eu faço para ler e escrever no mesmo arquivo, ao mesmo tempo?"


Se for isso mesmo, é uma pergunta super comum. E, como a maioria das
coisas em Perl, há mais de uma maneira de se fazer :)

Digamos, para efeito de exemplo, que você queira passar o nome de um
arquivo como parâmetro para o seu programa. O arquivo tem o formato:

----------8<-----------
>blablabla
ACTGAACAGTAGCTACTGACTCGTACGCTCGTAGC
>lalalala
CAGCTGATCGATCGTAGCATGCTACG
---------->8-----------

E o que você quer é transformar esse arquivo em algo como:

----------8<-----------
>contig0
ACTGAACAGTAGCTACTGACTCGTACGCTCGTAGC
>contig1
CAGCTGATCGATCGTAGCATGCTACG
---------->8-----------

Claro, não só duas linhas e sim centenas, mas acho que deu pra entender.


Solução 1 (one-liner)
================

perl -i -pe 'BEGIN { $i = 0 } $i++ if s{^>.+$}{>contig$i}' arquivo.txt

Essa solução usa perl como ferramenta para transformação de arquivos
"in place". O "-pe" executa o código entre aspas para cada linha do
arquivo passado, imprimindo o que estiver em $_ no final do
processamento de cada linha. O código em si é simples: substitua
"^>.+$" (início de linha seguido de ">" seguido de qualquer coisa,
seguido do fim da linha) pela string ">contig$i", onde $i é um número
que começa com zero (por isso o BEGIN { $i = 0 }, senão $i começa
vazio, e a primeira linha vira apenas ">contig" em vez de ">contig0").
Sempre que conseguir fazer essa substituição (ou seja, sempre que a
linha em questão casar com a regex do lado esquerdo do s{}{}),
incremente o valor de $i. Assim, quando a linha casar com a regex, a
substituição sera feita, o $i será incrementado, e a linha será
impressa (por causa do "-p"). Se a linha não casar com a regex, nada
será feito e a linha será impressa sem mudanças (de novo, por causa do
"-p").

Executando apenas com o "-pe", ele vai imprimir na tela o resultado
pra vc. Isso significa que vc pode redirecionar pra outros arquivos,
se quiser. Mas como em nosso problema queremos que o próprio arquivo
seja modificado, usamos a opção "-i" (de "in place"), que escreve a
saída no próprio arquivo de entrada pra você, sem que você precise
ficar se preocupando com criação e cópia de arquivos temporários. Se
quiser manter o original para fins de backup, basta trocar "-i" por
"-i.bak", e o perl manterá uma cópia do original em "arquivo.txt.bak".

Mais detalhes: perldoc perlrun



Solução 2 (one-liner, em arquivo)
==========================

----------8<-----------
#!/usr/bin/env perl -pi
BEGIN { $i = 0 }
$i++ if s{^>.+$}{>contig$i}
---------->8-----------

One-liners são bacanas, mas são também difíceis de manter e de
lembrar. Uma alternativa é copiá-los para arquivos que podem ser
executados normalmente. Sabe o "shebang", aquela linha com "#!" que
chama o "perl"? Você pode passar parâmetros por ela também. No caso,
estamos passando "-p" e "-i" juntos ("-pi"), já que não precisamos do
"-e" pois o código está no próprio arquivo. Repare também que o "-i"
tem que ser o último parâmetro (sem nada depois), pois se
escrevessemos "-ip" o perl vai achar que vc quer editar o arquivo e
gravar um backup com extensão "p".

Claro que um "one-liner em arquivo" é difícil de manter, porque vc tem
que prestar atenção nos modificadores passados (em nosso caso, "-pi")
e fazer a "tradução" de cabeça do que de fato está acontecendo. Também
tem que, idealmente, adicionar strict, warnings, declarar variáveis,
aquilo tudo que é imprescindível para um programa profissional e fácil
de manter. Para contornar esse problema, você pode procurar soluções
como o App::Rad para converter seus one-liners em programas, ou
simplesmente continuar lendo :)



Solução 3 (alteração in-place, versão extendida)
=====================================

----------8<-----------
#!/usr/bin/env perl
use strict;
use warnings;

# para mudar arquivos 'in-place'
# vindos do ARGV.
local $^I = q{};

my $i = 0;
while ( my $linha = <ARGV> ) {
  if ($linha =~ s/^>.+$/>contig$i/ ) {
    $i++;
  }
}
continue {
  die "erro editando arquivo in-place: $!\n"
    unless print $linha;
}
---------->8-----------

Essa versão é a forma "extendida" do one-liner. Acho legal ver como
uma simples linha se transforma dessa forma, dá outra perspectiva para
quanto o perl facilita a nossa vida (outra comparação que sempre me
impressiona é Moose x OO tradicional, mas isso é outro papo). A
primeira coisa a notar é que estamos inicializando a variável local
$^I, uma variável especial do Perl que transforma os arquivos de
entrada nos próprios arquivos de saída, exatamente como o "-i" faz na
linha de comando. Colocamos a string vazia pois, nesse exemplo, não
estamos gravando backup do original. Depois, lemos o conteúdo dos
arquivos linha-a-linha e fazemos nossa já conhecida substituição e
incremento do $i (dessa vez botei o if na forma "ativa", pra mostrar
que tanto faz: vale o que ficar mais claro para você). Finalmente,
termos o bloco "continue", que é executado no final de cada linha.
Poderíamos ter escrito nosso while sem o continue, assim:

----------8<-----------
while ( my $linha = <ARGV> ) {
  if ($linha =~ s/^>.+$/>contig$i/ ) {
    $i++;
  }
  die "erro editando arquivo in-place: $!\n"
    unless print $linha;
}
---------->8-----------

Mas, se o efeito é o mesmo, pq usar continue? Simples: se amanhã vc
quiser incrementar seu parser e pular linhas, sair do loop, etc, o que
estiver dentro do bloco continue será executado mesmo após chamar
"next". O efeito é o mesmo, e vc pode deixar sem o "continue" se te
confundir (apenas lembre que ele existe, caso precise desse recurso no
futuro).

Para mais informações: perldoc -f continue



Solução 4 (usando arquivos temporários)
================================

----------8<-----------
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;

my $nome_original = 'arquivo.txt';
my $nome_tmp      = 'tmp.txt';

open my $orig_fh, '<', $nome_original;
open my $tmp_fh,  '>', $nome_tmp;

my $i = 0;
while ( my $linha = <$orig_fh> ) {
  if ($linha =~ s/^>.+$/>contig$i/ ) {
    $i++;
  }
}
continue {
    print $tmp_fh $linha;
}

close $orig_fh;
close $tmp_fh;
rename $nome_tmp => $nome_original;
---------->8-----------

Essa abordagem com arquivo temporário usa menos memória do que a
solução sem arquivos temporários mais abaixo. Também é mais segura de
trabalhar e fácil de entender, e te dá a oportunidade de gravar um
arquivo de backup - é só adicionar um rename($nome_original =>
"outro_nome") antes do rename que está na última linha do exemplo.
Outra vantagem é que vc pode gravar o nome dos arquivos de origem
direto no programa ou em um arquivo de configurações, em vez de ser
obrigado a passar o nome do arquivo como parâmetro, como nas soluções
anteriores. Notas: certifique-se que os arquivos não estão sendo
editados por mais ninguém durante o processo, de preferência com lock
files. Outra: o rename() não funciona entre sistemas de arquivos,
então crie o arquivo temporário de preferência no mesmo diretório do
outro ou use File::Copy para uma solução realmente portátil.



Solução 5 (sem arquivos temporários)
=============================

----------8<-----------
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;

open my $fh, '+<', 'arquivo.txt';

my @novas_linhas = ();
my $i = 0;
while ( my $linha = <$fh> ) {
  if ($linha =~ s/^>.+$/>contig$i/ ) {
    $i++;
  }
}
continue {
    push @novas_linhas => $linha;
}

# volta ao inicio do arquivo,
# sobrescreve tudo com as novas
# e arranca fora (trunca) o que sobrar
seek $fh, 0, 0;
print $fh @novas_linhas;
truncate $fh, tell($fh);
close $fh;
---------->8-----------

Essa solução abre o arquivo em modo '+<' (update), o que te permite
ler e escrever ao mesmo tempo no arquivo. Note que, se vc le e escreve
AO MESMO TEMPO, o "stream de bytes" do arquivo muda o tempo todo, e
você certamente vai se perder em relação ao ponto exato no arquivo em
que seu handle está. Por isso, a solução é primeiro ler todo o arquivo
em memória (ou ler somente as linhas que interessam, já com o conteúdo
modificado, como fazemos acima), e só depois que processar todas as
linhas, voltar ao início do arquivo e sobrescrever o conteúdo. De
novo, faça lock do arquivo ou tenha certeza que ninguém vai editar o
arquivo junto com seu programa.

Vale ressaltar que essa solução sem arquivo temporário só é
recomendada se os arquivos forem pequenos e vc REALMENTE não puder
usar arquivos temporários (por quê não poderia?), pois consome muito
mais memória, é mais difícil de escrever, entender e manter, sem falar
que não te dá a oportunidade de gravar uma cópia de segurança do
arquivo original e pode confundir outros processos tentando ler o
arquivo que você está editando.

Se, por outro lado, você puder GARANTIR que os tamanhos dos campos
editáveis serão fixos (e substituidos pela mesma quantidade de bytes
do mesmo tamanho), aí sim dá pra fazer algo bacana com o trio
seek/read/print de forma eficiente e sem usar arquivos temporários.
Mas isso normalmente só é verdade para arquivos binários.

Para mais informações: perldoc -f funcao (onde "funcao" seja uma das
que foram usadas acima, tipo seek, tell e truncate).
Para mais informações sobre lockfiles: perldoc -f flock



Bom, é isso. A solução 1 é a mais prática, mas não escala bem.
Particularmente, prefiro a solução número 4, pelos motivos já citados.

Se essa foi realmente a sua pergunta, taí. Agora, se não foi, espero
que pelo menos ajude alguém :)

Finalmente, mais uma dica: essas e muitas outras "receitas" podem ser
encontradas no fantástico livro "Perl Cookbook" do Tom Christiansen e
do Nathan Torkington. Foi um dos meus primeiros livros de Perl e um
dos poucos que vira e mexe ainda consulto. Está todo muito bem
dividido em grupos de problemas (string, arquivos, regex,...) e vc
encontra rapidamente o que quer olhando o índice. Cada problema vem
com a solução e uma discussão em cima do problema, de forma bastante
didática.


Versão Eletrônica (apenas Kindle):
-----------------------------------------------

R$ 48,68 na amazon.com.br
http://www.amazon.com.br/Perl-Cookbook-COOKBOOKS-ebook/dp/B0043GXMTS/ref=sr_1_1?s=digital-text&ie=UTF8&qid=1354942271&sr=1-1

USD$ 22,79 na amazon.com
http://www.amazon.com/Perl-Cookbook-COOKBOOKS-ebook/dp/B0043GXMTS/ref=sr_1_1?ie=UTF8&qid=1354942175&sr=8-1&keywords=perl+cookbook+kindle


Versão Eletrônica (vários formatos pra escolher):
-----------------------------------------------------------------

USD$ 19,99* na loja da O'Reilly
http://shop.oreilly.com/product/9780596003135.do

*USD$ o preço real da versão eletrônica é 39,99. Como vc faz parte de
um grupo de usuários filiado à O'Reilly, é só usar o código de
desconto "DSUG" e ganhar 50% de desconto para ebooks (40% para livros
impressos).

R$48,49 na Cultura
http://www.livrariacultura.com.br/scripts/resenha/resenha.asp?nitem=17405530&sid=12225113814128111046650811


Versão Impressa:
------------------------

- não encontrei na Saraiva

- na Cultura tem sob encomenda, à R$144,40 (+frete)

- na Estante Virtual só tem 1 exemplar, à R$90,00 (+frete)
http://www.estantevirtual.com.br/arkanthum/Tom-Christiansen-e-Nathan-Torkington-Perl-Cookbook-72690537


Independente da sua escolha, é um livro que recomendo vivamente para a
cabeceira de qualquer programador Perl, especialmente para quem está
começando. Me ajudou bastante e tenho certeza que vai ajudar você
também. Acredite: vale *cada* centavo.


Qq dúvida, como sempre, estamos aí!

[]s

-b


Mais detalhes sobre a lista de discussão Rio-pm