Secara efisien menghapus dua baris terakhir dari file teks yang sangat besar

31

Saya memiliki file yang sangat besar (~ 400 GB), dan saya harus menghapus 2 baris terakhir darinya. Saya mencoba menggunakan sed, tetapi itu berjalan selama berjam-jam sebelum saya menyerah. Apakah ada cara cepat untuk melakukan ini, atau saya terjebak sed?

Russ Bradberry
sumber
6
Anda bisa mencoba GNU head. head -n -2 file
user31894
Ada beberapa saran Perl dan Java satu baris yang diberikan di stackoverflow.com/questions/2580335/…
mtrw

Jawaban:

31

Saya belum mencoba ini pada file besar untuk melihat seberapa cepat itu, tetapi harus cukup cepat.

Untuk menggunakan skrip untuk menghapus baris dari akhir file:

./shorten.py 2 large_file.txt

Itu mencari ke akhir file, memeriksa untuk memastikan karakter terakhir adalah baris baru, kemudian membaca setiap karakter satu per satu akan mundur hingga ditemukan tiga baris baru dan memotong file tepat setelah titik itu. Perubahan dilakukan di tempat.

Sunting: Saya telah menambahkan versi Python 2.4 di bagian bawah.

Ini adalah versi untuk 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)

Ini versi 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)

Berikut adalah versi 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)
Dijeda sampai pemberitahuan lebih lanjut.
sumber
sistem kami menjalankan python 2.4, dan saya tidak yakin apakah ada layanan kami yang bergantung padanya, apakah ini akan berhasil?
Russ Bradberry
@Russ: Saya telah menambahkan versi untuk Python 2.4.
Dijeda sampai pemberitahuan lebih lanjut.
1
Benar benar menakjubkan! bekerja seperti pesona dan dalam waktu kurang dari sedetik!
Russ Bradberry
12

Anda dapat mencoba kepala GNU

head -n -2 file
pengguna31894
sumber
Ini adalah solusi terbaik karena sederhana.
xiao
1
Ini akan menunjukkan kepadanya dua baris terakhir file, tetapi tidak menghapusnya dari file-nya .. bahkan tidak berfungsi pada sistem sayahead: illegal line count -- -2
SooDesuNe
2
@ SoooDesuNe: Tidak, itu akan mencetak semua baris dari awal hingga 2 baris dari akhir, sesuai manual. Namun, ini perlu diarahkan ke file, dan kemudian ada masalah dengan file ini menjadi raksasa, jadi itu bukan solusi yang sempurna untuk masalah ini.
Daniel Andersson
+1 Mengapa ini tidak diterima sebagai jawaban yang benar? Cepat, sederhana, dan bekerja seperti yang diharapkan.
aefxx
6
@PetrMarek dan yang lainnya: Masalahnya adalah menyangkut file raksasa . Solusi ini akan membutuhkan seluruh file untuk diumpankan melalui pipa dan menulis ulang semua data ke lokasi baru - dan inti pertanyaannya adalah untuk menghindari itu. Diperlukan solusi di tempat, seperti yang ada di jawaban yang diterima.
Daniel Andersson
7

Saya melihat sistem Squian / pengujian Debian saya (tetapi bukan Lenny / stable) menyertakan perintah "truncate" sebagai bagian dari paket "coreutils".

Dengan itu Anda bisa melakukan sesuatu seperti

truncate --size=-160 myfile

untuk menghapus 160 byte dari akhir file (jelas Anda perlu mencari tahu persis berapa banyak karakter yang perlu Anda hapus).

timday
sumber
Ini akan menjadi rute tercepat karena memodifikasi file di tempat, dan oleh karena itu tidak memerlukan menyalin atau parsing file. Namun, Anda masih perlu memeriksa berapa banyak byte untuk dihapus ... Saya / tebak / bahwa ddskrip sederhana akan melakukan itu (Anda perlu menentukan offset input untuk mendapatkan kilobyte terakhir dan kemudian menggunakan tail -2 | LANG= wc -c, atau sth seperti itu).
liori
Saya menggunakan CentOS, jadi tidak, saya tidak punya truncate. Namun, inilah tepatnya yang saya cari.
Russ Bradberry
tailefisien untuk file besar, juga - dapat digunakan tail | wc -cuntuk menghitung jumlah byte yang akan dipangkas.
krlmlr
6

Masalah dengan sed adalah bahwa itu adalah editor aliran - itu akan memproses seluruh file bahkan jika Anda hanya ingin membuat modifikasi di akhir. Jadi, apa pun yang terjadi, Anda membuat file 400GB baru, baris demi baris. Editor apa pun yang beroperasi pada seluruh file mungkin akan mengalami masalah ini.

Jika Anda tahu jumlah garis, Anda bisa menggunakan head , tetapi sekali lagi ini membuat file baru alih-alih mengubah yang sudah ada di tempat. Anda mungkin mendapatkan keuntungan cepat dari kesederhanaan tindakan, saya kira.

Anda mungkin lebih beruntung menggunakan splituntuk memecah file menjadi potongan-potongan kecil, mengedit yang terakhir, dan kemudian menggunakan catuntuk menggabungkannya lagi, tapi saya tidak yakin apakah itu akan lebih baik. Saya akan menggunakan jumlah byte daripada garis, jika tidak, mungkin tidak akan lebih cepat sama sekali - Anda masih akan membuat file 400GB baru.

Zac Thompson
sumber
2

Coba VIM ... Saya tidak yakin apakah ini akan berhasil atau tidak, karena saya belum pernah menggunakannya pada file sebesar itu, tapi saya sudah menggunakannya pada file yang lebih kecil sebelumnya, coba saja.

leeand00
sumber
Saya percaya vim hanya memuat apa yang langsung ada di sekitar buffer saat mengedit , namun saya tidak tahu bagaimana cara menyimpannya.
Phoshi
vim hang ketika mencoba memuat file
Russ Bradberry
Nah jika hang, ah tunggu dulu. Mulai memuat, pergi bekerja, pulang, lihat apakah sudah selesai.
leeand00
1

Jenis file apa dan dalam format apa? Mungkin lebih mudah untuk menggunakan sesuatu seperti Perl tergantung pada jenis file apa itu - teks, grafik, biner? Bagaimana cara diformat - CSV, TSV ...

Blackbeagle
sumber
itu diformat pipa delimeted teks, namun 2 baris terakhir adalah masing-masing satu kolom yang akan merusak impor saya jadi saya perlu mereka dihapus
Russ Bradberry
Apakah memperbaiki apa pun yang "diimpor" untuk menangani kasus ini merupakan opsi?
timday
tidak impor itu adalah "memuat data infile" infobright
Russ Bradberry
1

Jika Anda tahu ukuran file ke byte (400000000160 katakan) dan Anda tahu bahwa Anda harus menghapus 160 karakter untuk menghapus dua baris terakhir, maka sesuatu seperti

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

harus melakukan trik. Sudah lama sejak saya menggunakan dd dalam kemarahan; Sepertinya saya ingat semuanya berjalan lebih cepat jika Anda menggunakan ukuran blok yang lebih besar, tetapi apakah Anda bisa melakukannya tergantung pada apakah garis yang ingin Anda turun berada pada kelipatan yang bagus.

dd memiliki beberapa opsi lain untuk menyalin catatan teks ke ukuran tetap yang mungkin berguna sebagai pass awal.

timday
sumber
Saya mencoba ini, tapi itu berjalan dengan kecepatan yang sama seperti sed. Itu telah menulis sekitar 200MB dalam 10 menit, pada tingkat ini benar-benar akan memakan waktu ratusan jam untuk selesai.
Russ Bradberry
1

Jika perintah "truncate" tidak tersedia di sistem Anda (lihat jawaban saya yang lain), lihat "man 2 truncate" untuk panggilan sistem untuk memotong file hingga panjang tertentu.

Tentunya Anda perlu tahu berapa banyak karakter yang Anda butuhkan untuk memotong file (ukuran dikurangi panjang masalah dua baris; jangan lupa untuk menghitung karakter cr / lf).

Dan buat cadangan file sebelum Anda mencoba ini!

timday
sumber
1

Jika Anda lebih suka solusi unix-style, Anda dapat menyimpan dan memotong garis interaktif menggunakan tiga baris kode (Diuji pada Mac dan Linux).

pemotongan garis kecil + aman unix-style (meminta konfirmasi):

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

Solusi ini bergantung pada beberapa unix-tools umum, tetapi masih digunakan perl -e "truncate(file,length)"sebagai pengganti terdekat truncate(1), yang tidak tersedia di semua sistem.

Anda juga dapat menggunakan program shell komprehensif yang dapat digunakan kembali berikut ini, yang menyediakan info penggunaan dan konfirmasi pemotongan fitur, penguraian opsi, dan penanganan kesalahan.

skrip pemotongan garis komprehensif :

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

Ini adalah contoh penggunaan:

$ 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
sumber
0
#! / bin / sh

ed "$ 1" << DI SINI
$
d
d
w
SINI

perubahan dilakukan di tempat. Ini lebih sederhana dan lebih efisien daripada skrip python.

Justin Smith
sumber
Di sistem saya, menggunakan file teks yang terdiri dari sejuta baris dan lebih dari 57MB, edmembutuhkan waktu 100 kali lebih lama untuk dieksekusi daripada skrip Python saya. Saya hanya bisa membayangkan berapa besar perbedaannya untuk file OP yang 7000 kali lebih besar.
Dijeda sampai pemberitahuan lebih lanjut.
0

Memodifikasi jawaban yang diterima untuk memecahkan masalah serupa. Bisa di-tweak sedikit untuk menghapus n baris.

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

Dan tes yang sesuai:

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
sumber
0

Anda dapat menggunakan Vim dalam mode Ex:

ex -sc '-,d|x' file
  1. -, pilih 2 baris terakhir

  2. d menghapus

  3. x Simpan dan tutup

Steven Penny
sumber