Il existe plusieurs SO questions portant sur une forme ou une autre de ce sujet, mais elles semblent toutes terriblement inefficaces pour supprimer une seule ligne d'un fichier csv (généralement, elles impliquent de copier l'intégralité du fichier). Si j'ai un csv formaté comme ceci:
fname,lname,age,sex
John,Doe,28,m
Sarah,Smith,27,f
Xavier,Moore,19,m
Quelle est la façon la plus efficace de supprimer la ligne de Sarah? Si possible, je voudrais éviter de copier tout le fichier.
Vous avez ici un problème fondamental. Aucun système de fichiers actuel (à ma connaissance) ne permet de supprimer un tas d'octets au milieu d'un fichier. Vous pouvez remplacer les octets existants ou écrire un nouveau fichier. Donc, vos options sont:
\0
). Si vous voulez être complètement générique, ce n'est pas une option avec les fichiers CSV, car il n'y a pas de caractère de commentaire défini.La dernière option n'aide évidemment pas beaucoup si vous essayez de supprimer la première ligne (mais elle est pratique si vous souhaitez supprimer une ligne vers la fin). Il est également horriblement vulnérable aux plantages au milieu du processus.
C'est une façon. Vous devez charger le reste du fichier dans un tampon, mais c'est le meilleur que je puisse penser 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()
J'ai résolu cela comme si vous connaissiez le numéro de ligne. Si vous voulez vérifier le texte, au lieu de la boucle ci-dessus:
pos = fd.tell()
while fd.readline().startswith('Sarah'): pos = fd.tell()
Il y aura une exception si "Sarah" n'est pas trouvée.
Cela peut être plus efficace si la ligne que vous supprimez est plus proche de la fin, mais je ne suis pas sûr de tout lire, de supprimer la ligne et de la sauvegarder permettra d'économiser beaucoup par rapport au temps de l'utilisateur (étant donné qu'il s'agit d'une application Tk). Il suffit également d'ouvrir et de vider une fois le fichier, donc à moins que les fichiers ne soient extrêmement longs et que Sarah soit vraiment loin, cela ne sera probablement pas perceptible.
Utilisez sed:
sed -ie "/Sahra/d" your_file
Edit, Désolé, je n'ai pas lu entièrement toutes les balises et commentaires sur la nécessité d'utiliser python. De toute façon, j'essaierais probablement de le résoudre avec un prétraitement en utilisant un utilitaire Shell pour éviter tout ce code supplémentaire proposé dans les autres réponses. Mais comme je ne connais pas parfaitement votre problème, il pourrait ne pas être possible?
Bonne chance!
L'édition de fichiers sur place est une tâche criblée de pièges (tout comme la modification d'un itérable tout en l'itérant) et ne vaut généralement pas la peine. Dans la plupart des cas, l'écriture dans un fichier temporaire (ou la mémoire de travail, en fonction de ce que vous avez de plus - de l'espace de stockage ou de la RAM), puis la suppression du fichier source et le remplacement du fichier source par le fichier temporaire seront tout aussi efficaces que d'essayer de faire le même chose en place.
Mais, si vous insistez, voici une solution généralisée:
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
Cela supprimera uniquement la ligne correspondant à la fonction de comparaison fournie, puis itérera sur le reste du fichier en déplaçant les données sur la ligne "supprimée". Vous n'aurez pas non plus besoin de charger le reste du fichier dans votre mémoire de travail. Pour le tester, avec test.csv
contenant:
fname, lname, age, sex John, Doe, 28, m Sarah, Smith, 27, f Xavier, Moore, 19, m
Vous pouvez l'exécuter comme:
remove_line("test.csv", lambda x: x.startswith(b"Sarah"))
Et vous obtiendrez test.csv
avec la ligne Sarah
supprimée sur place:
fname, lname, age, sex John, Doe, 28 ans, m Xavier, Moore, 19 ans, m
Gardez à l'esprit que nous transmettons une fonction de comparaison bytes
lorsque le fichier est ouvert en mode binaire pour conserver des sauts de ligne cohérents lors de la troncature/de l'écrasement.
[~ # ~] mise à jour [~ # ~] : Je m'intéressais aux performances réelles des différentes techniques présentées ici mais je n'avais pas le temps pour les tester hier, donc avec un peu de retard, j'ai créé un benchmark qui pourrait faire la lumière dessus. Si vous êtes uniquement intéressé par les résultats, faites défiler vers le bas. D'abord, je vais vous expliquer ce que j'étais en train de comparer et comment j'ai configuré le test. Je fournirai également tous les scripts afin que vous puissiez exécuter le même benchmark sur votre système.
En ce qui concerne quoi, j'ai testé toutes les techniques mentionnées dans cette réponse et d'autres, à savoir le remplacement de ligne à l'aide d'un fichier temporaire (temp_file_*
fonctions) et à l'aide d'une modification sur place (in_place_*
) les fonctions. J'ai tous les deux mis en place dans un streaming (lecture ligne par ligne, *_stream
fonctions) et la mémoire (lecture du reste du fichier dans la mémoire de travail, *_wm
fonctions) modes. J'ai également ajouté une technique de suppression de ligne sur place à l'aide du module mmap
(le in_place_mmap
une fonction). Le script de référence contenant toutes les fonctions ainsi qu'un petit bout de logique à contrôler via la CLI est le suivant:
#!/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))
La prochaine étape consiste à construire un testeur qui exécutera ces fonctions dans un environnement aussi isolé que possible, en essayant d'obtenir une référence équitable pour chacune d'entre elles. Mon test est structuré comme suit:
chrt -f 99
) par /usr/bin/time
pour le benchmark depuis Python ne peut pas vraiment faire confiance pour mesurer avec précision ses performances dans des scénarios comme ceux-ci.Malheureusement, je n'avais pas de système à portée de main où je pouvais exécuter le test complètement isolé, donc mes chiffres sont obtenus en l'exécutant dans un hyperviseur. Cela signifie que les performances d'E/S sont probablement très asymétriques, mais elles devraient également affecter tous les tests fournissant toujours des données comparables. Quoi qu'il en soit, vous pouvez exécuter ce test sur votre propre système pour obtenir des résultats auxquels vous pouvez vous identifier.
J'ai défini un script de test exécutant le scénario susmentionné comme suit:
#!/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.")
Enfin (et TL; DR ): voici mes résultats - je n'extrait que les meilleures données de temps et de mémoire de l'ensemble de résultats, mais vous pouvez obtenir les jeux de résultats complets ici: Python 2.7 Raw Test Data et Python 3.6 Raw Test Data .
Sur la base des données que j'ai recueillies, quelques notes finales:
*_stream
les fonctions offrent un faible encombrement. Sur Python 3.x à mi-chemin serait la technique mmap
.in_place_*
les fonctions sont viables.in_place_stream
mais au détriment du temps de traitement et de l'augmentation des appels d'E/S (par rapport à *_wm
les fonctions).in_place_*
les fonctions sont dangereuses car elles peuvent entraîner une corruption des données si elles sont arrêtées à mi-chemin. temp_file_*
les fonctions (sans vérification d'intégrité) ne sont dangereuses que sur les systèmes de fichiers non transactionnels.Vous pouvez le faire en utilisant Pandas. Si vos données sont enregistrées sous data.csv, les éléments suivants devraient vous aider:
import pandas as pd
df = pd.read_csv('data.csv')
df = df[df.fname != 'Sarah' ]
df.to_csv('data.csv', index=False)
Quelle est la façon la plus efficace de supprimer la ligne de Sarah? Si possible, je voudrais éviter de copier tout le fichier.
Le moyen le plus efficace consiste à remplacer cette ligne par quelque chose que l'analyseur csv ignore. Cela évite d'avoir à déplacer les lignes après celle supprimée.
Si votre analyseur csv peut ignorer les lignes vides, remplacez cette ligne par \n
symboles. Sinon, si votre analyseur supprime les espaces des valeurs, écrasez cette ligne avec des symboles (espace).
Cela pourrait aider:
with open("sample.csv",'r') as f:
for line in f:
if line.startswith('sarah'):continue
print(line)