web-dev-qa-db-fra.com

Supprimer une seule ligne d'un csv sans copier de fichiers

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.

20
SamBG

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:

  • Créez une copie du fichier sans la ligne incriminée, supprimez l'ancien et renommez le nouveau fichier en place. (C'est l'option que vous souhaitez éviter).
  • Remplacez les octets de la ligne par quelque chose qui sera ignoré. Selon exactement ce qui va lire le fichier, un caractère de commentaire peut fonctionner, ou des espaces peuvent fonctionner (ou peut-être même \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.
  • Comme dernière mesure désespérée, vous pourriez:
    • lire jusqu'à la ligne que vous souhaitez supprimer
    • lire le reste du fichier en mémoire
    • et écraser la ligne et toutes les lignes suivantes avec les données que vous souhaitez conserver.
    • tronquer le fichier comme position finale (les systèmes de fichiers le permettent généralement).

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.

27
Martin Bonner

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.

3
kabanus

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!

3
UlfR

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:

  • Trois échantillons CSV de données sont générés sous forme de matrices 1Mx10 (fichiers ~ 200MB) de nombres aléatoires avec une ligne identifiable placée respectivement au début, au milieu et à la fin, générant ainsi des cas de test pour trois scénarios extrêmes.
  • Les fichiers de données de l'échantillon principal sont copiés en tant que fichiers temporaires (car la suppression de ligne est destructrice) avant chaque test.
  • Diverses méthodes de synchronisation de fichiers et d'effacement du cache sont utilisées pour garantir des tampons propres avant le début de chaque test.
  • Les tests sont exécutés en utilisant la priorité la plus élevée (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.
  • Au moins trois cycles de chaque test sont effectués pour atténuer les fluctuations imprévisibles.
  • Les tests sont également exécutés dans Python 2.7 et Python 3.6 (CPython) pour voir s'il existe une cohérence des performances entre les versions).
  • Toutes les données de référence sont collectées et enregistrées au format CSV pour une analyse future.

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 .

Python File Line Removal - Selected Results


Sur la base des données que j'ai recueillies, quelques notes finales:

  • Si la mémoire de travail pose problème (utilisation de fichiers exceptionnellement volumineux, etc.), seul le *_stream les fonctions offrent un faible encombrement. Sur Python 3.x à mi-chemin serait la technique mmap.
  • Si le stockage pose problème, seul le in_place_* les fonctions sont viables.
  • Si les deux sont rares, la seule technique cohérente est le 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.
3
zwer

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)
2
shep4rd

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).

1
Maxim Egorushkin

Cela pourrait aider:

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