Эффективно удалить последние две строки очень большого текстового файла
У меня очень большой файл (~400 ГБ), и мне нужно удалить из него последние 2 строки. Я пытался использовать sed
, но это длилось несколько часов, прежде чем я сдался. Есть ли быстрый способ сделать это, или я застрял с sed
?
12 ответов
Я не пробовал это на большом файле, чтобы увидеть, насколько это быстро, но это должно быть довольно быстро.
Чтобы использовать сценарий для удаления строк из конца файла:
./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)
Я вижу, что мои системы тестирования / сжатия Debian (но не Lenny/stable) включают команду "truncate" как часть пакета "coreutils".
С его помощью вы можете просто сделать что-то вроде
truncate --size=-160 myfile
удалить 160 байтов из конца файла (очевидно, вам нужно точно определить, сколько символов вам нужно удалить).
Проблема с sed в том, что это потоковый редактор - он будет обрабатывать весь файл, даже если вы хотите вносить изменения ближе к концу. Поэтому, несмотря ни на что, вы создаете новый файл размером 400 ГБ, строка за строкой. Любой редактор, который работает с целым файлом, вероятно, будет иметь эту проблему.
Если вы знаете количество строк, вы можете использовать head
, но опять же это создает новый файл вместо того, чтобы изменить существующий на месте. Я полагаю, вы можете получить выигрыш в скорости благодаря простоте действия.
Возможно, вам повезет больше, используя split
разбить файл на более мелкие части, отредактировать последний и затем использовать cat
объединить их снова, но я не уверен, что будет лучше. Я бы использовал количество байтов, а не строк, иначе это, скорее всего, будет совсем не быстрее - вы все равно будете создавать новый файл объемом 400 ГБ.
Попробуйте VIM... Я не уверен, что это сработает или нет, так как я никогда не использовал его на таких больших файлах, но в прошлом я использовал его на более крупных файлах.
Что за файл и в каком формате? Может быть проще использовать что-то вроде Perl, в зависимости от того, какой это файл - текстовый, графический, двоичный? Как это отформатировано - CSV, TSV...
Если команда "truncate" не доступна в вашей системе (см. Мой другой ответ), посмотрите на "man 2 truncate" для системного вызова, чтобы обрезать файл до указанной длины.
Очевидно, вам нужно знать, до скольких символов вам нужно обрезать файл (размер минус длина проблемы, две строки; не забудьте подсчитать любые символы cr/lf).
И сделайте резервную копию файла, прежде чем попробовать это!
Если вы предпочитаете решения в стиле Unix, вы можете сохранить и интерактивное усечение строк, используя три строки кода (протестировано на Mac и Linux).
small + safe усечение строки в стиле 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
Если вы знаете размер файла в байтах (скажем, 400000000160) и знаете, что вам нужно удалить ровно 160 символов, чтобы убрать последние две строки, тогда что-то вроде
dd if=originalfile of=truncatedfile ibs=1 count=400000000000
должен сделать свое дело. Прошло много лет с тех пор, как я использовал dd в гневе; Кажется, я помню, что дела идут быстрее, если вы используете больший размер блока, но то, сможете ли вы это сделать, зависит от того, достаточно ли кратны строки, которые вы хотите отбросить.
У dd есть некоторые другие опции для добавления текстовых записей к фиксированному размеру, что может быть полезно в качестве предварительного прохода.
#!/ Bin/ ш ed "$1" << ЗДЕСЬ $ d d вес ВОТ
изменения сделаны на месте. Это проще и эффективнее, чем скрипт python.
Изменен принятый ответ для решения аналогичной проблемы. Можно немного подправить, чтобы убрать 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()
Вы можете использовать Vim в режиме Ex:
ex -sc '-,d|x' file
-,
выберите последние 2 строкиd
удалятьx
сохрани и закрой