web-dev-qa-db-ja.com

ファイルをコピーせずにcsvから単一の行を削除する

このトピックのいくつかの形式を扱う複数のSOの質問がありますが、それらはすべてcsvファイルから1行のみを削除するのに非常に非効率的です(通常、ファイル全体をコピーする必要があります)。次のような形式のcsv:

fname,lname,age,sex
John,Doe,28,m
Sarah,Smith,27,f
Xavier,Moore,19,m

サラの列を削除する最も効率的な方法は何ですか?可能であれば、ファイル全体をコピーしないようにしたいと思います。

20
SamBG

ここに根本的な問題があります。 (私が知っている)現在のファイルシステムには、ファイルの中央から大量のバイトを削除する機能はありません。既存のバイトを上書きするか、新しいファイルを書き込むことができます。したがって、オプションは次のとおりです。

  • 問題のある行のないファイルのコピーを作成し、古いものを削除して、新しいファイルの名前を変更します。 (これは回避したいオプションです)。
  • 行のバイトを無視されるもので上書きします。 exactlyファイルの読み取り内容に応じて、コメント文字が機能するか、スペースが機能する場合があります(または\0)。ただし、完全に汎用的にしたい場合、コメント文字が定義されていないため、これはCSVファイルのオプションではありません。
  • 最後の必死の手段として、次のことができます。
    • 削除したい行まで読む
    • 残りのファイルをメモリに読み込む
    • 行と後続のすべての行を保持するデータで上書きします。
    • ファイルを最終位置として切り捨てます(通常、ファイルシステムはこれを許可します)。

最初の行を削除しようとする場合、最後のオプションは明らかにあまり役に立ちません(ただし、最後の行を削除する場合は便利です)。また、プロセスの途中でクラッシュするという恐ろしい脆弱性もあります。

27
Martin Bonner

これは一つの方法です。残りのファイルをバッファにロードする必要がありますが、Pythonで考えることができる最高の方法です。

with open('afile','r+') as fd:
    delLine = 4
    for i in range(delLine):
        pos = fd.tell()
        fd.readline()
    rest = fd.read()
    fd.seek(pos)
    fd.truncate()
    fd.write(rest)
    fd.close()

行番号を知っているかのようにこれを解決しました。テキストを確認したい場合は、上記のループの代わりに:

pos = fd.tell()
while fd.readline().startswith('Sarah'): pos = fd.tell()

「サラ」が見つからない場合は例外があります。

削除している行が最後に近い場合、これはより効率的かもしれませんが、すべてを読んで、行を削除し、それを戻すことはユーザー時間と比較して大幅に節約できます(これはTkアプリだと考えてください)。また、これは一度開いてファイルに一度フラッシュするだけでよいので、ファイルが極端に長く、Sarahが本当にずっと下にない限り、おそらく目立たないでしょう。

3
kabanus

Sedを使用:

sed -ie "/Sahra/d" your_file

編集、申し訳ありませんが、Pythonを使用する必要性に関するすべてのタグとコメントを完全に読んでいません。いずれにせよ、他の回答で提案された余分なコードをすべて回避するために、シェルユーティリティを使用した前処理で解決しようとするでしょう。しかし、私はあなたの問題を完全に知らないので、それは不可能かもしれません?

幸運を!

3
UlfR

ファイルをその場で編集することは、イテレータを繰り返し処理している間に変更するのと同じように)やりがいのある作業であり、通常は面倒の価値はありません。ほとんどの場合、一時ファイル(または作業スペース、またはストレージスペースまたはRAMに依存)に書き込んでからソースファイルを削除し、ソースファイルを一時ファイルに置き換えることは、同様に実行しようとします。同じことをその場で。

しかし、あなたが主張するなら、ここに一般化された解決策があります:

import os

def remove_line(path, comp):
    with open(path, "r+b") as f:  # open the file in rw mode
        mod_lines = 0  # hold the overwrite offset
        while True:
            last_pos = f.tell()  # keep the last line position
            line = f.readline()  # read the next line
            if not line:  # EOF
                break
            if mod_lines:  # we've already encountered what we search for
                f.seek(last_pos - mod_lines)  # move back to the beginning of the gap
                f.write(line)  # fill the gap with the current line
                f.seek(mod_lines, os.SEEK_CUR)  # move forward til the next line start
            Elif comp(line):  # search for our data
                mod_lines = len(line)  # store the offset when found to create a gap
        f.seek(last_pos - mod_lines)  # seek back the extra removed characters
        f.truncate()  # truncate the rest

これにより、提供された比較関数に一致する行のみが削除され、ファイルの残りの部分が繰り返されて、「削除された」行にデータがシフトされます。残りのファイルを作業メモリーにロードする必要もありません。テストするには、test.csv含む:

fname、lname、age、sex 
 John、Doe、28、m 
 Sarah、Smith、27、f 
 Xavier、Moore、19、m

次のように実行できます。

remove_line("test.csv", lambda x: x.startswith(b"Sarah"))

そして、あなたはtest.csvインプレースを削除したSarah行:

fname、lname、age、sex 
 John、Doe、28、m 
 Xavier、Moore、19、m

ファイルがバイナリモードで開かれるときにbytes比較関数を渡して、切り捨て/上書き中に一貫した改行を維持することに注意してください。

[〜#〜] update [〜#〜]:ここで紹介するさまざまなテクニックの実際のパフォーマンスに興味がありましたが、時間がありませんでした昨日それらをテストするために、少し遅れて、それを明らかにするベンチマークを作成しました。結果のみに関心がある場合は、一番下までスクロールします。最初に、ベンチマークの内容とテストの設定方法について説明します。また、システムで同じベンチマークを実行できるように、すべてのスクリプトも提供します。

何に関しては、私はこれと他の答え、すなわち一時ファイル(temp_file_*関数)およびインプレース編集の使用(in_place_*) 関数。これらの両方をストリーミングで設定しています(1行ずつ読む、*_stream関数)およびメモリ(作業メモリ内の残りのファイルの読み取り、*_wm関数)モード。また、mmapモジュール(in_place_mmap 関数)。すべての機能と、CLIを介して制御される小さなロジックを含むベンチマークスクリプトは次のとおりです。

#!/usr/bin/env python

import mmap
import os
import shutil
import sys
import time

def get_temporary_path(path):  # use tempfile facilities in production
    folder, filename = os.path.split(path)
    return os.path.join(folder, "~$" + filename)

def temp_file_wm(path, comp):
    path_out = get_temporary_path(path)
    with open(path, "rb") as f_in, open(path_out, "wb") as f_out:
        while True:
            line = f_in.readline()
            if not line:
                break
            if comp(line):
                f_out.write(f_in.read())
                break
            else:
                f_out.write(line)
        f_out.flush()
        os.fsync(f_out.fileno())
    shutil.move(path_out, path)

def temp_file_stream(path, comp):
    path_out = get_temporary_path(path)
    not_found = True  # a flag to stop comparison after the first match, for fairness
    with open(path, "rb") as f_in, open(path_out, "wb") as f_out:
        while True:
            line = f_in.readline()
            if not line:
                break
            if not_found and comp(line):
                continue
            f_out.write(line)
        f_out.flush()
        os.fsync(f_out.fileno())
    shutil.move(path_out, path)

def in_place_wm(path, comp):
    with open(path, "r+b") as f:
        while True:
            last_pos = f.tell()
            line = f.readline()
            if not line:
                break
            if comp(line):
                rest = f.read()
                f.seek(last_pos)
                f.write(rest)
                break
        f.truncate()
        f.flush()
        os.fsync(f.fileno())

def in_place_stream(path, comp):
    with open(path, "r+b") as f:
        mod_lines = 0
        while True:
            last_pos = f.tell()
            line = f.readline()
            if not line:
                break
            if mod_lines:
                f.seek(last_pos - mod_lines)
                f.write(line)
                f.seek(mod_lines, os.SEEK_CUR)
            Elif comp(line):
                mod_lines = len(line)
        f.seek(last_pos - mod_lines)
        f.truncate()
        f.flush()
        os.fsync(f.fileno())

def in_place_mmap(path, comp):
    with open(path, "r+b") as f:
        stream = mmap.mmap(f.fileno(), 0)
        total_size = len(stream)
        while True:
            last_pos = stream.tell()
            line = stream.readline()
            if not line:
                break
            if comp(line):
                current_pos = stream.tell()
                stream.move(last_pos, current_pos, total_size - current_pos)
                total_size -= len(line)
                break
        stream.flush()
        stream.close()
        f.truncate(total_size)
        f.flush()
        os.fsync(f.fileno())

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: {} target_file.ext <search_string> [function_name]".format(__file__))
        exit(1)
    target_file = sys.argv[1]
    search_func = globals().get(sys.argv[3] if len(sys.argv) > 3 else None, in_place_wm)
    start_time = time.time()
    search_func(target_file, lambda x: x.startswith(sys.argv[2].encode("utf-8")))
    # some info for the test runner...
    print("python_version: " + sys.version.split()[0])
    print("python_time: {:.2f}".format(time.time() - start_time))

次のステップでは、これらの機能を可能な限り隔離された環境で実行するテスターを構築し、各機能の公平なベンチマークを取得します。私のテストは次のように構成されています:

  • 3つのサンプルデータCSVが乱数の1Mx10マトリックス(〜200MBファイル)として生成され、識別可能な行がそれらの先頭、中央、および末尾にそれぞれ配置されるため、3つの極端なシナリオのテストケースが生成されます。
  • マスタサンプルデータファイルは、各テストの前に一時ファイルとしてコピーされます(行の削除は破壊的であるため)。
  • 各テストを開始する前に、ファイルの同期とキャッシュのクリアのさまざまな方法を使用して、バッファをクリーンにします。
  • テストは最高の優先度(chrt -f 99) 使って /usr/bin/time Python以来、このようなシナリオでパフォーマンスを正確に測定することは実際には信頼できません。
  • 予測不可能な変動を滑らかにするために、各テストを少なくとも3回実行します。
  • テストはPython 2.7およびPython 3.6(CPython)でも実行され、バージョン間でパフォーマンスの一貫性があるかどうかが確認されます。
  • すべてのベンチマークデータが収集され、将来の分析のためにCSVとして保存されます。

残念ながら、テストを完全に分離して実行できるシステムが手元になかったため、ハイパーバイザーでテストを実行して数値を取得しました。つまり、I/Oパフォーマンスはおそらく非常にゆがんでいますが、同等のデータを提供するすべてのテストに同様に影響するはずです。どちらの方法でも、このテストを独自のシステムで実行して、関連する結果を得ることができます。

前述のシナリオを実行するテストスクリプトを次のように設定しました。

#!/usr/bin/env python

import collections
import os
import random
import shutil
import subprocess
import sys
import time

try:
    range = xrange  # cover Python 2.x
except NameError:
    pass

try:
    DEV_NULL = subprocess.DEVNULL
except AttributeError:
    DEV_NULL = open(os.devnull, "wb")  # cover Python 2.x

SAMPLE_ROWS = 10**6  # 1M lines
TEST_LOOPS = 3
CALL_SCRIPT = os.path.join(os.getcwd(), "remove_line.py")  # the above script

def get_temporary_path(path):
    folder, filename = os.path.split(path)
    return os.path.join(folder, "~$" + filename)

def generate_samples(path, data="LINE", rows=10**6, columns=10):  # 1Mx10 default matrix
    sample_beginning = os.path.join(path, "sample_beg.csv")
    sample_middle = os.path.join(path, "sample_mid.csv")
    sample_end = os.path.join(path, "sample_end.csv")
    separator = os.linesep
    middle_row = rows // 2
    with open(sample_beginning, "w") as f_b, \
            open(sample_middle, "w") as f_m, \
            open(sample_end, "w") as f_e:
        f_b.write(data)
        f_b.write(separator)
        for i in range(rows):
            if not i % middle_row:
                f_m.write(data)
                f_m.write(separator)
            for t in (f_b, f_m, f_e):
                t.write(",".join((str(random.random()) for _ in range(columns))))
                t.write(separator)
        f_e.write(data)
        f_e.write(separator)
    return ("beginning", sample_beginning), ("middle", sample_middle), ("end", sample_end)

def normalize_field(field):
    field = field.lower()
    while True:
        s_index = field.find('(')
        e_index = field.find(')')
        if s_index == -1 or e_index == -1:
            break
        field = field[:s_index] + field[e_index + 1:]
    return "_".join(field.split())

def encode_csv_field(field):
    if isinstance(field, (int, float)):
        field = str(field)
    escape = False
    if '"' in field:
        escape = True
        field = field.replace('"', '""')
    Elif "," in field or "\n" in field:
        escape = True
    if escape:
        return ('"' + field + '"').encode("utf-8")
    return field.encode("utf-8")

if __name__ == "__main__":
    print("Generating sample data...")
    start_time = time.time()
    samples = generate_samples(os.getcwd(), "REMOVE THIS LINE", SAMPLE_ROWS)
    print("Done, generation took: {:2} seconds.".format(time.time() - start_time))
    print("Beginning tests...")
    search_string = "REMOVE"
    header = None
    results = []
    for f in ("temp_file_stream", "temp_file_wm",
              "in_place_stream", "in_place_wm", "in_place_mmap"):
        for s, path in samples:
            for test in range(TEST_LOOPS):
                result = collections.OrderedDict((("function", f), ("sample", s),
                                                  ("test", test)))
                print("Running {function} test, {sample} #{test}...".format(**result))
                temp_sample = get_temporary_path(path)
                shutil.copy(path, temp_sample)
                print("  Clearing caches...")
                subprocess.call(["Sudo", "/usr/bin/sync"], stdout=DEV_NULL)
                with open("/proc/sys/vm/drop_caches", "w") as dc:
                    dc.write("3\n")  # free pagecache, inodes, dentries...
                # you can add more cache clearing/invalidating calls here...
                print("  Removing a line starting with `{}`...".format(search_string))
                out = subprocess.check_output(["Sudo", "chrt", "-f", "99",
                                               "/usr/bin/time", "--verbose",
                                               sys.executable, CALL_SCRIPT, temp_sample,
                                               search_string, f], stderr=subprocess.STDOUT)
                print("  Cleaning up...")
                os.remove(temp_sample)
                for line in out.decode("utf-8").split("\n"):
                    pair = line.strip().rsplit(": ", 1)
                    if len(pair) >= 2:
                        result[normalize_field(pair[0].strip())] = pair[1].strip()
                results.append(result)
                if not header:  # store the header for later reference
                    header = result.keys()
    print("Cleaning up sample data...")
    for s, path in samples:
        os.remove(path)
    output_file = sys.argv[1] if len(sys.argv) > 1 else "results.csv"
    output_results = os.path.join(os.getcwd(), output_file)
    print("All tests completed, writing results to: " + output_results)
    with open(output_results, "wb") as f:
        f.write(b",".join(encode_csv_field(k) for k in header) + b"\n")
        for result in results:
            f.write(b",".join(encode_csv_field(v) for v in result.values()) + b"\n")
    print("All done.")

最後に(およびTL; DR):ここに私の結果があります-結果セットから最適な時間とメモリデータのみを抽出していますが、取得できます完全な結果セット: Python 2.7 Raw Test Data および Python 3.6 Raw Test Data .

Python File Line Removal - Selected Results


私が収集したデータに基づいて、いくつかの最後のメモ:

  • 作業メモリが問題である場合(非常に大きなファイルの処理など)、*_stream関数はフットプリントを小さくします。 On Python 3.x途中でmmapテクニックになります。
  • ストレージに問題がある場合は、in_place_*関数は実行可能です。
  • 両方とも不足している場合、唯一の一貫した手法はin_place_streamただし、処理時間とI/O呼び出しの増加を犠牲にして(*_wm 関数)。
  • in_place_*関数は、途中で停止するとデータが破損する可能性があるため、危険です。 temp_file_*関数(整合性チェックなし)は、非トランザクションファイルシステムでのみ危険です。
3
zwer

パンダを使用してそれを行うことができます。データがdata.csvの下に保存されている場合、以下が役立ちます。

import pandas as pd

df = pd.read_csv('data.csv')
df = df[df.fname != 'Sarah' ]
df.to_csv('data.csv', index=False)
2
shep4rd

サラの列を削除する最も効率的な方法は何ですか?可能であれば、ファイル全体をコピーしないようにしたいと思います。

最も効率的な方法は、csvパーサーが無視するものでその行を上書きすることです。これにより、削除された行の後に行を移動する必要がなくなります。

Csvパーサーが空の行を無視できる場合、\n記号でその行を上書きします。そうでない場合、パーサーが値から空白を削除すると、その行は(スペース)記号で上書きされます。

1

これは役立つかもしれません:

with open("sample.csv",'r') as f:
    for line in f:
        if line.startswith('sarah'):continue
        print(line)
0
Rachit kapadia