Remova com eficiência as duas últimas linhas de um arquivo de texto extremamente grande

31

Eu tenho um arquivo muito grande (~ 400 GB) e preciso remover as duas últimas linhas dele. Eu tentei usar sed, mas funcionou por horas antes de desistir. Existe uma maneira rápida de fazer isso, ou eu estou preso sed?

Russ Bradberry
fonte
6
você pode experimentar o GNU head. head -n -2 file
usar o seguinte comando
Havia um par de uma linha de Perl e Java sugestões dadas em stackoverflow.com/questions/2580335/...
MTRW

Respostas:

31

Eu não tentei isso em um arquivo grande para ver o quão rápido é, mas deve ser bastante rápido.

Para usar o script para remover linhas do final de um arquivo:

./shorten.py 2 large_file.txt

Ele procura o final do arquivo, verifica se o último caractere é uma nova linha, depois lê cada caractere um de cada vez, retrocedendo até encontrar três novas linhas e truncar o arquivo logo após esse ponto. A alteração é feita no local.

Edit: Adicionei uma versão do Python 2.4 na parte inferior.

Aqui está uma versão do Python 2.5 / 2.6:

#!/usr/bin/env python2.5
from __future__ import with_statement
# also tested with Python 2.6

import os, sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b') as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        char = f.read(1)
        if char != '\n' and f.tell() == end:
            print "No change: file does not end with a newline"
            exit(1)
        if char == '\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print "Removed " + str(number) + " lines from end of file"
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    exit(3)

Aqui está uma versão do Python 3:

#!/usr/bin/env python3.0

import os, sys

if len(sys.argv) != 3:
    print(sys.argv[0] + ": Invalid number of arguments.")
    print ("Usage: " + sys.argv[0] + " linecount filename")
    print ("to remove linecount lines from the end of the file")
    exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0

with open(file,'r+b', buffering=0) as f:
    f.seek(0, os.SEEK_END)
    end = f.tell()
    while f.tell() > 0:
        f.seek(-1, os.SEEK_CUR)
        print(f.tell())
        char = f.read(1)
        if char != b'\n' and f.tell() == end:
            print ("No change: file does not end with a newline")
            exit(1)
        if char == b'\n':
            count += 1
        if count == number + 1:
            f.truncate()
            print ("Removed " + str(number) + " lines from end of file")
            exit(0)
        f.seek(-1, os.SEEK_CUR)

if count < number + 1:
    print("No change: requested removal would leave empty file")
    exit(3)

Aqui está uma versão do Python 2.4:

#!/usr/bin/env python2.4

import sys

if len(sys.argv) != 3:
    print sys.argv[0] + ": Invalid number of arguments."
    print "Usage: " + sys.argv[0] + " linecount filename"
    print "to remove linecount lines from the end of the file"
    sys.exit(2)

number = int(sys.argv[1])
file = sys.argv[2]
count = 0
SEEK_CUR = 1
SEEK_END = 2

f = open(file,'r+b')
f.seek(0, SEEK_END)
end = f.tell()

while f.tell() > 0:
    f.seek(-1, SEEK_CUR)
    char = f.read(1)
    if char != '\n' and f.tell() == end:
        print "No change: file does not end with a newline"
        f.close()
        sys.exit(1)
    if char == '\n':
        count += 1
    if count == number + 1:
        f.truncate()
        print "Removed " + str(number) + " lines from end of file"
        f.close()
        sys.exit(0)
    f.seek(-1, SEEK_CUR)

if count < number + 1:
    print "No change: requested removal would leave empty file"
    f.close()
    sys.exit(3)
Pausado até novo aviso.
fonte
nosso sistema está executando o python 2.4 e não tenho certeza se algum de nossos serviços depende dele, isso funcionará nisso?
Russ Bradberry
@ Russ: Eu adicionei uma versão para Python 2.4.
Pausado até novo aviso.
11
absolutamente surpreendente! funcionou como um encanto e em menos de um segundo!
Russ Bradberry
12

você pode tentar o GNU head

head -n -2 file
user31894
fonte
É a melhor solução, pois é simples.
X16
11
Isto irá mostrar-lhe as duas últimas linhas do arquivo, mas não removê-los do seu file..an não mesmo trabalhar no meu sistemahead: illegal line count -- -2
SooDesuNe
2
@SooDesuNe: Não, imprimirá todas as linhas do início até 2 linhas do final, conforme o manual. No entanto, isso precisaria ser redirecionado para um arquivo e, em seguida, há o problema desse arquivo ser gigante, portanto não é a solução perfeita para esse problema.
Daniel Andersson
+1 Por que isso não está sendo aceito como a resposta correta? É rápido, simples e funciona como esperado.
Aefxx 12/09/12
6
@PetrMarek e outros: O problema era que se tratava de um arquivo gigante . Essa solução exigiria que o arquivo inteiro fosse alimentado por um pipe e reescrevesse todos os dados em um novo local - e o ponto principal da questão é evitar isso. É necessária uma solução no local, como a da resposta aceita.
Daniel Andersson
7

Eu vejo que meus sistemas Debian Squeeze / testing (mas não o Lenny / stable) incluem um comando "truncate" como parte do pacote "coreutils".

Com isso, você poderia simplesmente fazer algo como

truncate --size=-160 myfile

para remover 160 bytes do final do arquivo (obviamente você precisa descobrir exatamente quantos caracteres você precisa remover).

timday
fonte
Essa será a rota mais rápida, pois modifica o arquivo no local e, portanto, não requer cópia nem análise do arquivo. No entanto, você ainda precisará verificar quantos bytes ddserão removidos ... Acho que um script simples fará isso (você precisa especificar o deslocamento da entrada para obter o último kilobyte fe e depois usá-lo tail -2 | LANG= wc -cou sth assim).
Liori 06/04
Estou usando o CentOS, então não, eu não tenho truncado. No entanto, é exatamente isso que estou procurando.
Russ Bradberry
tailtambém é eficiente para arquivos grandes - pode ser usado tail | wc -cpara calcular o número de bytes a serem aparados.
precisa saber é o seguinte
6

O problema com o sed é que ele é um editor de fluxo - ele processará o arquivo inteiro, mesmo que você queira fazer modificações apenas no final. Então, não importa o quê, você está criando um novo arquivo de 400 GB, linha por linha. Qualquer editor que opere no arquivo inteiro provavelmente terá esse problema.

Se você souber o número de linhas, poderá usar head, mas novamente isso cria um novo arquivo em vez de alterar o existente. Você pode obter ganhos de velocidade com a simplicidade da ação, eu acho.

Você pode ter mais sorte usando splitpara quebrar o arquivo em pedaços menores, editando o último e, em seguida, usando-os catpara combiná-los novamente, mas não tenho certeza se será melhor. Eu usaria contagens de bytes em vez de linhas, caso contrário provavelmente não será mais rápido - você ainda estará criando um novo arquivo de 400 GB.

Zac Thompson
fonte
2

Experimente o VIM ... Não tenho certeza se isso funcionará ou não, pois nunca o usei em um arquivo tão grande, mas o usei em arquivos maiores menores no passado, tente.

leeand00
fonte
Eu acredito que o vim carrega apenas o que está imediatamente ao redor do buffer durante a edição , no entanto, não faço ideia de como ele salva.
Phoshi
vim trava enquanto ele tenta carregar o arquivo
Russ Bradberry
Bem, se travar, espero. Inicie o carregamento, vá para o trabalho, volte para casa e veja se está pronto.
leeand00
1

Que tipo de arquivo e em qual formato? Pode ser mais fácil usar algo como Perl, dependendo do tipo de arquivo - texto, gráficos, binário? Como é formatado - CSV, TSV ...

Blackbeagle
fonte
ele é formatado texto tubulação delimeted, no entanto as últimas 2 linhas são uma coluna de cada uma, que vai quebrar minha importação então eu preciso removê-los
Russ Bradberry
está corrigindo o que é que a "importação" para lidar com este caso é uma opção?
timday
sem a importação é de Infobright "dados de carga INFILE"
Russ Bradberry
1

Se você sabe o tamanho do arquivo no byte (digamos 400000000160) e sabe que precisa remover exatamente 160 caracteres para remover as duas últimas linhas, algo como

dd if=originalfile of=truncatedfile ibs=1 count=400000000000

deve fazer o truque. Já faz muito tempo desde que eu usei dd com raiva; Parece que lembro que as coisas ficam mais rápidas se você usar um tamanho de bloco maior, mas se você pode fazer isso depende se as linhas que você deseja soltar estão em um bom múltiplo.

O dd tem outras opções para preencher os registros de texto em um tamanho fixo, o que pode ser útil como passe preliminar.

timday
fonte
Eu tentei isso, mas estava indo na mesma velocidade que sed. Ele havia escrito aproximadamente 200 MB em 10 minutos; nesse ritmo, levaria literalmente centenas de horas para ser concluído.
Russ Bradberry
1

Se o comando "truncar" não estiver disponível no seu sistema (veja minha outra resposta), consulte o "man 2 truncar" da chamada do sistema para truncar um arquivo com um comprimento especificado.

Obviamente, você precisa saber quantos caracteres precisa truncar o arquivo (tamanho menos o comprimento do problema em duas linhas; não se esqueça de contar caracteres cr / lf).

E faça um backup do arquivo antes de tentar isso!

timday
fonte
1

Se você preferir soluções no estilo unix, poderá ter truncamento de linha interativo e salvo usando três linhas de código (Testado no Mac e Linux).

pequeno + truncamento de linha no estilo unix seguro (solicita confirmação):

n=2; file=test.csv; tail -n $n $file &&
read -p "truncate? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', `wc -c <$file` - `tail -n $n $file | wc -c` )"

Essa solução conta com algumas ferramentas unix comuns, mas ainda é usada perl -e "truncate(file,length)"como substituta mais próxima truncate(1), o que não está disponível em todos os sistemas.

Você também pode usar o seguinte programa abrangente de shell reutilizável, que fornece informações de uso e apresenta confirmação de truncamento, análise de opções e tratamento de erros.

script de truncamento de linha abrangente :

#!/usr/bin/env bash

usage(){
cat <<-EOF
  Usage:   $0 [-n NUM] [-h] FILE
  Options:
  -n NUM      number of lines to remove (default:1) from end of FILE
  -h          show this help
EOF
exit 1
}

num=1

for opt in $*; do case $opt in
  -n) num=$2;                 shift;;
  -h) usage;                  break;;
  *)  [ -f "$1" ] && file=$1; shift;;
esac done

[ -f "$file" ] || usage

bytes=`wc -c <$file`
size=`tail -n $num $file | wc -c`

echo "using perl 'truncate' to remove last $size of $bytes bytes:"
tail -n $num $file
read -p "truncate these lines? (y/N)" -n1 key && [ "$key" == "y" ] &&
perl -e "truncate('$file', $bytes - $size )"; echo ""
echo "new tail is:"; tail $file

Aqui está um exemplo de uso:

$ cat data/test.csv
1 nice data
2 cool data
3 just data

GARBAGE to be removed (incl. empty lines above and below)

$ ./rmtail.sh -n 3 data/test.csv
using perl 'truncate' to remove last 60 of 96 bytes:

GARBAGE to be removed (incl. empty lines above and below)

truncate these lines? (y/N)y
new tail is:
1 nice data
2 cool data
3 just data
$ cat data/test.csv
1 nice data
2 cool data
3 just data
Juve
fonte
0
#! / bin / sh

ed "$ 1" << AQUI
$
d
d
W
AQUI

mudanças são feitas no local. Isso é mais simples e mais eficiente que o script python.

Justin Smith
fonte
No meu sistema, usando um arquivo de texto composto por um milhão de linhas e mais de 57 MB, eddemorou 100 vezes mais tempo para ser executado do que o meu script Python. Posso apenas imaginar quanto mais a diferença seria para o arquivo do OP, que é 7000 vezes maior.
Pausado até novo aviso.
0

Modificou a resposta aceita para resolver um problema semelhante. Pode ser ajustado um pouco para remover n linhas.

import os

def clean_up_last_line(file_path):
    """
    cleanup last incomplete line from a file
    helps with an unclean shutdown of a program that appends to a file
    if \n is not the last character, remove the line
    """
    with open(file_path, 'r+b') as f:
        f.seek(0, os.SEEK_END)

        while f.tell() > 0: ## current position is greater than zero
            f.seek(-1, os.SEEK_CUR)

            if f.read(1) == '\n':
                f.truncate()
                break

            f.seek(-1, os.SEEK_CUR) ## don't quite understand why this has to be called again, but it doesn't work without it

E o teste correspondente:

import unittest

class CommonUtilsTest(unittest.TestCase):

    def test_clean_up_last_line(self):
        """
        remove the last incomplete line from a huge file
        a line is incomplete if it does not end with a line feed
        """
        file_path = '/tmp/test_remove_last_line.txt'

        def compare_output(file_path, file_data, expected_output):
            """
            run the same test on each input output pair
            """
            with open(file_path, 'w') as f:
                f.write(file_data)

            utils.clean_up_last_line(file_path)

            with open(file_path, 'r') as f:
                file_data = f.read()
                self.assertTrue(file_data == expected_output, file_data)        

        ## test a multiline file
        file_data = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
136235"""

        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b
1362358458954466,2013-03-03 16:54:18,34.5,3.0,b
1362358630923094,2013-03-03 16:57:10,34.5,50.0,b
"""        
        compare_output(file_path, file_data, expected_output)

        ## test a file with no line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"
        compare_output(file_path, file_data, expected_output)

        ## test a file a leading line break
        file_data = u"""\n1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b"""
        expected_output = "\n"
        compare_output(file_path, file_data, expected_output)

        ## test a file with one line break
        file_data = u"""1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        expected_output = """1362358424445914,2013-03-03 16:53:44,34.5,151.16345879,b\n""" 
        compare_output(file_path, file_data, expected_output)

        os.remove(file_path)


if __name__ == '__main__':
    unittest.main()
tponthieux
fonte
0

Você pode usar o Vim no modo Ex:

ex -sc '-,d|x' file
  1. -, selecione as últimas 2 linhas

  2. d excluir

  3. x salvar e fechar

Steven Penny
fonte