Como posso remover a primeira linha de um arquivo de texto usando o script bash / sed?

554

Preciso remover repetidamente a primeira linha de um arquivo de texto enorme usando um script bash.

No momento eu estou usando sed -i -e "1d" $FILE- mas leva cerca de um minuto para fazer a exclusão.

Existe uma maneira mais eficiente de conseguir isso?

Brent
fonte
o que -i representa?
Cikatomo 9/03/2013
4
@ cikatomo: significa edição embutida - edita o arquivo com o que você gerar.
Drewrockshard #
4
cauda é MUITO MAIS LENTA que sed. cauda precisa de 13,5 segundos, sed precisa de 0,85 segundos. Meu arquivo tem ~ 1 milhão de linhas, ~ 100 MB. MacBook Air 2013 com SSD.
jcsahnwaldt diz GoFundMonica

Respostas:

1029

Tente cauda :

tail -n +2 "$FILE"

-n x: Basta imprimir as últimas xlinhas. tail -n 5daria as últimas 5 linhas da entrada. O +sinal meio que inverte o argumento e torna a tailimpressão qualquer coisa, menos as primeiras x-1linhas. tail -n +1imprimiria o arquivo inteiro, tail -n +2tudo menos a primeira linha etc.

GNU tailé muito mais rápido que sed. tailtambém está disponível no BSD e o -n +2sinalizador é consistente nas duas ferramentas. Verifique as páginas de manual do FreeBSD ou OS X para mais.

A versão BSD pode ser muito mais lenta do sedque isso. Eu me pergunto como eles conseguiram isso; taildeve apenas ler um arquivo linha por linha enquanto sedrealiza operações bastante complexas que envolvem a interpretação de um script, a aplicação de expressões regulares e similares.

Nota: Você pode ficar tentado a usar

# THIS WILL GIVE YOU AN EMPTY FILE!
tail -n +2 "$FILE" > "$FILE"

mas isso lhe dará um arquivo vazio . O motivo é que o redirecionamento ( >) acontece antes tailé chamado pelo shell:

  1. Shell trunca arquivo $FILE
  2. A Shell cria um novo processo para tail
  3. O Shell redireciona o stdout do tailprocesso para$FILE
  4. tail lê a partir do agora vazio $FILE

Se você deseja remover a primeira linha dentro do arquivo, você deve usar:

tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE"

O &&irá certificar-se de que o arquivo não obter substituído quando há um problema.

Aaron Digulla
fonte
3
De acordo com este ss64.com/bash/tail.html, o buffer padrão é 32k ao usar o 'tail' do BSD com a -ropção Talvez haja uma configuração de buffer em algum lugar do sistema? Ou -né um número assinado de 32 bits?
Ýzmir Ramirez
41
@ Eddie: user869097 disse que não funciona quando uma única linha é de 15 Mb ou mais. Contanto que as linhas sejam mais curtas, tailfuncionará para qualquer tamanho de arquivo.
Aaron Digulla
6
você poderia explicar esses argumentos?
Dreampuf # 6/11
17
@Dreampuf - a partir da página de manual:-n N means output the last N lines, instead of the last 10; or use +N to output lines starting with the Nth
Will Sheppard
11
Eu concordaria com @JonaChristopherSahnwaldt - a cauda é muito, muito mais lenta que a variante sed, por uma ordem de magnitude. Estou testando em um arquivo de 500.000 K linhas (não mais que 50 caracteres por linha). No entanto, percebi que estava usando a versão FreeBSD do tail (que vem com o OS X por padrão). Quando mudei para o GNU tail, a chamada tail era 10 vezes mais rápida que a sed call (e a chamada sed GNU também). AaronDigulla está correto aqui, se você estiver usando o GNU.
Dan Nguyen
179

Você pode usar -i para atualizar o arquivo sem usar o operador '>'. O comando a seguir excluirá a primeira linha do arquivo e salvará no arquivo.

sed -i '1d' filename
amit
fonte
1
Eu recebo o erro:unterminated transform source string
Daniel Kobe
10
isso funciona sempre e deve ser realmente a melhor resposta!
xtheking
4
Apenas para lembrar, o Mac exige que seja fornecido um sufixo ao usar o sed com edições no local. Execute o procedimento acima com -i.bak
mjp
3
Apenas uma nota - para remover várias linhas de usarsed -i '1,2d' filename
The Godfather
4
Esta versão é realmente muito mais legível e universal do que tail -n +2. Não sei por que não é a melhor resposta.
Luke Davis
74

Para aqueles que estão no SunOS que não é GNU, o seguinte código ajudará:

sed '1d' test.dat > tmp.dat 
Nasri Najib
fonte
18
Informações demográficas interessantes
capitão
17

Não, isso é tão eficiente quanto você conseguirá. Você poderia escrever um programa em C que pudesse fazer o trabalho um pouco mais rápido (menos tempo de inicialização e argumentos de processamento), mas provavelmente tenderá à mesma velocidade que o sed, à medida que os arquivos aumentam (e suponho que sejam grandes se demorar um minuto )

Mas sua pergunta sofre do mesmo problema que tantas outras, pois pressupõe a solução. Se você nos disser em detalhes o que está tentando fazer, então como , podemos sugerir uma opção melhor.

Por exemplo, se esse é um arquivo A que outro programa B processa, uma solução seria não retirar a primeira linha, mas modificar o programa B para processá-lo de maneira diferente.

Digamos que todos os seus programas anexem a esse arquivo A e o programa B atualmente lê e processa a primeira linha antes de excluí-lo.

Você pode reprojetar o programa B para que ele não tente excluir a primeira linha, mas mantenha um deslocamento persistente (provavelmente baseado em arquivo) no arquivo A, para que, da próxima vez que seja executado, ele possa procurar esse deslocamento, processo a linha lá e atualize o deslocamento.

Então, em um horário silencioso (meia-noite?), Ele poderia executar um processamento especial do arquivo A para excluir todas as linhas processadas no momento e definir o deslocamento de volta para 0.

Certamente será mais rápido para um programa abrir e buscar um arquivo, em vez de abrir e reescrever. Esta discussão assume que você tem controle sobre o programa B, é claro. Não sei se é esse o caso, mas pode haver outras soluções possíveis se você fornecer mais informações.

paxdiablo
fonte
Acho que o OP está tentando alcançar o que me fez encontrar essa pergunta. Eu tenho 10 arquivos CSV com 500k linhas em cada um. Todo arquivo tem a mesma linha de cabeçalho da primeira linha. Estou criando esses arquivos em um arquivo e importando-os para um banco de dados, permitindo que o banco de dados crie nomes de colunas a partir da primeira linha. Obviamente, não quero que essa linha seja repetida no arquivo 2-10.
db
1
@db Nesse caso, awk FNR-1 *.csvprovavelmente é mais rápido.
jinawee
10

Você pode editar os arquivos no local: Basta usar o -isinalizador do perl , assim:

perl -ni -e 'print unless $. == 1' filename.txt

Isso faz a primeira linha desaparecer, como você pede. O Perl precisará ler e copiar o arquivo inteiro, mas organiza para que a saída seja salva com o nome do arquivo original.

alexis
fonte
10

Você pode fazer isso facilmente com:

cat filename | sed 1d > filename_without_first_line

na linha de comando; ou para remover a primeira linha de um arquivo permanentemente, use o modo local de sed com o -isinalizador:

sed -i 1d <filename>
Ingo Baab
fonte
9

Como Pax disse, você provavelmente não vai ficar mais rápido que isso. O motivo é que quase não existem sistemas de arquivos que suportem truncamento desde o início do arquivo, portanto esta será uma noperação O ( ) em que né o tamanho do arquivo. O que você pode fazer muito mais rápido é sobrescrever a primeira linha com o mesmo número de bytes (talvez com espaços ou um comentário) que pode funcionar para você, dependendo exatamente do que você está tentando fazer (o que é isso, a propósito?).

Robert Gamble
fonte
Re "... quase nenhum sistema de arquivos que suporta truncamento ..." : isso é interessante; considere incluir uma nota entre parênteses nomeando esse sistema de arquivos.
agc
1
@agc: irrelevante agora, mas meu primeiro trabalho nos anos 70 foi com a Quadex, uma pequena startup (agora desaparecida e não relacionada às duas empresas que agora usam esse nome). Eles tinham um sistema de arquivos que permitia adicionar ou remover no início ou no final de um arquivo, usado principalmente para implementar a edição em menos de 3 KB, colocando arquivos acima da janela e abaixo da janela. Não tinha nome próprio, era apenas parte do QMOS, o sistema operacional Quadex Multiuser. ( 'Multi' era geralmente 2-3 em um LSI-11/02, com sob 64KB RAM e normalmente numa pequena de tipo RX01 8" disquetes cada 250KB.) :-)
dave_thompson_085
9

O spongeutilitário evita a necessidade de manipular um arquivo temporário:

tail -n +2 "$FILE" | sponge "$FILE"
agc
fonte
spongeé de fato muito mais limpo e mais robusta do que a solução aceite ( tail -n +2 "$FILE" > "$FILE.tmp" && mv "$FILE.tmp" "$FILE")
Jealie
1
Deve ficar claro que 'sponge' requer que o pacote 'moreutils' seja instalado.
FedFranzoni 22/01
Esta é a única solução que funcionou para mim para alterar um arquivo do sistema (em uma imagem da janela de encaixe Debian). Outras soluções falharam devido ao erro "Dispositivo ou recurso ocupado" ao tentar gravar o arquivo.
FedFranzoni 22/01
Mas o spongebuffer todo o arquivo está na memória? Isso não funcionará se forem centenas de GB.
1811 OrangeDog
@OrangeDog, desde que o sistema de arquivos possa armazená-lo, spongeo absorverá, pois ele usa um arquivo / tmp como uma etapa intermediária, que é usada para substituir o original posteriormente.
agc
8

Se você quiser modificar o arquivo no lugar, você pode sempre usar o original edem vez do seu s sucessor treaming sed:

ed "$FILE" <<<$'1d\nwq\n'

O edcomando era o editor de texto UNIX original, antes mesmo de existirem terminais de tela cheia, muito menos estações de trabalho gráficas. O exeditor, mais conhecido como o que você está usando quando digitação no prompt do cólon em vi, é um ex versão tendia de ed, por isso, muitos dos mesmos comandos de trabalho. Embora edseja para ser usado interativamente, também pode ser usado no modo em lote enviando uma sequência de comandos para ele, que é o que esta solução faz.

A seqüência <<<$'1d\nwq\n'tira proveito do apoio do Bash para cadeias de caracteres here ( <<<) e citações POSIX ( $'... ') à entrada de alimentação para o edcomando que consiste em duas linhas: 1d, que d eletes linha 1 , e, em seguida wq, que w ritos o arquivo de volta para disco e, em seguida, q sai da sessão de edição.

Mark Reed
fonte
isso é elegante. +1
Armin
Mas você precisa ler o arquivo inteiro na memória, o que não funcionará se forem centenas de GB.
21419 OrangeDog
5

deve mostrar as linhas, exceto a primeira linha:

cat textfile.txt | tail -n +2
serup
fonte
4
- você deve fazer "tail -n +2 textfile.txt"
niglesias
5
@niglesiais Não concordo com o "uso inútil de gato", pois deixa claro que esta solução está correta no conteúdo canalizado e não apenas nos arquivos.
Titou
5

Pode usar o vim para fazer isso:

vim -u NONE +'1d' +'wq!' /tmp/test.txt

Isso deve ser mais rápido, pois o vim não lê o arquivo inteiro durante o processo.

Hongbo Liu
fonte
Pode ser necessário citar +wq!se o seu shell é bash. Provavelmente não, já que a !palavra não está no começo de uma palavra, mas adquirir o hábito de citar as coisas provavelmente é bom. (E se você está buscando supereficiência sem citar desnecessariamente, também não precisa de aspas 1d).
Mark Reed
vim não precisa ler o arquivo inteiro. De fato, se o arquivo for maior que a memória, conforme solicitado neste Q, o vim lê o arquivo inteiro e o grava (ou quase todo) em um arquivo temporário, e após a edição grava tudo de volta (no arquivo permanente). Não sei como você acha que poderia funcionar sem isso.
David_thompson_085
4

Que tal usar o csplit?

man csplit
csplit -k file 1 '{1}'
Shahbaz
fonte
Esta sintaxe também trabalho, mas apenas gerar dois arquivos ao invés de três saída: csplit file /^.*$/1. Ou, mais simplesmente: csplit file //1. Ou ainda mais simples: csplit file 2.
Marco Roy
1

Como parece que não posso acelerar a exclusão, acho que uma boa abordagem pode ser processar o arquivo em lotes como este:

While file1 not empty
  file2 = head -n1000 file1
  process file2
  sed -i -e "1000d" file1
end

A desvantagem disso é que, se o programa for morto no meio (ou se houver algum sql ruim lá - causando a morte ou o bloqueio da parte "processo"), haverá linhas que serão ignoradas ou processadas duas vezes .

(arquivo1 contém linhas de código sql)

Brent
fonte
O que a primeira linha contém? Você pode simplesmente substituí-lo com um comentário sql, como sugeri no meu post?
Robert Gamble
0

Se o que você está procurando é se recuperar após a falha, basta criar um arquivo com o que você fez até agora.

if [[ -f $tmpf ]] ; then
    rm -f $tmpf
fi
cat $srcf |
    while read line ; do
        # process line
        echo "$line" >> $tmpf
    done
Tim
fonte
0

Este liner fará:

echo "$(tail -n +2 "$FILE")" > "$FILE"

Funciona, já que tailé executado antes echoe depois o arquivo é desbloqueado, portanto, não há necessidade de um arquivo temporário.

egors
fonte
-1

Usar cauda em linhas N-1 e direcioná-lo para um arquivo, seguido pela remoção do arquivo antigo e renomear o novo arquivo para o nome antigo, faria o trabalho?

Se eu estivesse fazendo isso de maneira programática, leria o arquivo e lembre-se do deslocamento do arquivo, depois de ler cada linha, para que eu pudesse procurar novamente nessa posição para ler o arquivo com menos uma linha.

EvilTeach
fonte
A primeira solução é essencialmente idêntica à que Brent está fazendo agora. Eu não entendo sua abordagem programática, apenas a primeira linha precisa ser excluída, você apenas lê e descarta a primeira linha e copia o restante para outro arquivo que é novamente o mesmo que o sed e tail se aproxima.
Robert Gamble
A segunda solução implica que o arquivo não é reduzido pela primeira linha de cada vez. O programa simplesmente o processa, como se tivesse sido reduzido, mas começando na próxima linha de cada vez
EvilTeach
Ainda não entendo qual é a sua segunda solução.
Robert Gamble