Ефективно видаліть останні два рядки надзвичайно великого текстового файлу


31

У мене дуже великий файл (~ 400 ГБ), і мені потрібно видалити з нього останні 2 рядки. Я намагався використовувати sed, але це пробігло години, перш ніж я здався. Чи є швидкий спосіб зробити це, або я застряг sed?


6
ви можете спробувати керівника GNU. head -n -2 file
користувач31894

Були пару одна лінія 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

Це найкраще рішення, оскільки воно просте.
Сяо

1
Це покаже йому останні два рядки файлу, але не видалить їх зі свого файлу. Він навіть не працює в моїй системіhead: illegal line count -- -2
SooDesuNe

2
@SooDesuNe: Ні, він буде друкувати всі рядки від початку до 2 рядків з кінця, відповідно до інструкції. Однак це потрібно буде переспрямувати на файл, і тоді виникає проблема, коли цей файл є гігантським, тому це не ідеальне рішення для цієї проблеми.
Даніель Андерссон

+1 Чому це не сприймається як правильна відповідь? Це швидко, просто і працює так, як очікувалося.
aefxx

6
@PetrMarek та інші. Проблема полягала в тому, що стосувалася гігантського файлу. Це рішення вимагає подачі всього файлу через трубу та переписування всіх даних на нове місце - і вся суть питання полягає в тому, щоб уникнути цього. Потрібне рішення на місці, наприклад, у прийнятій відповіді.
Даніель Андерссон

7

Я бачу, що мої системи Debian Squeeze / тестування (але не Lenny / stable) включають команду "усікати" як частину пакету "coreutils".

З ним можна просто зробити щось на кшталт

truncate --size=-160 myfile

щоб видалити 160 байт з кінця файлу (очевидно, потрібно точно розібратися, скільки символів потрібно видалити).


Це буде найшвидший шлях, оскільки він змінює файл на місці, а тому не вимагає ні копіювання, ні розбору файлу. Однак вам все одно доведеться перевірити, скільки байтів буде видалено ... Я / здогадуюсь /, що простий ddскрипт зробить це (вам потрібно вказати зміщення входу, щоб отримати останній кілобайт, а потім використовувати tail -2 | LANG= wc -c, або що-небудь подібне).
liori

Я використовую CentOS, тому ні в мене немає усікання. Однак саме це я шукаю.
Russ Bradberry

tailефективний і для великих файлів - може використовувати tail | wc -cдля обчислення кількості байтів, які слід обрізати.
krlmlr

6

Проблема sed полягає в тому, що це редактор потоків - він обробить весь файл, навіть якщо ви хочете лише внести зміни до кінця. Тож незалежно від того, ви створюєте новий 400 Гб файл, рядок за рядком. Будь-який редактор, який працює над усім файлом, мабуть, матиме цю проблему.

Якщо ви знаєте кількість рядків, ви можете використовувати head, але знову ж таки це створює новий файл замість того, щоб змінювати існуючий на місці. Можливо, ви можете отримати швидкість завдяки простоті дії, я думаю.

Ви , можливо , краще удачі , використовуючи splitрозбити файл на більш дрібні шматки, редагування останнього, а потім з допомогою catз'єднати їх знову, але я не впевнений , якщо це буде краще. Я б використовував кількість байтів, а не рядки, інакше це, швидше за все, не буде швидше - ви все одно будете створювати новий 400 ГБ файл.


2

Спробуйте VIM ... Я не впевнений, чи вдасться це зробити, чи ні, так як я ніколи не використовував його на такому великому файлі, але я раніше його використовував на менших великих файлах, дайте спробувати.


Я вірю, що при редагуванні vim завантажує лише те, що відбувається безпосередньо навколо буфера , проте я не знаю, як це економить.
Phoshi

vim висить, поки він намагається завантажити файл
Russ Bradberry

Добре, якщо воно повисне, ах, почекайте. Почніть його завантажувати, йдіть на роботу, приходьте додому, подивіться, чи це зроблено.
leeand00


1

Який файл і в якому форматі? Можливо, буде простіше використовувати щось на кшталт Perl, залежне від типу файлу - текст, графіка, двійковий файл? Як це відформатовано - CSV, TSV ...


це відформатований текст з накресленим текстом, проте останні два рядки - це один стовпець, кожен з яких порушить імпорт, тому мені потрібно їх видалити
Russ Bradberry

чи виправляє варіант "імпорт" для вирішення цього випадку варіант?
тайм

немає імпорту - "завантаження даних завантаження даних" Інфорайта
Russ Bradberry

1

Якщо ви знаєте розмір файлу до байта (400000000160 кажуть) і знаєте, що вам потрібно видалити рівно 160 символів, щоб зняти останні два рядки, то щось подібне

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

повинен зробити трюк. Минуло віки, коли я в гніві використовував дд; Здається, я пам’ятаю, що справи йдуть швидше, якщо ви використовуєте більший розмір блоку, але від того, чи зможете ви це зробити, залежить від того, чи будуть лінії, які ви хочете відпустити, у кращому кратному.

У dd є деякі інші варіанти, щоб викласти текстові записи до фіксованого розміру, які можуть бути корисними як попередній пропуск.


Я спробував це, але він йшов приблизно з тією ж швидкістю, що і sed. Він написав близько 200 Мб за 10 хвилин, з такою швидкістю буквально знадобиться сотні годин.
Russ Bradberry

1

Якщо команда "усікати" недоступна у вашій системі (див. Мою іншу відповідь), перегляньте "усікання чоловіка 2" для системного виклику для врізання файлу заданої довжини.

Очевидно, ви повинні знати, скільки символів потрібно для врізання файлу (розмір мінус довжина проблеми два рядки; не забудьте порахувати жодні символи cr / lf).

І зробіть резервну копію файлу, перш ніж спробувати це!


1

Якщо ви віддаєте перевагу рішенням у стилі unix, ви можете мати збереження та інтерактивне обрізання ліній, використовуючи три рядки коду (випробувано на Mac та Linux).

невеликий + безпечний відсікання ліній у стилі Unix (запитує підтвердження):

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

Це рішення покладається на кілька поширених інструментів unix, але все ще використовується 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
#! / бін / ш

ed "$ 1" << ТУТ
$
г
г
ш
ТУТ

зміни вносяться на місце. Це простіше і ефективніше, ніж сценарій python.


У моїй системі, використовуючи текстовий файл, що складається з мільйона рядків і понад 57 Мб, edпотрібно було виконати в 100 разів довше, ніж мій сценарій Python. Я можу лише уявити, наскільки більша різниця була б у файлі ОП, який у 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()

0

Ви можете використовувати Vim в режимі Ex:

ex -sc '-,d|x' file
  1. -, виберіть останні 2 рядки

  2. d видалити

  3. x зберегти і закрити

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.