web-dev-qa-db-ja.com

非常に大きなテキストファイルの最後の2行を効率的に削除する

非常に大きなファイル(約400 GB)があり、最後の2行を削除する必要があります。 sedを使用しようとしましたが、あきらめるまでに数時間かかりました。これを行う簡単な方法はありますか、それともsedで行き詰まっていますか?

31
Russ Bradberry

大きなファイルでこれを試してみて速度を確認していませんが、かなり高速です。

スクリプトを使用してファイルの末尾から行を削除するには:

./shorten.py 2 large_file.txt

ファイルの最後までシークし、最後の文字が改行であることを確認してから、3つの改行が見つかるまで1文字ずつ逆方向に読み取り、その直後のファイルを切り捨てます。変更が行われます。

編集: 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)

GNU headを試すことができます

head -n -2 file
12
user31894

私のDebian Squeeze/testingシステム(Lenny/stableは除く)には、「coreutils」パッケージの一部として「truncate」コマンドが含まれています。

それであなたは単に次のようなことをすることができます

truncate --size=-160 myfile

ファイルの最後から160バイトを削除します(明らかに、削除する必要がある文字数を正確に把握する必要があります)。

7
timday

Sedの問題は、sedがストリームエディターであることです。最後の方でのみ変更を加えたい場合でも、ファイル全体を処理します。したがって、何があっても、新しい400GBファイルを1行ずつ作成します。ファイル全体を操作するエディタには、おそらくこの問題があります。

行数がわかっている場合は、headを使用できますが、これでも、既存のファイルを変更する代わりに、新しいファイルが作成されます。アクションのシンプルさからスピードが向上するかもしれません。

あなた可能性がありますsplitを使用してファイルをより小さな部分に分割し、最後のファイルを編集してから、catを使用してそれらを再度結合することで幸運を祈りますが、私はそれがもっと良くなるかどうかわかりません。行数ではなくバイトカウントを使用します。そうしないと、おそらくまったく速くなりません。新しい400GBファイルを作成することになります。

6
Zac Thompson

VIMを試してみてください...そのような大きなファイルで使用したことがないので、うまくいくかどうかはわかりませんが、過去に小さなファイルで使用したことがあるので、試してみてください。

2
leeand00

どのような種類のファイルで、どの形式ですか?それがどんな種類のファイルであるかに依存するPerlのようなものを使う方が簡単かもしれません-テキスト、グラフィックス、バイナリ?フォーマット方法-CSV、TSV ...

1
Blackbeagle

UNIXスタイルのソリューションが必要な場合は、3行のコード(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-toolsに依存していますが、すべてのシステムで使用できるわけではない 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
_
1
Juve

バイトまでのファイルのサイズ(400000000160など)がわかっていて、最後の2行を取り除くために正確に160文字を削除する必要があることがわかっている場合は、次のようになります。

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

トリックを行う必要があります。私が怒りの中でddを使って以来、それは長い年月です。大きいブロックサイズを使用すると処理が速くなることを覚えているようですが、それを実行できるかどうかは、ドロップする行がニースの倍数かどうかによって異なります。

ddには、テキストレコードを固定サイズにパディングする他のオプションがいくつかあります。これは、予備パスとして役立つ場合があります。

1
timday

「truncate」コマンドがシステムで使用できない場合(他の回答を参照)、「man 2 truncate」でシステムコールを確認し、ファイルを指定された長さに切り詰めます。

明らかに、ファイルを切り捨てるのに必要な文字数を知る必要があります(サイズから問題の2行の長さを引いたものです。cr/ lf文字を数えることを忘れないでください)。

これを試す前に、ファイルのバックアップを作成してください。

1
timday

受け入れられた回答を変更して、同様の問題を解決しました。 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
tponthieux

VimはExモードで使用できます。

ex -sc '-,d|x' file
  1. -,最後の2行を選択

  2. d削除

  3. x保存して閉じる

0
Steven Penny
#!/ bin/sh 
 
 ed "$ 1" <<ここ
 $ 
 d 
 d 
 w 
ここに

変更が行われます。これは、pythonスクリプトよりも簡単で効率的です。

0
Justin Smith