Elimine una sola fila de un csv sin copiar archivos

Existen múltiples preguntas de SO que abordan alguna forma de este tema, pero todas parecen ser terriblemente ineficientes para eliminar solo una fila de un archivo csv (generalmente implican copiar todo el archivo). Si tengo un csv formateado así:

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

¿Cuál es la forma más eficiente de eliminar la fila de Sarah? Si es posible, me gustaría evitar copiar todo el archivo.

Tienes un problema fundamental aquí. Ningún sistema de archivos actual (que yo sepa) proporciona una facilidad para eliminar un montón de bytes de la mitad de un archivo. Puede sobrescribir los bytes existentes o escribir un nuevo archivo. Por lo tanto, sus opciones son:

  • Cree una copia del archivo sin la línea ofensiva, elimine la antigua y cambie el nombre del nuevo archivo en su lugar. (Esta es la opción que quieres evitar).
  • Sobrescriba los bytes de la línea con algo que será ignorado. Dependiendo de exactamente lo que va a leer el archivo, un carácter de comentario podría funcionar o los espacios podrían funcionar (o posiblemente incluso \0 ). Sin embargo, si desea ser completamente genérico, esta no es una opción con archivos CSV, porque no hay un carácter de comentario definido.
  • Como última medida desesperada, podrías:
    • lee hasta la línea que quieres eliminar
    • leer el rest del archivo en la memoria
    • y sobrescriba la línea y todas las líneas subsiguientes con los datos que desea conservar.
    • truncar el archivo como la posición final (los sistemas de archivos generalmente lo permiten).

La última opción obviamente no ayuda mucho si está intentando eliminar la primera línea (pero es útil si desea eliminar una línea cerca del final). También es terriblemente vulnerable a estrellarse en medio del proceso.

Utilice sed:

 sed -ie "/Sahra/d" your_file 

Edit , Sorry, no leí completamente todas las tags y comentarios acerca de la necesidad de usar python. De cualquier manera, probablemente intentaría resolverlo con un poco de preprocesamiento usando alguna utilidad de shell para evitar todo el código adicional propuesto en las otras respuestas. Pero como no conozco su problema, puede que no sea posible.

¡Buena suerte!

Esta es una manera Tienes que cargar el rest del archivo en un búfer, pero es lo mejor que se me ocurre en 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() 

Resolví esto como si supieras el número de línea. Si desea verificar el texto, en lugar del bucle anterior:

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

Habrá una excepción si ‘Sarah’ no se encuentra.

Esto puede ser más eficiente si la línea que está eliminando está más cerca del final, pero no estoy seguro de que leer todo, dejar caer la línea y devolverla ahorrará mucho en comparación con el tiempo del usuario (considerando que esta es una aplicación Tk). Esto solo necesita abrirse y vaciarse una vez en el archivo, así que, a menos que los archivos sean extremadamente largos y Sarah esté muy abajo, probablemente no se notará.

Editar archivos en el lugar es una tarea plagada de errores (como la modificación de un iterable mientras se itera sobre él) y generalmente no vale la pena. En la mayoría de los casos, escribir en un archivo temporal (o en la memoria de trabajo, dependiendo de lo que tenga más: espacio de almacenamiento o RAM) y luego eliminar el archivo de origen y reemplazar el archivo de origen con el archivo temporal tendrá el mismo rendimiento que intentar hacer el archivo. Lo mismo en el lugar.

Pero, si insistes, aquí hay una solución generalizada:

 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 

Esto eliminará solo la línea que coincida con la función de comparación provista y luego se repetirá sobre el rest del archivo cambiando los datos sobre la línea ‘eliminada’. Tampoco necesitará cargar el rest del archivo en su memoria de trabajo. Para probarlo, con test.csv contiene:

  nombre, nombre, edad, sexo
 John, Doe, 28, m
 Sara, smith, 27, f
 Xavier, Moore, 19, m 

Puedes ejecutarlo como:

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

Y obtendrás test.csv con la línea de Sarah eliminada en el lugar:

  nombre, nombre, edad, sexo
 John, Doe, 28, m
 Xavier, Moore, 19, m 

Tenga en cuenta que estamos pasando una función de comparación de bytes , ya que el archivo se abre en modo binario para mantener saltos de línea consistentes al truncar / sobrescribir.

ACTUALIZACIÓN : Me interesó el rendimiento real de varias técnicas presentadas aquí, pero no tuve tiempo para probarlas ayer, así que con un poco de retraso he creado un punto de referencia que podría arrojar algo de luz sobre ello. Si solo está interesado en los resultados, desplácese hacia abajo. Primero, explicaré en qué consistí el benchmarking y cómo configuré la prueba. También proporcionaré todos los scripts para que pueda ejecutar el mismo punto de referencia en su sistema.

En cuanto a qué, he probado todas las técnicas mencionadas en esta y otras respuestas, a saber, el reemplazo de línea usando un archivo temporal (funciones temp_file_* ) y usando funciones de edición en el lugar ( in_place_* ). Tengo ambos configurados en los modos de transmisión (lectura línea por línea, *_stream funciones *_stream ) y memoria (lectura del rest del archivo en memoria de trabajo, funciones *_wm ). También agregué una técnica de eliminación de líneas en el lugar usando el módulo mmap (la función in_place_mmap ). La secuencia de comandos de referencia que contiene todas las funciones, así como un poco de lógica para controlar a través de la CLI es la siguiente:

 #!/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  [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)) 

El siguiente paso es construir un probador que ejecute estas funciones en un entorno lo más aislado posible, tratando de obtener un punto de referencia justo para cada una de ellas. Mi prueba se estructura como:

  • Tres CSV de datos de muestra se generan como matrices de 1Mx10 (~ 200 MB de archivos) de números aleatorios con una línea identificable colocada al principio, en la mitad y al final de ellos respectivamente, generando así casos de prueba para tres escenarios extremos.
  • Los archivos de datos de muestra maestros se copian como archivos temporales (ya que la eliminación de la línea es destructiva) antes de cada prueba.
  • Se emplean varios métodos de sincronización de archivos y borrado de caché para garantizar buffers limpios antes de que comience cada prueba.
  • Las pruebas se ejecutan utilizando la prioridad más alta ( chrt -f 99 ) a través de /usr/bin/time para el punto de referencia, ya que no se puede confiar en Python para medir con precisión su desempeño en escenarios como estos.
  • Se realizan al menos tres ejecuciones de cada prueba para suavizar las fluctuaciones impredecibles.
  • Las pruebas también se ejecutan en Python 2.7 y Python 3.6 (CPython) para ver si hay coherencia de rendimiento entre las versiones.
  • Todos los datos de referencia se recostackn y guardan como CSV para futuros análisis.

Desafortunadamente, no tenía un sistema a mano para poder ejecutar la prueba completamente aislada, por lo que mis números se obtienen al ejecutarla en un hipervisor. Esto significa que el rendimiento de E / S es probablemente muy sesgado, pero debería afectar de manera similar a todas las pruebas que todavía proporcionan datos comparables. De cualquier manera, puede realizar esta prueba en su propio sistema para obtener resultados con los que pueda relacionarse.

He establecido un script de prueba que realiza el escenario mencionado como:

 #!/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.") 

Finalmente (y TL; DR ): aquí están mis resultados: estoy extrayendo solo los mejores datos de tiempo y memoria del conjunto de resultados, pero puede obtener todos los conjuntos de resultados aquí: Python 2.7 Raw Test Data y Python 3.6 Raw Test Test .

Eliminación de la línea de archivos de Python - Resultados seleccionados


Basándome en los datos que reuní, un par de notas finales:

  • Si la memoria de trabajo es un problema (trabajar con archivos excepcionalmente grandes, etc.), solo las funciones *_stream proporcionan una pequeña huella. En Python 3.xa a mitad de camino sería la técnica mmap .
  • Si el almacenamiento es un problema, solo las funciones in_place_* son viables.
  • Si ambos son escasos, la única técnica consistente es in_place_stream pero a costa del tiempo de procesamiento y el aumento de las llamadas de E / S (en comparación con las funciones *_wm ).
  • in_place_* funciones in_place_* son peligrosas, ya que pueden provocar daños en los datos si se detienen a mitad de camino. temp_file_* funciones temp_file_* (sin controles de integridad) solo son peligrosas en sistemas de archivos no transaccionales.

Puedes hacerlo usando Pandas. Si sus datos se guardan en data.csv, lo siguiente debería ayudar:

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

¿Cuál es la forma más eficiente de eliminar la fila de Sarah? Si es posible, me gustaría evitar copiar todo el archivo.

La forma más eficiente es sobrescribir esa fila con algo que el analizador csv ignora. Esto evita tener que mover las filas siguiendo la eliminada.

Si su analizador csv puede ignorar líneas vacías, sobrescriba esa fila con \n símbolos. De lo contrario, si su analizador elimina los espacios en blanco de los valores, sobrescriba esa fila con (espacio) símbolos.

Esto podría ayudar:

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