Skutecznie usuń dwa ostatnie wiersze bardzo dużego pliku tekstowego


Odpowiedzi:


31

Nie próbowałem tego na dużym pliku, aby zobaczyć, jak to jest szybkie, ale powinno być dość szybkie.

Aby użyć skryptu do usunięcia wierszy z końca pliku:

./shorten.py 2 large_file.txt

Szuka do końca pliku, sprawdza, czy ostatni znak jest nową linią, a następnie odczytuje każdy znak po kolei, cofając się, aż znajdzie trzy nowe linie i obcina plik zaraz po tym punkcie. Zmiana została wprowadzona.

Edycja: Dodałem wersję Python 2.4 na dole.

Oto wersja dla 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)

Oto wersja 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)

Oto wersja 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)

nasz system działa w Pythonie 2.4 i nie jestem pewien, czy którakolwiek z naszych usług na nim polega, czy to zadziała?
Russ Bradberry,

@Russ: Dodałem wersję dla Python 2.4.
Wstrzymano do odwołania.

1
absolutnie wspaniałe! działało jak urok w mniej niż sekundę!
Russ Bradberry

12

możesz spróbować GNU head

head -n -2 file

To najlepsze rozwiązanie, ponieważ jest proste.
xiao

1
Spowoduje to wyświetlenie dwóch ostatnich wierszy pliku, ale nie usunie ich z jego pliku. Nawet nie działa w moim systemiehead: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: Nie, wydrukuje wszystkie linie od początku do 2 linii od końca, zgodnie z instrukcją. Musi to jednak zostać przekierowane do pliku, a następnie istnieje problem z gigantycznym plikiem, więc nie jest to idealne rozwiązanie tego problemu.
Daniel Andersson,

+1 Dlaczego nie jest to akceptowane jako poprawna odpowiedź? Jest szybki, prosty i działa zgodnie z oczekiwaniami.
aefxx

6
@PetrMarek i inni: Problem polegał na tym, że dotyczyło to ogromnego pliku. To rozwiązanie wymagałoby przepuszczenia całego pliku przez potok i przepisania wszystkich danych w nowe miejsce - a sednem pytania jest uniknięcie tego. Potrzebne jest rozwiązanie na miejscu, takie jak to w zaakceptowanej odpowiedzi.
Daniel Andersson,

7

Widzę, że moje systemy wyciskania / testowania Debiana (ale nie Lenny / stable) zawierają polecenie „obcinania” jako część pakietu „coreutils”.

Dzięki niemu możesz po prostu zrobić coś takiego

truncate --size=-160 myfile

aby usunąć 160 bajtów z końca pliku (oczywiście musisz dokładnie ustalić, ile znaków musisz usunąć).


Będzie to najszybsza trasa, ponieważ modyfikuje plik w miejscu, a zatem nie wymaga kopiowania ani analizowania pliku. Jednak nadal musisz sprawdzić, ile bajtów usunąć ... I / zgaduję / że ddzrobi to prosty skrypt (musisz określić przesunięcie wejściowe, aby uzyskać np. Ostatni kilobajt, a następnie użyć tail -2 | LANG= wc -c, lub coś takiego).
liori

Używam CentOS, więc nie, nie mam obcinania. Jednak tego właśnie szukam.
Russ Bradberry,

tailjest również skuteczny w przypadku dużych plików - można go użyć tail | wc -cdo obliczenia liczby bajtów do przycięcia.
krlmlr

6

Problem z sed polega na tym, że jest to edytor strumieniowy - przetworzy cały plik, nawet jeśli chcesz wprowadzać modyfikacje tylko pod koniec. Niezależnie od tego, tworzysz nowy plik 400 GB, linia po linii. Każdy edytor działający na całym pliku prawdopodobnie będzie miał ten problem.

Jeśli znasz liczbę wierszy, możesz użyć head, ale ponownie tworzy to nowy plik zamiast zmieniać istniejący. Wydaje mi się, że możesz uzyskać wzrost prędkości dzięki prostocie akcji.

Państwo może mieć więcej szczęścia przy użyciu splitzłamać plik na mniejsze kawałki, edytując ostatni, a następnie za pomocą catpołączyć je ponownie, ale nie jestem pewien, czy to będzie lepiej. Użyłbym raczej liczby bajtów niż linii, w przeciwnym razie prawdopodobnie nie będzie to wcale szybsze - nadal będziesz tworzyć nowy plik 400 GB.


2

Wypróbuj VIM ... Nie jestem pewien, czy to zrobi, czy nie, ponieważ nigdy nie użyłem go na tak dużym pliku, ale użyłem go na mniejszych większych plikach w przeszłości, spróbuj.


Wierzę, że vim ładuje tylko to, co jest bezpośrednio wokół bufora podczas edycji , jednak nie mam pojęcia, jak to oszczędza.
Phoshi,

vim zawiesza się podczas próby załadowania pliku
Russ Bradberry

Cóż, jeśli się zawiesza, ah poczekaj na to. Rozpocznij ładowanie, idź do pracy, wróć do domu, sprawdź, czy to zrobione.
leeand00


1

Jakiego rodzaju plik iw jakim formacie? Może być łatwiejsze w użyciu coś takiego jak Perl w zależności od rodzaju pliku - tekst, grafika, plik binarny? Jak jest sformatowany - CSV, TSV ...


jest to sformatowany tekst
rozdzielany potokami

czy naprawianie czegoś, co „import” zajmuje się tą sprawą, jest opcją?
dzisiaj

nie, import jest infilight „ładuj dane infile”
Russ Bradberry

1

Jeśli znasz rozmiar pliku do bajtu (powiedzmy 400000000160) i wiesz, że musisz usunąć dokładnie 160 znaków, aby usunąć dwa ostatnie wiersze, to coś w rodzaju

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

powinien załatwić sprawę. Minęły wieki, odkąd użyłem dd w gniewie; Wydaje mi się, że pamiętam, że rzeczy idą szybciej, jeśli używasz większego rozmiaru bloku, ale to, czy możesz to zrobić, zależy od tego, czy linie, które chcesz upuścić, są w dobrym wielokrotności.

dd ma kilka innych opcji wypisywania rekordów tekstowych do ustalonego rozmiaru, które mogą być przydatne jako wstępne przejście.


Próbowałem tego, ale prędkość była zbliżona do sed. Napisał około 200 MB w 10 minut, przy tym tempie zajęłoby dosłownie setki godzin.
Russ Bradberry,

1

Jeśli polecenie „obcinania” nie jest dostępne w twoim systemie (patrz moja inna odpowiedź), spójrz na „obcinanie man 2” dla wywołania systemowego w celu obcinania pliku do określonej długości.

Oczywiście musisz wiedzieć, do ilu znaków musisz obciąć plik (rozmiar minus długość problemu dwie linie; nie zapomnij policzyć znaków cr / lf).

I wykonaj kopię zapasową pliku, zanim spróbujesz!


1

Jeśli wolisz rozwiązania w stylu uniksowym, możesz zapisać i interaktywne obcinanie linii za pomocą trzech linii kodu (testowane na komputerach Mac i Linux).

małe + bezpieczne obcinanie linii w stylu uniksowym (prosi o potwierdzenie):

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

To rozwiązanie opiera się na kilku popularnych narzędziach uniksowych, ale nadal stosuje się je perl -e "truncate(file,length)"jako najbliższą zamiennik dla truncate(1), który nie jest dostępny we wszystkich systemach.

Możesz także skorzystać z następującego kompleksowego programu powłoki wielokrotnego użytku, który zawiera informacje o użytkowaniu oraz funkcje potwierdzania obcięcia, analizy opcji i obsługi błędów.

kompleksowy skrypt obcięcia linii :

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

Oto przykład użycia:

$ 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” << TUTAJ
$
re
re
w
TUTAJ

zmiany są wprowadzane. Jest to prostsze i bardziej wydajne niż skrypt Pythona.


W moim systemie użycie pliku tekstowego składającego się z miliona linii i ponad 57 MB edzajęło 100 razy więcej czasu niż wykonanie skryptu w języku Python. Mogę sobie tylko wyobrazić, o ile większa byłaby różnica dla pliku OP, który jest 7000 razy większy.
Wstrzymano do odwołania.

0

Zmodyfikowano zaakceptowaną odpowiedź, aby rozwiązać podobny problem. Można go trochę ulepszyć, aby usunąć n linii.

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

I odpowiedni 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

Możesz używać Vima w trybie Ex:

ex -sc '-,d|x' file
  1. -, wybierz ostatnie 2 linie

  2. d kasować

  3. x Zapisz i zamknij

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.