Remover com eficiência as últimas duas 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 ele durou horas antes de eu desistir. Existe uma maneira rápida de fazer isso, ou estou preso com sed ?

    
por Russ Bradberry 06.04.2010 / 01:25

12 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 busca até o final do arquivo, verifica se o último caractere é uma nova linha e, em seguida, lê cada caractere, um de cada vez, até encontrar três novas linhas e truncar o arquivo logo após esse ponto. A mudança é feita no lugar.

Editar: adicionei uma versão do Python 2.4 na parte inferior.

Aqui está uma versão para o 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)
    
por 06.04.2010 / 04:28
12

você pode experimentar a cabeça GNU

head -n -2 file
    
por 06.04.2010 / 07:54
7

Eu vejo que meus sistemas Debian Squeeze / testing (mas não Lenny / stable) incluem um comando "truncar" 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).

    
por 06.04.2010 / 14:53
6

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

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

Você pode ter mais sorte usando split para dividir o arquivo em partes menores, editando o último e usando cat para 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 criará um novo arquivo de 400 GB.

    
por 06.04.2010 / 01:37
2

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

    
por 06.04.2010 / 01:27
1

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

    
por 06.04.2010 / 01:36
1

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

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

deve fazer o truque. Já faz muito tempo desde que eu usei dd em raiva; Eu pareço lembrar que as coisas vão mais rápido se você usar um tamanho de bloco maior, mas se você pode fazer isso depende se as linhas que você deseja eliminar estão em um bom múltiplo.

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

    
por 06.04.2010 / 14:36
1

Se o comando "truncate" não estiver disponível em seu sistema (veja minha outra resposta), veja o "man 2 truncate" para a chamada do sistema para truncar um arquivo para um tamanho especificado.

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

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

    
por 06.04.2010 / 14:44
1

Se você preferir soluções em estilo unix, poderá salvar e truncar linhas interativas usando três linhas de código (testado no Mac e no Linux).

truncamento de linhas pequenas e seguras no estilo unix (pede 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' )"

Esta solução se baseia em algumas ferramentas unix comuns, mas ainda usa perl -e "truncate(file,length)" como substituto mais próximo de truncate(1) , que não está disponível em todos os sistemas.

Você também pode usar o seguinte programa shell reutilizável abrangente, que fornece informações de uso e recursos de 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
    
por 04.06.2015 / 18:25
0
#!/bin/sh

ed "$1" << HERE
$
d
d
w
HERE

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

    
por 07.04.2010 / 02:02
0

Modificou a resposta aceita para resolver um problema semelhante. Poderia ser mexido 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()
    
por 05.03.2013 / 07:19
0

Você pode usar o Vim no modo Ex:

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

  2. d delete

  3. x salvar e fechar

por 17.04.2016 / 00:02