Petit snippet pour obtenir un nom de fichier unique en Python 10


Bidouillerie du jour, bonjour.

import re
import os
 
def get_unique_path(path):
 
    # si le nom de fichier existe, on en cherche un autre
    while os.path.exists(path):
        # on vire l'extension
        base, ext = os.path.splitext(path)
        try:
            # on extrait le compteur si il existe
            base, counter, _ = re.split(r" \((\d+)\)$", base)
        except ValueError:
            counter = 0
 
        # on reconstruit le path
        path = "%s (%s)%s" % (base, int(counter) + 1, ext)
 
    return path

Le plus gros de l’astuce est dans :

base, counter, _ = re.split(r" \((\d+)\)$", base)

\((\d+)\)$ va matcher ‘espace(un nombre)’ à la fin d’une chaîne et r.split va retourner soit ['le chemin complet'] si il n’y a pas de compteur, soit ['base', 'compteur', ''] si il y en a un.

Du coup on unpack tout ça, _ étant utilisé pour signaler une variable inutilisée par convention et on a notre compteur, prêt à être incrémenté.

A l’usage, ça donne ça dans ipython :

>>> !rm /tmp/test*
>>> get_unique_path('/tmp/test.txt')
u'/tmp/test.txt'
>>> get_unique_path('/tmp/test.txt')
u'/tmp/test.txt'
>>> !touch /tmp/test.txt
>>> get_unique_path('/tmp/test.txt')
u'/tmp/test (1).txt'
>>> !touch "/tmp/test (1).txt"
>>> get_unique_path('/tmp/test.txt')
u'/tmp/test (2).txt'
>>> get_unique_path('/tmp/test (101).txt')
u'/tmp/test (101).txt'
>>> !touch '/tmp/test (101).txt'
>>> get_unique_path('/tmp/test (101).txt')
u'/tmp/test (102).txt'
>>> get_unique_path('/tmp/test')
u'/tmp/test'
>>> !touch /tmp/test
>>> get_unique_path('/tmp/test')
u'/tmp/test (1)'
>>> get_unique_path('/tmp/.test')
u'/tmp/.test'
>>> !touch "/tmp/.test"
>>> get_unique_path('/tmp/.test')
u'/tmp/.test (1)'
>>> get_unique_path('/tmp/test.path.text')
u'/tmp/test.path.text'
>>> !touch '/tmp/test.path.text'
>>> get_unique_path('/tmp/test.path.text')
u'/tmp/test.path (1).text'
>>> get_unique_path('/tmp/test.path (1)(1) (1).text')
u'/tmp/test.path (1)(1) (1).text'
>>> !touch '/tmp/test.path (1)(1) (1).text'
>>> get_unique_path('/tmp/test.path (1)(1) (1).text')
u'/tmp/test.path (1)(1) (2).text'

Bien entendu, si vos fichiers ne seront jamais visibles par l’utilisateur, il vaut mieux se simplifier la vie et utiliser uuid.uuid4().

Je pense qu’après le dossier sur les tests unitaires, je ferai un dossier regex. Aux alentours de 2018.

10 thoughts on “Petit snippet pour obtenir un nom de fichier unique en Python

  • JoJo

    heeuu, j’ai pas compris l’intérêt de ça par rapport à un truc du genre tempfile.mktemp… ?

  • bob

    Bonjour,

    \((\d+)\)$ va matcher ‘espace(un nombre)’

    Du coup on unpack tout ça

    Au revoir !

  • kontre

    @JoJo: j’ai eu la même réaction, mais en fait tu peux vouloir sauvegarder un fichier sans en écraser d’autres, sans que le fichier en question soit un fichier temporaire.

  • Sam Post author

    Rien, ce n’est pas le but de gérer le concurrence, juste de trouver les noms. Si tu as besoin de faire ça dans un environnement concurrent, il faut rajouter un système de lock, ce qui doit se faire hors du contexte de cette fonction. Ou mieux, créer une architecture qui n’accède pas aux ressources de manières concurrente. Typiquement l’uuid évite le problème, et sur un serveur ça a plus de sens vu qu’aucun user ne va voir le fichier. Si c’est dans le cas d’une UI monoutilisateur, le mieux est d’ignorer le cas et de laisser crasher, ça arrivera si rarement que pour un ou deux crash en 10 ans, c’est pas suffisamment important pour se faire chier.

  • Sam Post author

    @JoJo : typiquement c’est pour implémenter un copier / coller qui n’écrase pas les duplicates ou ce genre de chose.

  • Sam Post author

    Ben oui, ça a rien à voir avec les fichiers temporaires au final.

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.