Remover eficientemente o primeiro par de linhas de um arquivo de texto

1
  • head / tail precisará iterar quase todo o arquivo (dependendo da posição da linha que você fornecer como parâmetro). Então você copia esse resultado para um novo arquivo e exclui o antigo.

  • Não tenho certeza se sed estará iterando o arquivo inteiro, mas você precisará copiar esse resultado para um novo arquivo e excluir o antigo. Mesmo com -i (no lugar) cria um arquivo temporário sob o capô, então a mesma coisa se aplica.

Por que não basta mover o ponteiro que aponta para a primeira linha do arquivo e movê-lo para a linha que queremos?

Como poderíamos fazer uma coisa dessas? Eu tenho que fazer em C? Existe outra maneira?

Isso faz sentido? Estou pensando errado? Se sim porque?

    
por HashWizard 17.05.2017 / 18:15

3 respostas

5

Why not just move the pointer that points to the first line of the file and move it to the line that we want?

Porque não existe um "ponteiro que aponte para a primeira linha do arquivo".

As operações básicas para modificar um arquivo são: sobrescrever um intervalo de bytes (isto é, substituir uma parte por dados do mesmo tamanho), anexar (isto é, adicionar no final), truncar (ou seja, remover do final).

A maioria dos sistemas de arquivos armazena arquivos em blocos de tamanho fixo, exceto que o último bloco pode ser parcial. Não há como modificar os dados em vigor se a modificação alterar o tamanho do que é modificado, a menos que a alteração esteja no final ou a modificação altere os dados por um número inteiro de blocos. Deslocar dados por um número inteiro de blocos só funcionaria por coincidência e não há interface para fazer isso.

A maneira mais eficiente de remover dados no início de um arquivo é copiar os dados que precisam ser mantidos em um novo arquivo. Qual é precisamente o que tail -n +42 ou sed '41,$p' faz.

    
por 18.05.2017 / 03:00
3

Gilles me bateu nisso: não há "ponteiro que aponte para a primeira linha do arquivo". A primeira linha do arquivo - o começo do arquivo - é sempre o primeiro caractere do arquivo. (Pode haver aplicações individuais obscuras que reconhecem tal noção, mas não há nada como isso no nível do sistema.)

O que você já sabe:

Comandos como

  • sed '1,6d' filename
  • sed -n '7,$p' filename
  • tail -n +7 filename

(e provavelmente outras variantes) gravará todas as primeiras seis linhas de filename para a saída padrão. (Todos, é claro, leem todo o arquivo.) Enquanto estamos nisso,

  • sed -n '1,6p' filename
  • sed '7,$d' filename
  • head -n 6 filename
  • sed '6q' filename

grava as 6 primeiras linhas de filename na saída padrão. Os dois primeiros podem ou não ler o arquivo inteiro; os dois últimos provavelmente não o farão.

Além disso,

command input_filename > the_same_filename
não funciona, conforme discutido em Aviso sobre ">" .

O que você talvez não saiba:

command arguments    1<> filename

abrirá filename para leitura e escrita sem truncar (estragar). Então,

sed '1,6d' filename  1<> the_same_filename
pode ser o primeiro passo na solução que você está procurando. Isso provavelmente é o mais próximo que você chegará remover as primeiras linhas M de um arquivo “in-loco”; ele irá ler o arquivo e sobrescrevê-lo simultaneamente, sem criar outro arquivo. Se M é pequeno o suficiente (ou, especificamente, se o número de bytes nas primeiras linhas M for pequeno o suficiente), isto pode ler cada bloco do arquivo uma vez e escrever cada bloco uma vez - e você não pode fazer nada melhor que isso.

Apenas o primeiro passo?

Eu criei este arquivo de teste:

$ cat -n foo
     1  a
     2  bcd
     3  efghi
     4  jklmnop
     5  qrstuvwxy
     6  z0123456789
     7  ABCDEFGHIJKLM
     8  Once upon a midnight dreary, while I pondered, weak and weary,
     9  Over many a quaint and curious volume of forgotten lore—
    10  While I nodded, nearly napping, suddenly there came a tapping,
    11  As of some one gently rapping—rapping at my chamber door.
    12  "'Tis some visitor," I muttered, "tapping at my chamber door—
    13                                    Only this and nothing more."
    14  The quick brown
    15  fox jump over the
    16  lazy dog. Once upon
    17  this midnight dreary,

Este arquivo é meticulosamente construído de modo que os comprimentos das linhas (incluindo novas linhas) são 2, 4, 6, 8, 10, 12, 14 , 63, 57, 63, 58, 62, 63, 16, 18, 20 , e 22 . Observe que as primeiras seis linhas, portanto, contêm 2 + 4 + 6 + 8 + 10 + 12 = 42 bytes. As duas últimas linhas contêm 20 + 22 bytes, o que é coincidentemente (!) Também 42. (O tamanho total do arquivo é 504.) Então,

$ ls -l foo
-rw-r--r-- 1 myusername mygroupname 504 May 18 04:25 foo

$ sed '1,6d' foo 1<> foo

$ ls -l foo
-rw-r--r-- 1 myusername mygroupname 504 May 18 04:32 foo

$ cat -n foo
     1  ABCDEFGHIJKLM
     2  Once upon a midnight dreary, while I pondered, weak and weary,
     3  Over many a quaint and curious volume of forgotten lore—
     4  While I nodded, nearly napping, suddenly there came a tapping,
     5  As of some one gently rapping—rapping at my chamber door.
     6  "'Tis some visitor," I muttered, "tapping at my chamber door—
     7                                    Only this and nothing more."
     8  The quick brown
     9  fox jump over the
    10  lazy dog. Once upon
    11  this midnight dreary,
    12  lazy dog. Once upon
    13  this midnight dreary,

OK, bom, as primeiras seis linhas desapareceram. O número da linha original7 (“ABCDEFGHIJKLM”) é agora a linha número1. Mas o que é isso? O arquivo passou de 17 linhas para 13. Deve ser 11 (17 a 6). E as duas últimas linhas ("cachorro preguiçoso ... meia-noite triste") estão lá duas vezes.

Esta é uma das armadilhas do operador 1<> - se você não truncar o arquivo de saída, você não pode acabar com um arquivo menor do que aquele com o qual você começou. Especificamente, aqui, a saída de sed '1,6d' foo é de 462 bytes (504−42, desde as primeiras seis linhas contêm 42 bytes), e por isso substitui os primeiros 462 bytes do arquivo de saída - que também é foo . E os primeiros 462 bytes de foo são todos menos os últimos 42 (504 a 462) - então as duas últimas linhas não são sobrescritas. As duas cópias das duas últimas linhas (“cachorro preguiçoso… meia-noite triste”) são uma saída de sed , seguido por um que sobrou do conteúdo original do arquivo.

Então, o que vem depois?

Tudo o que precisamos fazer agora é jogar fora os últimos 42 bytes do arquivo. Quando isso acontece, isso pode ser feito apenas movendo o ponteiro que aponta para o final do arquivo. OK, não é realmente um ponteiro; é um tamanho de arquivo inteiro potAto, potAHto. Nos últimos 20 ou 30 anos, O Unix permitiu que você truncar um arquivo para o tamanho desejado, deixando os dados até aquele ponto intocado, e descartando os dados além desse ponto.

Um antigo comando que fará isso é

dd if=/dev/null bs=462 seek=1 of=foo 2> /dev/null

que copia /dev/null sobre foo , iniciando no byte 462. Sim, é um pouco de kluge. Um novo comando que faz essa função é

truncate -s 462 foo

Isso pode não estar presente em todos os sistemas; não é especificado por POSIX.

Então, juntando tudo,

#!/bin/sh
filename="$1"
bytes_to_remove=$(sed '6q' "$filename" | wc -c)
total_size=$(stat -c '%s' "$filename")
sed '1,6d' "$filename" 1<> "$filename"
new_size=$((total_size - bytes_to_remove))
truncate -s "$new_size" "$filename"

Usamos wc -c para contar os caracteres nas primeiras seis linhas (produzido por sed '6q' ), subtraia isso do tamanho total do arquivo, e truncar o arquivo para esse tamanho. Você pode usar qualquer um dos comandos alternativos para produzir as primeiras linhas M ou as últimas linhas N-M , e você pode substituir a última linha com

dd if=/dev/null bs="$new_size" seek=1 of="$filename" 2> /dev/null

Advertências:

Eu não testei isso em arquivos com

  • terminações de linha CR-LF ou
  • caracteres multibyte,

e isso pode ser problemático.

    
por 18.05.2017 / 04:48
0

Olhando para a fonte da cauda , ela faz não parecem, na verdade, iterar em todo o arquivo. Começa no final e lê de trás para frente até ver o número correto de novas linhas (mais qualquer inserção de uma linha não terminada), observa esse local, pula para esse local e despeja o arquivo (ou canalizado) ou dados introduzidos) daí em diante.

    
por 17.05.2017 / 18:31