ลบสองบรรทัดสุดท้ายของไฟล์ข้อความที่มีขนาดใหญ่มากอย่างมีประสิทธิภาพ


31

ฉันมีไฟล์ที่มีขนาดใหญ่มาก (~ 400 GB) และฉันต้องลบ 2 บรรทัดสุดท้ายออกจากไฟล์ ฉันพยายามใช้sedแต่มันวิ่งไปหลายชั่วโมงก่อนจะยอมแพ้ มีวิธีที่รวดเร็วในการทำเช่นนี้หรือฉันติดอยู่กับsed?


6
คุณสามารถทดลองใช้ GNU ได้ head -n -2 file
user31894

มีคำแนะนำ Perl และ Java สองบรรทัดหนึ่งบรรทัดในstackoverflow.com/questions/2580335/…
mtrw

คำตอบ:


31

ฉันไม่ได้ลองในไฟล์ขนาดใหญ่เพื่อดูว่ามันเร็วแค่ไหน แต่ควรเร็วพอสมควร

หากต้องการใช้สคริปต์เพื่อลบบรรทัดออกจากจุดสิ้นสุดของไฟล์:

./shorten.py 2 large_file.txt

มันค้นหาที่ส่วนท้ายของไฟล์ตรวจสอบเพื่อให้แน่ใจว่าอักขระตัวสุดท้ายเป็นบรรทัดใหม่จากนั้นอ่านอักขระแต่ละตัวทีละครั้งย้อนหลังจนกว่าจะพบบรรทัดใหม่สามบรรทัดและตัดทอนไฟล์หลังจากจุดนั้น การเปลี่ยนแปลงเกิดขึ้น

แก้ไข:ฉันได้เพิ่มเวอร์ชัน Python 2.4 ที่ด้านล่าง

นี่คือเวอร์ชั่นสำหรับ 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)

นี่เป็นเวอร์ชั่น 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)

นี่คือเวอร์ชั่น 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)

ระบบของเราใช้ python 2.4 และฉันไม่แน่ใจว่าบริการใด ๆ ของเราขึ้นอยู่กับมันมันจะทำงานได้ไหม?
Russ Bradberry

@Russ: ฉันได้เพิ่มเวอร์ชันสำหรับ Python 2.4
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

1
น่าอัศจรรย์อย่างแน่นอน! ทำงานเหมือนมีเสน่ห์และใช้เวลาน้อยกว่าหนึ่งวินาที!
Russ Bradberry

12

คุณสามารถลองหัว GNU

head -n -2 file

มันเป็นทางออกที่ดีที่สุดเพราะมันง่าย
xiao

1
นี่จะแสดงให้เขาเห็นสองบรรทัดสุดท้ายของไฟล์ แต่จะไม่ลบออกจากไฟล์ของเขา .. และไม่สามารถใช้กับระบบของฉันได้head: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: ไม่มันจะพิมพ์ทุกบรรทัดตั้งแต่ต้นจนถึง 2 บรรทัดจากปลายตามคู่มือ อย่างไรก็ตามสิ่งนี้จะต้องถูกเปลี่ยนเส้นทางไปยังไฟล์จากนั้นมีปัญหากับไฟล์นี้ว่ามีขนาดใหญ่มากดังนั้นจึงไม่ใช่วิธีที่สมบูรณ์แบบสำหรับปัญหานี้
Daniel Andersson

+1 เหตุใดจึงไม่ได้รับการยอมรับว่าเป็นคำตอบที่ถูกต้อง มันรวดเร็วง่ายและทำงานได้ตามที่คาดไว้
aefxx

6
@ PetrMarek และคนอื่น ๆ : ปัญหาคือมันเกี่ยวข้องกับไฟล์ขนาดใหญ่ วิธีแก้ปัญหานี้ต้องการให้ป้อนไฟล์ทั้งหมดผ่านไปป์และเขียนข้อมูลทั้งหมดไปยังตำแหน่งใหม่และจุดทั้งหมดของคำถามคือหลีกเลี่ยงปัญหานั้น จำเป็นต้องมีโซลูชันในสถานที่เช่นคำตอบที่ได้รับการยอมรับ
Daniel Andersson

7

ฉันเห็น Debian Squeeze / ระบบการทดสอบของฉัน (แต่ไม่ใช่ Lenny / เสถียร) รวมคำสั่ง "truncate" ซึ่งเป็นส่วนหนึ่งของแพ็คเกจ "coreutils"

ด้วยคุณสามารถทำสิ่งที่ชอบ

truncate --size=-160 myfile

เพื่อลบ 160 ไบต์จากจุดสิ้นสุดของไฟล์ (เห็นได้ชัดว่าคุณต้องเข้าใจจำนวนตัวอักษรที่คุณต้องการลบ)


นี่จะเป็นเส้นทางที่เร็วที่สุดเนื่องจากจะทำการแก้ไขไฟล์แบบแทนที่ดังนั้นจึงไม่จำเป็นต้องคัดลอกหรือแยกไฟล์ อย่างไรก็ตามคุณจะต้องตรวจสอบจำนวนไบต์ที่จะลบ ... ฉัน / เดา / ว่าddสคริปต์แบบง่ายจะทำเช่นนั้น (คุณต้องระบุอินพุตออฟเซ็ตเพื่อรับกิโลไบต์ล่าสุดจากนั้นใช้tail -2 | LANG= wc -cหรือ sth เช่นนั้น)
liori

ฉันใช้ CentOS ดังนั้นไม่ฉันไม่ได้ตัดทอน อย่างไรก็ตามนี่คือสิ่งที่ฉันกำลังมองหา
Russ Bradberry

tailมีประสิทธิภาพสำหรับไฟล์ขนาดใหญ่เช่นกัน - สามารถใช้tail | wc -cคำนวณจำนวนไบต์ที่จะตัดแต่ง
krlmlr

6

ปัญหาของ sed คือมันเป็นตัวแก้ไขสตรีม - มันจะประมวลผลไฟล์ทั้งหมดแม้ว่าคุณจะต้องการแก้ไขในตอนท้ายเท่านั้น ไม่ว่าจะเกิดอะไรขึ้นคุณกำลังสร้างไฟล์ 400GB ใหม่ทีละบรรทัด เครื่องมือแก้ไขใด ๆ ที่ทำงานกับไฟล์ทั้งหมดอาจมีปัญหานี้

หากคุณทราบจำนวนบรรทัดคุณสามารถใช้headแต่จะสร้างไฟล์ใหม่แทนการแก้ไขไฟล์ที่มีอยู่ คุณอาจได้รับความเร็วจากความเรียบง่ายของแอ็คชั่นฉันเดา

คุณอาจโชคดีกว่าsplitในการแบ่งไฟล์ออกเป็นชิ้นเล็ก ๆ แก้ไขไฟล์สุดท้ายแล้วใช้catเพื่อรวมไฟล์เหล่านั้นอีกครั้ง แต่ฉันไม่แน่ใจว่ามันจะดีกว่านี้หรือไม่ ฉันจะใช้นับไบต์แทนเส้นมิฉะนั้นมันอาจจะไม่เร็วเลย - คุณจะยังคงสร้างไฟล์ 400GB ใหม่


2

ลองใช้ VIM ... ฉันไม่แน่ใจว่ามันจะใช้กลอุบายหรือไม่เพราะฉันไม่เคยใช้มันในไฟล์ขนาดใหญ่เช่นนี้ แต่ฉันเคยใช้มันในไฟล์ที่มีขนาดเล็กกว่าในอดีตให้ลองดู


ฉันเชื่อว่าเสียงเรียกเข้าจะโหลดเฉพาะสิ่งที่อยู่รอบ ๆ บัฟเฟอร์ทันทีเมื่อทำการแก้ไขแต่ฉันไม่ทราบว่ามันจะประหยัดได้อย่างไร
Phoshi

เป็นกลุ่มแฮงค์ในขณะที่พยายามโหลดไฟล์
รัสแบรดเบอร์รี่

ถ้ามันแฮงค์ก็จะรอ เริ่มต้นการโหลดไปทำงานกลับบ้านดูว่ามันเสร็จแล้ว
leeand00

2
ดูสิ่งนี้: stackoverflow.com/questions/159521/…
leeand00

1

ไฟล์ประเภทใดและในรูปแบบใด อาจจะง่ายกว่าที่จะใช้สิ่งที่ต้องการ Perl ขึ้นอยู่กับชนิดของไฟล์ - ข้อความกราฟิกไบนารี? มีการจัดรูปแบบอย่างไร - CSV, TSV ...


มันถูกจัดรูปแบบข้อความที่ถูกลบไปป์อย่างไรก็ตาม 2 บรรทัดสุดท้ายเป็นหนึ่งคอลัมน์ซึ่งจะทำให้การนำเข้าของฉันแตกหักดังนั้นฉันจึงต้องการให้นำออก
Russ Bradberry

จะแก้ไขสิ่งที่ "นำเข้า" เพื่อจัดการกับกรณีนี้เป็นตัวเลือกหรือไม่
timday

ไม่มีการนำเข้าคือ "โหลดข้อมูล infile" ของ infobright
Russ Bradberry

1

หากคุณรู้ขนาดของไฟล์เป็นไบต์ (400000000160 พูด) และคุณรู้ว่าคุณจำเป็นต้องลบอักขระ 160 ตัวเพื่อตัดสองบรรทัดสุดท้ายดังนั้น

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

ควรทำเคล็ดลับ เป็นเวลานานแล้วที่ฉันใช้ความโกรธในทางที่ผิด; ฉันดูเหมือนจะจำสิ่งต่าง ๆ ได้เร็วขึ้นหากคุณใช้ขนาดบล็อกที่ใหญ่กว่า แต่ไม่ว่าคุณจะทำเช่นนั้นได้หรือไม่ขึ้นอยู่กับว่าเส้นที่คุณต้องการจะลดลงนั้นมีความหลากหลาย

dd มีตัวเลือกอื่น ๆ ในการแพ็ดบันทึกข้อความเป็นขนาดคงที่ซึ่งอาจเป็นประโยชน์ในการผ่านเบื้องต้น


ฉันลองสิ่งนี้ แต่มันก็มีความเร็วเท่ากันกับความเร็ว มันเขียนประมาณ 200MB ใน 10 นาทีในอัตรานี้มันจะใช้เวลาหลายร้อยชั่วโมง
Russ Bradberry

1

หากคำสั่ง "truncate" ไม่มีอยู่ในระบบของคุณ (ดูคำตอบอื่น ๆ ของฉัน) ให้ดูที่ "man 2 truncate" สำหรับการเรียกระบบเพื่อตัดไฟล์ให้มีความยาวตามที่กำหนด

เห็นได้ชัดว่าคุณต้องรู้จำนวนตัวอักษรที่คุณต้องการตัดทอนไฟล์เป็น (ขนาดลบความยาวของปัญหาสองบรรทัดอย่าลืมนับตัวอักษร cr / lf)

และทำการสำรองไฟล์ก่อนที่จะลอง!


1

หากคุณต้องการโซลูชันแบบ unix คุณสามารถมีการตัดและบันทึกบรรทัดแบบโต้ตอบโดยใช้โค้ดสามบรรทัด (ทดสอบบน Mac และ Linux)

การตัดบรรทัดที่มีขนาดเล็กและปลอดภัยสไตล์ยูนิกซ์ (ขอให้ยืนยัน):

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

โซลูชันนี้ใช้เครื่องมือยูนิกซ์ทั่วไปเพียงบางส่วน แต่ยังคงใช้perl -e "truncate(file,length)"แทนแบบใกล้เคียงที่สุดtruncate(1)ซึ่งไม่สามารถใช้ได้กับทุกระบบ

นอกจากนี้คุณยังสามารถใช้โปรแกรมเชลล์ที่ใช้ซ้ำได้ต่อไปนี้ซึ่งให้ข้อมูลการใช้งานและการยืนยันการตัดทอนคุณสมบัติการแยกวิเคราะห์ตัวเลือกและการจัดการข้อผิดพลาด

สคริปต์การตัดบรรทัดที่ครอบคลุม :

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

นี่คือตัวอย่างการใช้งาน:

$ 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 / ดวลจุดโทษ

ed "$ 1" << ที่นี่
$
d
d
W
ที่นี่

มีการเปลี่ยนแปลงเกิดขึ้น นี่คือเรียบง่ายและมีประสิทธิภาพมากกว่าสคริปต์หลาม


ในระบบของฉันการใช้ไฟล์ข้อความที่ประกอบด้วยหนึ่งล้านบรรทัดและมากกว่า 57MB นั้นedใช้เวลานานกว่าการรันสคริปต์ Python ของฉันถึง 100 เท่า ฉันสามารถจินตนาการได้ว่าความแตกต่างของไฟล์ OP จะมากขึ้นเท่าไหร่ซึ่งใหญ่กว่า 7000 เท่า
หยุดชั่วคราวจนกว่าจะมีการแจ้งให้ทราบต่อไป

0

แก้ไขคำตอบที่ยอมรับเพื่อแก้ไขปัญหาที่คล้ายกัน อาจปรับแต่งเล็กน้อยเพื่อลบบรรทัดที่ n

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

และการทดสอบที่เกี่ยวข้อง:

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.