Existe uma maneira mais rápida de remover uma linha (dado um número de linha) de um arquivo?

6

Uma questão relacionada é aqui .

Muitas vezes preciso editar um arquivo grande removendo algumas linhas do meio dele. Eu sei quais linhas desejo remover e normalmente faço o seguinte:

sed "linenum1,linenum2 d" input.txt > input.temp

ou in-line adicionando a opção -i. Como conheço os números de linha, existe um comando para evitar a edição de fluxo e apenas remover as linhas específicas? input.txt pode ter até 50 GB.

    
por sturgman 03.03.2013 / 17:53

10 respostas

9

O que você poderia fazer para evitar escrever uma cópia do arquivo é escrever o arquivo como:

{
  sed "$l1,$l2 d" < file
  perl -le 'truncate STDOUT, tell STDOUT'
} 1<> file

Perigoso, já que você não tem cópia de segurança.

Ou evitando sed , roubando parte da ideia do manatwork:

{
  head -n "$(($l1 - 1))"
  head -n "$(($l2 - $l1 + 1))" > /dev/null
  cat
  perl -le 'truncate STDOUT, tell STDOUT'
} < file 1<> file

Isso ainda pode ser melhorado porque você está sobrescrevendo as primeiras linhas l1 - 1 , enquanto não é necessário, mas evitá-las significaria um pouco mais de programação e, por exemplo, faça tudo em perl , o que pode acabar sendo menos eficiente:

perl -ne 'BEGIN{($l1,$l2) = ($ENV{"l1"}, $ENV{"l2"})}
    if ($. == $l1) {$s = tell(STDIN) - length; next}
    if ($. == $l2) {seek STDOUT, $s, 0; $/ = 768; next}
    if ($. > $l2) {print}
    END {truncate STDOUT, tell STDOUT}' < file 1<> file

Alguns horários para remover linhas de 1000000 a 1000050 da saída de seq 1e7 :

  • sed -i "$l1,$l2 d" file : 16.2s
  • 1ª solução: 1,25s
  • 2ª solução: 0,057s
  • 3a solução: 0,48s

Todos trabalham com o mesmo princípio: abrimos dois descritores de arquivo no arquivo, um no modo somente leitura (0) usando < file short para 0< file e um no modo leitura-gravação (1) usando 1<> file ( <> file seria 0<> file ). Esses descritores de arquivo apontam para duas descrições de arquivos abertos que terão cada uma posição atual do cursor dentro do arquivo associado a elas.

Na segunda solução, por exemplo, o primeiro head -n "$(($l1 - 1))" lerá $l1 - 1 linhas de dados de fd 0 e grava esses dados em fd 1. Então, no final desse comando, o cursor em ambos descrições de arquivos abertos associadas aos fds 0 e 1 estarão no início da linha $l1 th.

Então, em head -n "$(($l2 - $l1 + 1))" > /dev/null , head lerá $l2 - $l1 + 1 linhas da mesma descrição do arquivo aberto através de seu fd 0 que ainda está associado a ele, então o cursor em fd 0 irá vá para o começo da linha depois do $l2 um.

Mas o seu fd 1 foi redirecionado para /dev/null , portanto, ao escrever para o fd 1, ele não moverá o cursor na descrição do arquivo aberto apontada por {...} 's fd 1 .

Portanto, ao iniciar cat , o cursor na descrição do arquivo aberto apontada por fd 0 estará no início da próxima linha após $l2 , enquanto o cursor em fd 1 ainda estará no início da linha $l1 th. Ou dito de outra forma, esse segundo head terá pulado essas linhas para remover na entrada, mas não na saída. Agora, cat sobrescreverá a linha $l1 th com a próxima linha após $l2 e assim por diante.

cat retornará quando chegar ao final do arquivo no fd 0. Mas o fd 1 apontará para algum lugar no arquivo que ainda não foi sobrescrito. Essa parte tem que ir embora, corresponde ao espaço ocupado pelas linhas deletadas agora deslocadas para o final do arquivo. O que precisamos é truncar o arquivo no local exato onde o fd aponta para agora.

Isso é feito com a chamada do sistema ftruncate . Infelizmente, não há nenhum utilitário Unix padrão para fazer isso, então recorremos a perl . tell STDOUT nos dá a posição atual do cursor associada ao fd 1. E nós truncamos o arquivo nesse deslocamento usando a interface do perl para a chamada do sistema ftruncate : truncate .

Na terceira solução, substituímos a gravação por fd 1 do primeiro comando head com uma chamada de sistema lseek .

    
por 03.03.2013 / 20:42
3

Usar sed é uma boa abordagem: é claro, ele transmite o arquivo (sem problemas com arquivos longos) e pode ser facilmente generalizado para fazer mais. Mas se você quiser uma maneira simples de editar o arquivo no local, o mais fácil é usar ed ou ex :

(echo 10,31d; echo wq) | ed input.txt

Uma abordagem melhor, garantida para trabalhar com arquivos de tamanho ilimitado (e para linhas contanto que sua RAM permita) é a seguinte perl one-liner que edita o arquivo no lugar:

perl -n -i -e 'print if $. < 10 || $. > 31' input.txt

Explicação:

-n: Apply the script to each line. Produce no other output.
-i: Edit the file in-place (use -i.bck to make a backup).
-e ...: Print each line, except lines 10 to 31.

    
por 04.03.2013 / 18:58
1

Você pode usar o Vim no modo Ex:

ex -sc '1d2|x' input.txt
  1. 1 passar para a primeira linha

  2. 2 selecione 2 linhas

  3. d delete

  4. x salvar e fechar

por 17.04.2016 / 08:30
1

Se você precisa ler e escrever 50GiB, isso irá demorar muito tempo, independentemente do que você fizer. E a menos que as linhas sejam de comprimento fixo, ou você tenha alguma outra maneira de saber onde estão as linhas a serem apagadas, não há maneira de ler o arquivo até a última linha a ser apagada. Talvez um programa personalizado que apenas conte novas linhas e depois copie blocos completos seja um pouco mais rápido que sed(1) , mas acredito que esse não seja o seu gargalo. Tente usar time(1) para descobrir como o tempo é dividido.

    
por 03.03.2013 / 19:46
0

Isso ajudaria?

perl -e '
           $num1 = 5;
           $num2= 10000;
           open IN,"<","input_file.txt";
           open OUT,">","output_file.txt";
           print OUT <IN> for (1 .. $num1-1)
           <IN> for ($num1 .. $num2);
           undef $/ and print OUT <IN>;
           close IN;
           close OUT;
          '

Isso remove todas as linhas entre 5 e 10000, inclusive. Altere os números para atender às suas necessidades. No entanto, não é possível ver uma maneira eficiente de fazer in situ (ou seja, essa abordagem terá que imprimir em um arquivo de saída diferente).

    
por 03.03.2013 / 20:13
0

Se você quiser editar o arquivo em vigor, a maioria das ferramentas do shell não ajudará, porque quando você abre um arquivo para gravação, você só tem a opção de truncá-lo ( > ) ou anexá-lo ( >> ), não sobrescrevendo o conteúdo existente. dd é uma exceção notável. Veja Existe uma maneira de modificar um arquivo no local?

export LC_ALL=C
lines_to_keep=$((linenum1 - 1))
lines_to_skip=$((linenum2 - linenum1 + 1))
deleted_bytes=$({ { head -n "$lines_to_keep"
                    head -n "$lines_to_skip" >&3;
                    cat
                  } <big_file | dd of=big_file conv=notrunc;
                } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek="$(($(wc -c <big_file) - $deleted_bytes))"

(Atenção: não testado!)

    
por 04.03.2013 / 01:32
0

Isso é legal e simples:

perl -ine 'print unless $.==13' /path/to/your/file

para remover, e. linha 13 de /path/to/your/file

    
por 21.09.2014 / 19:07
0

No caso especial em que o conteúdo das linhas que devem ser excluídas é exclusivo no arquivo, outra opção pode estar usando grep -v e o conteúdo da linha, em vez dos números de linha. Por exemplo, se apenas uma linha exclusiva deve ser excluída (a exclusão de uma única linha foi solicitada, por exemplo, nesta duplicata thread ), ou muitas linhas que possuem o mesmo conteúdo exclusivo.

Aqui está um exemplo

grep -v "content of lines to delete" input.txt > input.tmp

Eu fiz alguns benchmarks com um arquivo que contém 340 000 linhas. O caminho com grep parece ser cerca de 15 vezes mais rápido que o método sed neste caso.

Aqui estão os comandos e os horários:

time sed -i "/CDGA_00004.pdbqt.gz.tar/d" /tmp/input.txt

real    0m0.711s
user    0m0.179s
sys     0m0.530s

time perl -ni -e 'print unless /CDGA_00004.pdbqt.gz.tar/' /tmp/input.txt

real    0m0.105s
user    0m0.088s
sys     0m0.016s

time (grep -v CDGA_00004.pdbqt.gz.tar /tmp/input.txt > /tmp/input.tmp; mv /tmp/input.tmp /tmp/input.txt )

real    0m0.046s
user    0m0.014s
sys     0m0.019s

Eu tentei ambos com e sem a configuração LC_ALL = C, isso não altera os tempos. A string de pesquisa (CDGA_00004.pdbqt.gz.tar) está em algum lugar no meio do arquivo.

    
por 19.03.2017 / 13:29
-1

você pode adicionar uma instrução * q * ao seu comando sed quando o linenum2 for atingido, assim o sed para o processamento do arquivo.

sed 'linenum1,linenum2d;linenum2q' file
    
por 03.03.2013 / 18:02
-1

Observe que esta é uma resposta a uma pergunta diferente marcada como duplicada.

A questão era quente para remover a linha 4125889 do in.csv.

Você pode fazer coisas inseguras - então você pode ser rápido, mas pode perder todo o arquivo, ou você depende da velocidade do editor que você está usando.

Eu recomendo:

echo 'in.csv13in.csv.bak03y' | VED_FTMPFIR=. ved +4125878 in.csv

em que você precisa de 3x o tamanho do arquivo e termina com echo 'ved13VED_FTMPFIR=.03!' | VED_FTMPFIR=. ved +4125878 in.csv e %code%

ou:

%code%

onde você precisa de 2x o tamanho do arquivo e o arquivo resultante será gravado.

Note que você precisa de uma implementação shell (echo) compatível com POSIX para obter as saídas adequadamente expandidas. O editor %code% faz parte das ferramentas schily e está disponível em:

link

em schily - *. tar.bz2

Ele usa o mecanismo de arquivo de troca mais rápido que conheço.

O ambiente %code% configura o diretório do arquivo de troca para o diretório atual. selecione qualquer diretório que tenha espaço suficiente.

    
por 02.10.2015 / 16:26