Entfernen Sie effizient die letzten zwei Zeilen einer extrem großen Textdatei


Antworten:


31

Ich habe dies bei einer großen Datei nicht versucht, um zu sehen, wie schnell es ist, aber es sollte ziemlich schnell sein.

So entfernen Sie mit dem Skript Zeilen am Ende einer Datei:

./shorten.py 2 large_file.txt

Es sucht bis zum Ende der Datei, prüft, ob das letzte Zeichen eine neue Zeile ist, liest dann jedes Zeichen einzeln rückwärts, bis drei neue Zeilen gefunden wurden, und schneidet die Datei unmittelbar nach diesem Punkt ab. Die Änderung wurde vorgenommen.

Bearbeiten: Ich habe eine Python 2.4-Version am unteren Rand hinzugefügt.

Hier ist eine Version für 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)

Hier ist eine Python 3-Version:

#!/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)

Hier ist eine Python 2.4-Version:

#!/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)

Auf unserem System läuft Python 2.4, und ich bin mir nicht sicher, ob einer unserer Dienste darauf angewiesen ist. Funktioniert das in diesem Fall?
Russ Bradberry

@Russ: Ich habe eine Version für Python 2.4 hinzugefügt.
Bis auf weiteres angehalten.

1
Einfach unglaublich! Arbeitete wie ein Zauber und in weniger als einer Sekunde!
Russ Bradberry

12

Sie können GNU Kopf versuchen

head -n -2 file

Es ist die beste Lösung, da es einfach ist.
Xiao

1
Dies wird ihm die letzten beiden Zeilen der Datei anzeigen, aber nicht entfernt sie von seinem file..an ist auch auf meinem System nicht funktionierenhead: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: Nein, es werden alle Zeilen vom Anfang bis zu 2 Zeilen vom Ende gedruckt, wie im Handbuch angegeben. Dies müsste jedoch in eine Datei umgeleitet werden, und dann besteht das Problem, dass diese Datei riesig ist. Daher ist dies nicht die perfekte Lösung für dieses Problem.
Daniel Andersson

+1 Warum wird dies nicht als die richtige Antwort akzeptiert? Es ist schnell, einfach und funktioniert wie erwartet.
aefxx

6
@PetrMarek und andere: Das Problem war, dass es sich um eine riesige Datei handelte. Diese Lösung würde erfordern, dass die gesamte Datei durch eine Pipe geleitet und alle Daten an einen neuen Speicherort geschrieben werden - und der springende Punkt ist, dies zu vermeiden. Es ist eine In-Place-Lösung erforderlich, wie sie in der akzeptierten Antwort enthalten ist.
Daniel Andersson

7

Ich sehe, dass meine Debian Squeeze / Testing-Systeme (aber nicht Lenny / stable) einen "Truncate" -Befehl als Teil des "Coreutils" -Pakets enthalten.

Damit könnte man einfach so etwas machen

truncate --size=-160 myfile

um 160 Bytes vom Ende der Datei zu entfernen (offensichtlich müssen Sie genau herausfinden, wie viele Zeichen Sie entfernen müssen).


Dies ist die schnellste Route, da die Datei direkt geändert wird und daher weder kopiert noch analysiert werden muss. Sie müssen jedoch immer noch prüfen, wie viele Bytes entfernt werden müssen ... Ich vermute, dass ein einfaches ddSkript dies tun wird (Sie müssen den Eingabeversatz angeben, um zB das letzte Kilobyte zu erhalten und dann zu verwenden tail -2 | LANG= wc -c, oder so etwas).
Liori

Ich verwende CentOS, also nein, ich habe keine abgeschnittenen. Dies ist jedoch genau das, wonach ich suche.
Russ Bradberry

tailist auch für große Dateien effizient - kann verwendet werden tail | wc -c, um die Anzahl der zu schneidenden Bytes zu berechnen.
krlmlr

6

Das Problem bei sed ist, dass es sich um einen Stream-Editor handelt - er verarbeitet die gesamte Datei, auch wenn Sie erst gegen Ende Änderungen vornehmen möchten. Auf jeden Fall erstellen Sie zeilenweise eine neue 400-GB-Datei. Jeder Editor, der die gesamte Datei bearbeitet, wird wahrscheinlich dieses Problem haben.

Wenn Sie die Anzahl der Zeilen kennen, können Sie headdiese verwenden. Dadurch wird jedoch eine neue Datei erstellt, anstatt die vorhandene zu ändern. Sie könnten Geschwindigkeitsgewinne durch die Einfachheit der Aktion erzielen, denke ich.

Sie könnte mehr Glück mit splitder Datei in kleinere Stücke zu brechen, die letzte Bearbeitung, und dann mit catwieder , sie zu kombinieren, aber ich bin nicht sicher , ob es nicht besser sein. Ich würde eher die Anzahl der Bytes als die Anzahl der Zeilen verwenden, sonst wird es wahrscheinlich gar nicht schneller - Sie werden immer noch eine neue 400-GB-Datei erstellen.


2

Versuchen Sie es mit VIM ... Ich bin mir nicht sicher, ob es funktioniert oder nicht, da ich es noch nie für eine so große Datei verwendet habe, aber ich habe es in der Vergangenheit für kleinere, größere Dateien verwendet, probieren Sie es aus.


Ich glaube, vim lädt nur das, was beim Bearbeiten sofort um den Puffer herum ist , aber ich habe keine Ahnung, wie es sich spart.
Phoshi

vim bleibt hängen, während versucht wird, die Datei zu laden
Russ Bradberry

Nun, wenn es hängt, ach warte darauf. Beginnen Sie mit dem Laden, gehen Sie zur Arbeit, kommen Sie nach Hause und sehen Sie, ob es fertig ist.
Leeand00


1

Welche Art von Datei und in welchem ​​Format? Kann es einfacher sein, etwas wie Perl zu verwenden, abhängig davon, um welche Art von Datei es sich handelt - Text, Grafiken, Binärdateien? Wie ist es formatiert - CSV, TSV ...


Es ist formatierter Text mit Pipe-Begrenzung. Die letzten beiden Zeilen bestehen jedoch jeweils aus einer Spalte. Dadurch wird mein Import unterbrochen und ich muss sie entfernen.
Russ Bradberry

Ist es eine Option, den "Import" für diesen Fall zu korrigieren?
am

Nein, der Import ist Infobrights "Load Data Infile"
Russ Bradberry

1

Wenn Sie die Größe der Datei auf das Byte (400000000160 sagen) kennen und wissen, dass Sie genau 160 Zeichen entfernen müssen, um die letzten beiden Zeilen zu entfernen, dann ist so etwas wie

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

sollte den Trick machen. Es ist schon eine Ewigkeit her, dass ich dd im Zorn benutzt habe. Ich erinnere mich, dass die Dinge schneller gehen, wenn Sie einen größeren Block verwenden, aber ob Sie dies tun können, hängt davon ab, ob die Zeilen, die Sie löschen möchten, ein nettes Vielfaches haben.

dd verfügt über einige andere Optionen zum Auffüllen von Textdatensätzen mit einer festen Größe, die als vorläufiger Durchgang nützlich sein kann.


Ich habe es versucht, aber es lief ungefähr so ​​schnell wie sed. Es hatte ungefähr 200 MB in 10 Minuten geschrieben, bei dieser Geschwindigkeit würde es buchstäblich Hunderte von Stunden dauern, bis es fertig war.
Russ Bradberry

1

Wenn der Befehl "Truncate" auf Ihrem System nicht verfügbar ist (siehe meine andere Antwort), sehen Sie sich "Man 2 Truncate" für den Systemaufruf an, um eine Datei auf eine bestimmte Länge zu kürzen.

Natürlich müssen Sie wissen, auf wie viele Zeichen Sie die Datei kürzen müssen (Größe abzüglich der Länge der zwei Zeilen des Problems; vergessen Sie nicht, alle cr / lf-Zeichen zu zählen).

Erstellen Sie eine Sicherungskopie der Datei, bevor Sie dies versuchen!


1

Wenn Sie Lösungen im Unix-Stil bevorzugen, können Sie die Zeilen mithilfe von drei Codezeilen speichern und interaktiv abschneiden (Getestet auf Mac und Linux).

Small + Safe-Unix-Zeilenabbruch (Bestätigung erforderlich):

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` )"

Diese Lösung basiert auf ein paar gängigen Unix-Tools, verwendet aber immer noch den perl -e "truncate(file,length)"nächstliegenden Ersatz für truncate(1), der nicht auf allen Systemen verfügbar ist.

Sie können auch das folgende umfassende Programm für wiederverwendbare Shells verwenden, das Informationen zur Verwendung enthält und eine Bestätigung der Kürzung, Analyse von Optionen und Fehlerbehandlung bietet.

Umfassendes Skript zum Abschneiden von Zeilen :

#!/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

Hier ist ein Anwendungsbeispiel:

$ 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

0
#! / bin / sh

ed "$ 1" << HIER
$
d
d
w
HIER

Änderungen werden vorgenommen. Dies ist einfacher und effizienter als das Python-Skript.


Auf meinem System eddauerte die Ausführung einer aus einer Million Zeilen und über 57 MB bestehenden Textdatei 100-mal so lange wie bei meinem Python-Skript. Ich kann mir nur vorstellen, wie viel mehr der Unterschied für die 7000-fach größere OP-Datei wäre.
Bis auf weiteres angehalten.

0

Die akzeptierte Antwort wurde geändert, um ein ähnliches Problem zu lösen. Könnte ein wenig optimiert werden, um n Zeilen zu entfernen.

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

Und der entsprechende Test:

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()

0

Sie können Vim im Ex-Modus verwenden:

ex -sc '-,d|x' file
  1. -, wähle die letzten 2 Zeilen aus

  2. d löschen

  3. x speichern und schließen

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.