Удалите одну строку из csv без копирования файлов

Есть несколько вопросов SO, посвященных той или иной форме этой темы, но все они кажутся ужасно неэффективными для удаления только одной строки из файла csv (обычно это связано с копированием всего файла). Если у меня есть csv, отформатированный так:

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

Каков наиболее эффективный способ удаления строки Сара? Если возможно, я бы хотел избежать копирования всего файла.

Ответы

Ответ 1

У вас здесь фундаментальная проблема. Никакая текущая файловая система (я не знаю) предоставляет возможность удалить кучу байтов из середины файла. Вы можете перезаписать существующие байты или записать новый файл. Итак, ваши варианты:

  • Создайте копию файла без оскорбительной строки, удалите старый и переименуйте новый файл на место. (Это вариант, которого вы хотите избежать).
  • Перезапишите байты строки тем, что будет проигнорировано. В зависимости от того, что будет читать файл, может работать символ комментария, или пробелы могут работать (или, возможно, даже \0). Если вы хотите быть полностью общим, это не вариант с CSV файлами, потому что не существует определенного символа комментария.
  • В качестве последней отчаянной меры вы могли:
    • прочитайте до строки, которую хотите удалить.
    • прочитайте остальную часть файла в памяти
    • и перезапишите строку и все последующие строки данными, которые вы хотите сохранить.
    • усечь файл как конечную позицию (обычно это позволяют файловые системы).

Последний вариант, очевидно, мало помогает, если вы пытаетесь удалить первую строку (но это удобно, если вы хотите удалить строку ближе к концу). Он также ужасно уязвим для разбивки в середине процесса.

Ответ 2

Это один из способов. Вам нужно загрузить остальную часть файла в буфер, но это лучшее, что я могу придумать в 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). Это также нужно только открывать и стирать один раз в файл один раз, поэтому, если файлы не очень длинны, а Сара действительно далеко, это, вероятно, не будет заметным.

Ответ 3

Редактирование файлов на месте - это задача, пронизанная gotchas (так же, как изменение итерации при итерации по ней) и обычно не стоит проблем. В большинстве случаев запись во временный файл (или рабочую память, в зависимости от того, что у вас больше - пространство для хранения или оперативная память), то удаление исходного файла и замена исходного файла на временный файл будут одинаково эффективными, как попытка выполнить такой же предмет на месте.

Но, если вы настаиваете, вот обобщенное решение:

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 как файл открывается в двоичном режиме, чтобы поддерживать согласованные разрывы строк при усечении/перезаписи.

ОБНОВЛЕНИЕ: меня интересовала реальная производительность различных приемов, представленных здесь, но у меня не было времени, чтобы проверить их вчера, поэтому, немного задержавшись, я создал тест, который мог бы пролить свет на него. Если вас интересуют только результаты, прокрутите весь путь вниз. Сначала я объясню, что я сравнивал и как я настроил тест. Я также предоставил все сценарии, чтобы вы могли запускать те же тесты в своей системе.

Что касается того, что, я протестировал все упомянутые методы в этом и других ответах, а именно, замену строки с использованием временного файла (функции temp_file_*) и использование функций редактирования на месте (in_place_*). У меня есть оба из тех, которые настроены в потоковой передаче (чтение строки за строкой, *_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))

Следующим шагом будет создание тестера, который будет выполнять эти функции как можно более изолированную среду, пытаясь получить справедливый ориентир для каждого из них. Мой тест структурирован как:

  • Три примера данных CSV генерируются как матрицы 1Mx10 (файлы ~ 200 МБ) случайных чисел с идентифицируемой линией, помещенной в начале, середине и конце их соответственно, тем самым генерируя тестовые примеры для трех экстремальных сценариев.
  • Файлы основных образцов данных копируются как временные файлы (поскольку удаление строк является разрушительным) перед каждым тестом.
  • Различные способы синхронизации файлов и очистки кеша используются для обеспечения чистых буферов перед началом каждого теста.
  • Тесты выполняются с использованием наивысшего приоритета (chrt -f 99) через /usr/bin/time для теста, поскольку Python не может действительно быть уверенным в точном измерении его производительности в таких сценариях.
  • Для сглаживания непредсказуемых колебаний выполняются по меньшей мере три прогона каждого теста.
  • Тесты также выполняются в Python 2.7 и Python 3.6 (CPython), чтобы убедиться, что между версиями существует согласованность производительности.
  • Все контрольные данные собираются и сохраняются как CSV для будущего анализа.

К сожалению, у меня не было системы, где я мог бы выполнить тест полностью изолированным, чтобы мои числа были получены от запуска его в гипервизоре. Это означает, что производительность ввода-вывода, вероятно, очень искажена, но она также должна влиять на все тесты, которые по-прежнему предоставляют сопоставимые данные. В любом случае, вы можете запустить этот тест в своей собственной системе, чтобы получить результаты, с которыми вы можете иметь отношение.

Я установил тестовый скрипт, выполняющий вышеупомянутый сценарий, как:

#!/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 обеспечивают небольшую площадь. На Python 3.xa на середине будет метод mmap.
  • Если проблема с хранилищем является проблемой, только функции in_place_* являются жизнеспособными.
  • Если оба они недостаточны, единственной последовательной методикой является in_place_stream но за счет времени обработки и увеличенных вызовов ввода-вывода (по сравнению с функциями *_wm).
  • in_place_* опасны, поскольку они могут привести к повреждению данных, если они остановлены на середине. temp_file_* (без проверки целостности) опасны только для файловых систем без транзакций.

Ответ 4

Используйте sed:

sed -ie "/Sahra/d" your_file

Изменить, Извините, я не полностью прочитал все теги и комментарии о необходимости использовать python. В любом случае, я бы, вероятно, попытался решить эту проблему с некоторой предварительной обработкой, используя некоторую оболочную утилиту, чтобы избежать использования всего этого дополнительного кода в других ответах. Но поскольку я не полностью знаю вашу проблему, это может быть невозможно?

Удачи!

Ответ 5

Вы можете сделать это с помощью Pandas. Если ваши данные сохраняются в файле data.csv, следующее должно помочь:

import pandas as pd

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

Ответ 6

Каков наиболее эффективный способ удаления строки Сара? Если возможно, я бы хотел избежать копирования всего файла.

Самый эффективный способ - перезаписать эту строку с тем, что игнорирует парсер ssv. Это позволяет избежать перемещения строк после удаленной.

Если ваш синтаксический анализатор csv может игнорировать пустые строки, перезапишите эту строку символами \n. В противном случае, если ваш синтаксический анализатор перечеркивает пробелы со значений, перезапишите эту строку с помощью (пробел).

Ответ 7

Это может помочь:

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