138 votes

Vérifier si un chemin est valide en Python sans créer de fichier à la cible du chemin.

Je dispose d'un chemin d'accès (comprenant le nom du répertoire et du fichier).
Je dois tester si le nom du fichier est valide, c'est-à-dire si le système de fichiers me permet de créer un fichier avec un tel nom.
Le nom du fichier contient certains caractères unicode en elle.

On peut supposer que le segment répertoire du chemin est valide et accessible ( J'essayais de rendre la question plus générale et j'ai apparemment été trop loin. ).

Je ne veux absolument pas avoir à m'échapper de quoi que ce soit, sauf si je avoir à.

Je posterais bien quelques exemples de personnages avec lesquels je travaille, mais apparemment ils sont automatiquement supprimés par le système d'échange de piles. Quoi qu'il en soit, je veux conserver les entités unicode standard comme ö et n'échappe que les éléments non valides d'un nom de fichier.


Voici le problème. Il se peut (ou non) qu'il y ait déjà un fichier à la cible du chemin d'accès. Je dois conserver ce fichier s'il existe, et ne pas créer de fichier s'il n'existe pas.

En fait, je veux vérifier si je pourrait écrire sur un chemin sans pour autant ouvrir la voie à l'écriture (et la création automatique de fichiers qui en découle).

En tant que tel :

try:
    open(filename, 'w')
except OSError:
    # handle error here

à partir d'ici

n'est pas acceptable, car il écrasera le fichier existant, auquel je ne veux pas toucher (s'il existe), ou créera ce fichier s'il n'existe pas.

Je sais que je peux le faire :

if not os.access(filePath, os.W_OK):
    try:
        open(filePath, 'w').close()
        os.unlink(filePath)
    except OSError:
        # handle error here

Mais cela créer le fichier à l'adresse filePath que je devrais alors os.unlink .

En fin de compte, il semble que l'on dépense 6 ou 7 lignes pour faire quelque chose qui devrait être aussi simple que os.isvalidpath(filePath) ou similaire.


Par ailleurs, j'ai besoin que cela fonctionne (au moins) sous Windows et MacOS, et j'aimerais donc éviter les éléments spécifiques à une plate-forme.

``

226voto

Cecil Curry Points 4710

Tl;dr

Appeler le is_path_exists_or_creatable() définie ci-dessous.

Strictly Python 3. C'est ainsi que nous fonctionnons.

Un conte de deux questions

La question "Comment tester la validité des noms de chemin et, pour les noms de chemin valides, l'existence ou l'accessibilité en écriture de ces chemins" est clairement deux questions distinctes. Les deux sont intéressantes, et aucune n'a reçu de réponse vraiment satisfaisante ici... ou, bien, n'importe où que je puisse greper.

vikki 's répondre est probablement celui qui s'en rapproche le plus, mais il présente les inconvénients notables suivants :

  • Ouverture inutile ( ...et ne parviennent pas à se fermer de manière fiable ).
  • Écrire inutilement ( ...et ne parvenant pas à fermer ou à supprimer de manière fiable ) Fichiers de 0 octet.
  • Ignorer les erreurs spécifiques au système d'exploitation en faisant la différence entre les noms de chemin invalides non ignorables et les problèmes ignorables liés au système de fichiers. Sans surprise, ceci est critique sous Windows. ( Voir ci-dessous. )
  • Ignorer les conditions de course résultant de processus externes qui (re)déplacent simultanément les répertoires parents du chemin d'accès à tester. ( Voir ci-dessous. )
  • Ignorer les délais de connexion résultant du fait que ce nom de chemin réside sur des systèmes de fichiers périmés, lents ou temporairement inaccessibles. Ce chemin d'accès pourrait exposer les services publics à des risques potentiels DoS -Les attaques sont motivées par des considérations de sécurité. ( Voir ci-dessous. )

Nous allons remédier à tout cela.

Question #0 : Qu'est-ce que la validité du nom de chemin ?

Avant de jeter nos fragiles combinaisons de viande dans les fosses de douleur criblées de pythons, nous devrions probablement définir ce que nous entendons par "validité du nom de chemin". Qu'est-ce qui définit la validité, exactement ?

Par "validité du nom de chemin", nous entendons la norme l'exactitude syntaxique d'un nom de chemin par rapport à la Système de fichiers racine du système actuel - que ce chemin ou ses répertoires parents existent physiquement ou non. Un nom de chemin est syntaxiquement correct selon cette définition s'il respecte toutes les exigences syntaxiques du système de fichiers racine.

Par "système de fichiers racine", nous entendons :

  • Sur les systèmes compatibles POSIX, le système de fichiers monté dans le répertoire Root ( / ).
  • Sous Windows, le système de fichiers monté sur %HOMEDRIVE% , la lettre du lecteur avec les deux points qui contient l'installation actuelle de Windows (typiquement mais pas nécessairement C: ).

La signification de la "correction syntaxique" dépend à son tour du type de système de fichiers racine. Pour les ext4 (et la plupart des mais pas tous les systèmes de fichiers compatibles POSIX), un nom de chemin d'accès est syntaxiquement correct si et seulement si ce nom de chemin d'accès est correct :

  • Ne contient pas d'octets nuls (c'est-à-dire, \x00 en Python). Il s'agit d'une exigence absolue pour tous les systèmes de fichiers compatibles avec POSIX.
  • Ne contient pas de composants de chemin d'accès d'une longueur supérieure à 255 octets (par ex, 'a'*256 en Python). Un composant de chemin est la plus longue chaîne de caractères d'un nom de chemin ne contenant pas de / (par exemple, bergtatt , ind , i y fjeldkamrene dans le chemin d'accès /bergtatt/ind/i/fjeldkamrene ).

Correction syntaxique. Système de fichiers racine. C'est tout ce qu'il y a à faire.

Question n° 1 : Comment pouvons-nous désormais assurer la validité des noms de domaine ?

La validation des noms de chemin en Python est étonnamment non intuitive. Je suis tout à fait d'accord avec Faux nom ici : le site officiel os.path devrait fournir une solution prête à l'emploi. Pour des raisons inconnues (et probablement peu convaincantes), ce n'est pas le cas. Heureusement, dérouler votre propre solution ad-hoc n'est pas que qui fait froid dans le dos...

O.K., c'est en fait le cas. C'est poilu, c'est méchant, ça glousse sans doute quand ça brûle et ça rigole quand ça brille. Mais qu'allez-vous faire ? Rien.

Nous allons bientôt descendre dans l'abîme radioactif du code de bas niveau. Mais tout d'abord, parlons du code de haut niveau. La norme os.stat() y os.lstat() lèvent les exceptions suivantes lorsqu'on leur transmet des noms de chemin non valides :

  • Pour les noms de chemin résidant dans des répertoires inexistants, les instances de FileNotFoundError .
  • Pour les noms de chemin résidant dans des répertoires existants :
    • Sous Windows, les instances de WindowsError dont winerror est 123 (c'est-à-dire, ERROR_INVALID_NAME ).
    • Sous tous les autres systèmes d'exploitation :
    • Pour les noms de chemin contenant des octets nuls (c'est-à-dire, '\x00' ), les instances de TypeError .
    • Pour les noms de chemin contenant des éléments de chemin d'une longueur supérieure à 255 octets, les instances de OSError dont errcode est l'attribut :
      • Sous SunOS et la famille de systèmes d'exploitation *BSD, errno.ERANGE . (Il semble s'agir d'un bogue au niveau du système d'exploitation, autrement appelé "interprétation sélective" de la norme POSIX).
      • Sous tous les autres systèmes d'exploitation, errno.ENAMETOOLONG .

Cela implique notamment que seuls les noms de chemin résidant dans des répertoires existants sont valables. Les os.stat() y os.lstat() soulèvent des questions génériques FileNotFoundError des exceptions lorsqu'on lui transmet des noms de chemin résidant dans des répertoires inexistants, que ces noms de chemin soient invalides ou non. L'existence d'un répertoire est prioritaire sur l'invalidité d'un nom de chemin.

Cela signifie-t-il que les noms de chemin résidant dans des répertoires inexistants sont pas validable ? Oui - à moins que nous ne modifiions ces noms de chemin pour qu'ils résident dans des répertoires existants. Mais est-ce possible en toute sécurité ? La modification d'un nom de chemin ne devrait-elle pas nous empêcher de valider le nom de chemin original ?

Pour répondre à cette question, rappelons que les noms de chemin syntaxiquement corrects sur le serveur ext4 le système de fichiers ne contient pas de chemin d'accès (A) contenant des octets nuls ou (B) d'une longueur supérieure à 255 octets. Par conséquent, un ext4 est valide si et seulement si tous les composants de ce chemin sont valides. C'est le cas de le plus Systèmes de fichiers réels d'intérêt.

Ce point de vue pédant nous aide-t-il réellement ? Oui. Elle réduit le problème plus large de la validation du nom de chemin complet en une seule fois au problème plus petit de la validation de tous les composants du chemin dans ce nom de chemin. Tout nom de chemin arbitraire peut être validé (qu'il réside ou non dans un répertoire existant) de manière multiplateforme en suivant l'algorithme suivant :

  1. Diviser ce nom de chemin en composants de chemin (par exemple, le nom de chemin /troldskog/faren/vild dans la liste ['', 'troldskog', 'faren', 'vild'] ).
  2. Pour chacun de ces composants :
    1. Joindre le chemin d'accès d'un répertoire dont l'existence est garantie avec ce composant dans un nouveau chemin d'accès temporaire (par ex, /troldskog ) .
    2. Transmettez ce chemin d'accès à os.stat() o os.lstat() . Si ce nom de chemin, et donc ce composant, n'est pas valide, cet appel est assuré de lever une exception exposant le type d'invalidité plutôt qu'une exception générique de type FileNotFoundError exception. Pourquoi ? Parce que ce chemin d'accès se trouve dans un répertoire existant. (La logique circulaire est circulaire.)

Existe-t-il un répertoire dont l'existence est garantie ? Oui, mais généralement un seul : le répertoire le plus haut du système de fichiers Root (tel que défini ci-dessus).

Transmettre des noms de chemin résidant dans un autre répertoire (et dont l'existence n'est donc pas garantie) à l'option os.stat() o os.lstat() invite à des conditions de course, même si l'existence de ce répertoire a été testée précédemment. Pourquoi ? Parce qu'il est impossible d'empêcher des processus externes de supprimer simultanément ce répertoire après ce test a été effectué mais avant ce nom de chemin est transmis à os.stat() o os.lstat() . Lâchez les chiens de la folie de l'esprit !

L'approche susmentionnée présente également un avantage secondaire substantiel : la sécurité. (N'est-ce pas que agréable ?) En particulier :

Les applications frontales qui valident des noms de chemin arbitraires provenant de sources non fiables en transmettant simplement ces noms de chemin à la fonction os.stat() o os.lstat() sont susceptibles de faire l'objet d'attaques par déni de service (DoS) et d'autres manigances de type "black-hat". Des utilisateurs malveillants peuvent tenter de valider de manière répétée des noms de chemin résidant sur des systèmes de fichiers connus pour être périmés ou lents (par exemple, les partages NFS Samba) ; dans ce cas, l'enregistrement aveugle des noms de chemin entrants est susceptible d'échouer avec des dépassements de délai de connexion ou de consommer plus de temps et de ressources que votre faible capacité à supporter le chômage.

L'approche ci-dessus permet d'éviter ce problème en ne validant les composants d'un nom de chemin que par rapport au répertoire racine du système de fichiers racine. (Si même c'est est périmé, lent ou inaccessible, vous avez des problèmes plus importants que la validation du nom de chemin).

Perdu ? Très bien. Commençons. (Python 3 assumé. Voir "Qu'est-ce que l'espoir fragile" pour 300, leycec ?")

import errno, os

# Sadly, Python fails to provide the following magic number for us.
ERROR_INVALID_NAME = 123
'''
Windows-specific error code indicating an invalid pathname.

See Also
----------
https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
    Official listing of all such codes.
'''

def is_pathname_valid(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname for the current OS;
    `False` otherwise.
    '''
    # If this pathname is either not a string or is but is empty, this pathname
    # is invalid.
    try:
        if not isinstance(pathname, str) or not pathname:
            return False

        # Strip this pathname's Windows-specific drive specifier (e.g., `C:\`)
        # if any. Since Windows prohibits path components from containing `:`
        # characters, failing to strip this `:`-suffixed prefix would
        # erroneously invalidate all valid absolute Windows pathnames.
        _, pathname = os.path.splitdrive(pathname)

        # Directory guaranteed to exist. If the current OS is Windows, this is
        # the drive to which Windows was installed (e.g., the "%HOMEDRIVE%"
        # environment variable); else, the typical root directory.
        root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
            if sys.platform == 'win32' else os.path.sep
        assert os.path.isdir(root_dirname)   # ...Murphy and her ironclad Law

        # Append a path separator to this directory if needed.
        root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep

        # Test whether each path component split from this pathname is valid or
        # not, ignoring non-existent and non-readable path components.
        for pathname_part in pathname.split(os.path.sep):
            try:
                os.lstat(root_dirname + pathname_part)
            # If an OS-specific exception is raised, its error code
            # indicates whether this pathname is valid or not. Unless this
            # is the case, this exception implies an ignorable kernel or
            # filesystem complaint (e.g., path not found or inaccessible).
            #
            # Only the following exceptions indicate invalid pathnames:
            #
            # * Instances of the Windows-specific "WindowsError" class
            #   defining the "winerror" attribute whose value is
            #   "ERROR_INVALID_NAME". Under Windows, "winerror" is more
            #   fine-grained and hence useful than the generic "errno"
            #   attribute. When a too-long pathname is passed, for example,
            #   "errno" is "ENOENT" (i.e., no such file or directory) rather
            #   than "ENAMETOOLONG" (i.e., file name too long).
            # * Instances of the cross-platform "OSError" class defining the
            #   generic "errno" attribute whose value is either:
            #   * Under most POSIX-compatible OSes, "ENAMETOOLONG".
            #   * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE".
            except OSError as exc:
                if hasattr(exc, 'winerror'):
                    if exc.winerror == ERROR_INVALID_NAME:
                        return False
                elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
                    return False
    # If a "TypeError" exception was raised, it almost certainly has the
    # error message "embedded NUL character" indicating an invalid pathname.
    except TypeError as exc:
        return False
    # If no exception was raised, all path components and hence this
    # pathname itself are valid. (Praise be to the curmudgeonly python.)
    else:
        return True
    # If any other exception was raised, this is an unrelated fatal issue
    # (e.g., a bug). Permit this exception to unwind the call stack.
    #
    # Did we mention this should be shipped with Python already?

Fait. Ne louchez pas sur ce code. ( Il mord. )

Question #2 : Existence ou création d'un nom de chemin possiblement invalide, hein ?

Tester l'existence ou la possibilité de créer des noms de chemin éventuellement invalides est, compte tenu de la solution décrite ci-dessus, pratiquement trivial. La petite clé ici est d'appeler la fonction précédemment définie avant tester le chemin passé :

def is_path_creatable(pathname: str) -> bool:
    '''
    `True` if the current user has sufficient permissions to create the passed
    pathname; `False` otherwise.
    '''
    # Parent directory of the passed path. If empty, we substitute the current
    # working directory (CWD) instead.
    dirname = os.path.dirname(pathname) or os.getcwd()
    return os.access(dirname, os.W_OK)

def is_path_exists_or_creatable(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname for the current OS _and_
    either currently exists or is hypothetically creatable; `False` otherwise.

    This function is guaranteed to _never_ raise exceptions.
    '''
    try:
        # To prevent "os" module calls from raising undesirable exceptions on
        # invalid pathnames, is_pathname_valid() is explicitly called first.
        return is_pathname_valid(pathname) and (
            os.path.exists(pathname) or is_path_creatable(pathname))
    # Report failure on non-fatal filesystem complaints (e.g., connection
    # timeouts, permissions issues) implying this path to be inaccessible. All
    # other exceptions are unrelated fatal issues and should not be caught here.
    except OSError:
        return False

Terminé y fait. Mais pas tout à fait.

Question #3 : Existence ou possibilité d'écriture d'un nom de chemin invalide sous Windows

Une mise en garde s'impose. Bien sûr qu'il y en a une.

En tant qu'organe officiel de l os.access() la documentation admet :

Nota: Les opérations d'E/S peuvent échouer même si os.access() indique qu'ils pourraient réussir, en particulier pour les opérations sur les systèmes de fichiers réseau qui peuvent avoir une sémantique de permissions allant au-delà du modèle POSIX habituel de bits de permission.

Sans surprise, Windows est le suspect habituel. Grâce à l'utilisation intensive des listes de contrôle d'accès (ACL) sur les systèmes de fichiers NTFS, le modèle simpliste POSIX des bits de permission s'adapte mal à la réalité sous-jacente de Windows. Bien que ce ne soit (sans doute) pas la faute de Python, cela peut néanmoins poser problème pour les applications compatibles avec Windows.

Si c'est votre cas, une alternative plus robuste est souhaitée. Si le chemin transmis pas existe, nous essayons plutôt de créer un fichier temporaire dont la suppression immédiate est garantie dans le répertoire parent de ce chemin - un test de créabilité plus portable (bien que coûteux) :

import os, tempfile

def is_path_sibling_creatable(pathname: str) -> bool:
    '''
    `True` if the current user has sufficient permissions to create **siblings**
    (i.e., arbitrary files in the parent directory) of the passed pathname;
    `False` otherwise.
    '''
    # Parent directory of the passed path. If empty, we substitute the current
    # working directory (CWD) instead.
    dirname = os.path.dirname(pathname) or os.getcwd()

    try:
        # For safety, explicitly close and hence delete this temporary file
        # immediately after creating it in the passed path's parent directory.
        with tempfile.TemporaryFile(dir=dirname): pass
        return True
    # While the exact type of exception raised by the above function depends on
    # the current version of the Python interpreter, all such types subclass the
    # following exception superclass.
    except EnvironmentError:
        return False

def is_path_exists_or_creatable_portable(pathname: str) -> bool:
    '''
    `True` if the passed pathname is a valid pathname on the current OS _and_
    either currently exists or is hypothetically creatable in a cross-platform
    manner optimized for POSIX-unfriendly filesystems; `False` otherwise.

    This function is guaranteed to _never_ raise exceptions.
    '''
    try:
        # To prevent "os" module calls from raising undesirable exceptions on
        # invalid pathnames, is_pathname_valid() is explicitly called first.
        return is_pathname_valid(pathname) and (
            os.path.exists(pathname) or is_path_sibling_creatable(pathname))
    # Report failure on non-fatal filesystem complaints (e.g., connection
    # timeouts, permissions issues) implying this path to be inaccessible. All
    # other exceptions are unrelated fatal issues and should not be caught here.
    except OSError:
        return False

Il convient toutefois de noter que même les este peut ne pas suffire.

Grâce au contrôle d'accès utilisateur (UAC), l'inimitable Windows Vista et toutes ses itérations ultérieures mentir de manière flagrante sur les autorisations relatives aux répertoires du système. Lorsque des utilisateurs qui ne sont pas administrateurs tentent de créer des fichiers dans l'un ou l'autre des répertoires canoniques C:\Windows o C:\Windows\system32 l'UAC autorise superficiellement l'utilisateur à le faire, alors qu'il n'a pas le droit de le faire. en fait isoler tous les fichiers créés dans un "magasin virtuel" dans le profil de l'utilisateur. (Qui aurait pu imaginer que le fait de tromper les utilisateurs aurait des conséquences néfastes à long terme ?)

C'est de la folie. C'est Windows.

Le prouver

Osons-nous ? Il est temps de mettre à l'épreuve les tests susmentionnés.

Puisque NULL est le seul caractère interdit dans les noms de chemin sur les systèmes de fichiers orientés UNIX, profitons-en pour démontrer la vérité pure et dure - en ignorant les manigances Windows non ignorables, qui franchement m'ennuient et me mettent en colère à parts égales :

>>> print('"foo.bar" valid? ' + str(is_pathname_valid('foo.bar')))
"foo.bar" valid? True
>>> print('Null byte valid? ' + str(is_pathname_valid('\x00')))
Null byte valid? False
>>> print('Long path valid? ' + str(is_pathname_valid('a' * 256)))
Long path valid? False
>>> print('"/dev" exists or creatable? ' + str(is_path_exists_or_creatable('/dev')))
"/dev" exists or creatable? True
>>> print('"/dev/foo.bar" exists or creatable? ' + str(is_path_exists_or_creatable('/dev/foo.bar')))
"/dev/foo.bar" exists or creatable? False
>>> print('Null byte exists or creatable? ' + str(is_path_exists_or_creatable('\x00')))
Null byte exists or creatable? False

Au-delà de la raison. Au-delà de la douleur. Vous trouverez les problèmes de portabilité de Python.

59voto

Nobody Points 4081
if os.path.exists(filePath):
    #the file is there
elif os.access(os.path.dirname(filePath), os.W_OK):
    #the file does not exists but write privileges are given
else:
    #can not write there

Il convient de noter que path.exists peut échouer pour d'autres raisons que la seule the file is not there Il se peut donc que vous deviez effectuer des tests plus fins, par exemple en vérifiant que le répertoire contenant le fichier existe, etc.


Après ma discussion avec l'OP, il s'est avéré que le problème principal semble être que le nom du fichier peut contenir des caractères qui ne sont pas autorisés par le système de fichiers. Bien sûr, ils doivent être supprimés, mais l'OP veut maintenir autant de lisibilité humaine que le système de fichiers le permet.

Malheureusement, je ne connais pas de bonne solution à ce problème. Cependant, je ne connais pas de bonne solution pour cela. Réponse de Cecil Curry s'intéresse de plus près à la détection du problème.

13voto

Zachary Points 337

J'ai trouvé un module PyPI appelé pathvalidate .

pip install pathvalidate

Il contient une fonction appelée sanitize_filepath qui prend un chemin de fichier et le convertit en un chemin de fichier valide :

from pathvalidate import sanitize_filepath
file1 = "ap:lle/fi:le"
print(sanitize_filepath(file1))
# Output: "apple/file"

Il fonctionne également avec les noms réservés. Si vous lui donnez le chemin d'accès au fichier con il renverra con_ .

Avec cette connaissance, nous pouvons vérifier si le chemin d'accès au fichier saisi est égal au chemin d'accès assaini, ce qui signifie que le chemin d'accès au fichier est valide.

import os
from pathvalidate import sanitize_filepath

def check(filePath):
    if os.path.exists(filePath):
        return True
    if filePath == sanitize_filepath(filePath):
        return True
    return False

11voto

Stephen Miller Points 379

Avec Python 3, pourquoi pas :

try:
    with open(filename, 'x') as tempfile: # OSError if file exists or is invalid
        pass
except OSError:
    # handle error here

Avec l'option "x", nous n'avons pas non plus à nous préoccuper des conditions de course. Voir la documentation aquí .

Cela créera un fichier temporaire de très courte durée s'il n'existe pas déjà - à moins que le nom ne soit pas valide. Si vous pouvez vous en accommoder, cela simplifie grandement les choses.

5voto

vikki Points 1699
open(filename,'r')   #2nd argument is r and not w

ouvrira le fichier ou affichera une erreur s'il n'existe pas. S'il y a une erreur, vous pouvez essayer d'écrire dans le chemin, si vous n'y arrivez pas, vous obtenez une deuxième erreur.

try:
    open(filename,'r')
    return True
except IOError:
    try:
        open(filename, 'w')
        return True
    except IOError:
        return False

A voir également aquí sur les autorisations sous Windows

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X