Tengo una ruta (incluido el directorio y el nombre del archivo).
Necesito probar si el nombre del archivo es válido, por ejemplo, si el sistema de archivos me permitirá crear un archivo con ese nombre.
El nombre del archivo tiene algunos caracteres Unicode .
Es seguro asumir que el segmento de directorio de la ruta es válido y accesible ( estaba tratando de hacer que la pregunta fuera más aplicable en general, y aparentemente estaba demasiado lejos ).
Yo mucho no quiero tener que escapar nada a menos que tenga a.
Publicaría algunos de los personajes de ejemplo con los que estoy tratando, pero aparentemente el sistema de intercambio de pila los elimina automáticamente. De todos modos, quiero mantener las entidades unicode estándar como ö
, y solo escapar de las cosas que no son válidas en un nombre de archivo.
Aquí está el truco. Es posible que ya exista (o no) un archivo en el destino de la ruta. Necesito conservar ese archivo si existe, y no crear un archivo si no existe.
Básicamente quiero comprobar si podía escribir en un camino sin tener que abrir el camino para la escritura (y la creación de archivos / archivo automático clobbering que por lo general conlleva).
Como tal:
try:
open(filename, 'w')
except OSError:
# handle error here
No es aceptable, porque sobrescribirá el archivo existente, que no quiero tocar (si está ahí), o creará dicho archivo si no lo está.
Sé que puedo hacer:
if not os.access(filePath, os.W_OK):
try:
open(filePath, 'w').close()
os.unlink(filePath)
except OSError:
# handle error here
Pero eso creará el archivo en el filePath
, que luego tendría que hacerlo os.unlink
.
Al final, parece que está gastando 6 o 7 líneas para hacer algo que debería ser tan simple os.isvalidpath(filePath)
o similar.
Aparte, necesito que esto se ejecute en (al menos) Windows y MacOS, así que me gustaría evitar cosas específicas de la plataforma.
''
fuente
Respuestas:
tl; dr
Llame a la
is_path_exists_or_creatable()
función definida a continuación.Estrictamente Python 3. Así es como rodamos.
Historia de dos preguntas
La pregunta de "¿Cómo pruebo la validez del nombre de ruta y, para nombres de ruta válidos, la existencia o capacidad de escritura de esas rutas?" son claramente dos preguntas separadas. Ambos son interesantes, y ninguno ha recibido una respuesta genuinamente satisfactoria aquí ... o, bueno, en cualquier lugar donde pudiera grep.
La respuesta de vikki probablemente sea la más cercana, pero tiene las notables desventajas de:
Vamos a arreglar todo eso.
Pregunta # 0: ¿Qué es la validez del nombre de ruta nuevamente?
Antes de arrojar nuestros frágiles trajes de carne a los moshpits de dolor plagados de pitones, probablemente deberíamos definir lo que queremos decir con "validez de nombre de ruta". ¿Qué define la validez exactamente?
Por "validez de nombre de ruta", nos referimos a la corrección sintáctica de un nombre de ruta con respecto a la sistema de archivos raíz del sistema actual, independientemente de si esa ruta o los directorios principales del mismo existen físicamente. Un nombre de ruta es sintácticamente correcto según esta definición si cumple con todos los requisitos sintácticos del sistema de archivos raíz.
Por "sistema de archivos raíz", queremos decir:
/
).%HOMEDRIVE%
la letra de unidad con el sufijo de dos puntos que contiene la instalación actual de Windows (normalmente, pero no necesariamenteC:
).El significado de "corrección sintáctica", a su vez, depende del tipo de sistema de archivos raíz. Para los sistemas de archivos
ext4
(y para la mayoría, pero no todos, compatibles con POSIX), un nombre de ruta es sintácticamente correcto si y solo si ese nombre de ruta:\x00
en Python). Este es un requisito estricto para todos los sistemas de archivos compatibles con POSIX.'a'*256
en Python). Un componente de la ruta es una subcadena más larga de una ruta de acceso que no contiene/
caracteres (por ejemplo,bergtatt
,ind
,i
, yfjeldkamrene
en el nombre de ruta/bergtatt/ind/i/fjeldkamrene
).Corrección sintáctica. Sistema de archivos raíz. Eso es.
Pregunta n. ° 1: ¿Cómo haremos ahora la validez del nombre de ruta?
Validar nombres de rutas en Python es sorprendentemente poco intuitivo. Estoy completamente de acuerdo con Fake Name aquí: el
os.path
paquete oficial debería proporcionar una solución lista para usar para esto. Por razones desconocidas (y probablemente poco convincentes), no es así. Afortunadamente, desenrollando su propia solución ad-hoc no es que desgarrador ...OK, en realidad lo es. Es peludo; es desagradable; probablemente se ríe mientras burbujea y se ríe mientras brilla. Pero que vas a hacer Nada.
Pronto descenderemos al abismo radiactivo del código de bajo nivel. Pero primero, hablemos de la tienda de alto nivel. El estándar
os.stat()
y lasos.lstat()
funciones generan las siguientes excepciones cuando se pasan nombres de ruta no válidos:FileNotFoundError
.WindowsError
cuyowinerror
atributo es123
( es decir,ERROR_INVALID_NAME
).'\x00'
), instancias deTypeError
.OSError
cuyoerrcode
atributo es:errno.ERANGE
. (Esto parece ser un error a nivel del sistema operativo, también conocido como "interpretación selectiva" del estándar POSIX).errno.ENAMETOOLONG
.Fundamentalmente, esto implica que solo los nombres de ruta que residen en directorios existentes son validables. Las funciones
os.stat()
yos.lstat()
generanFileNotFoundError
excepciones genéricas cuando se pasan nombres de ruta que residen en directorios no existentes, independientemente de si esos nombres de ruta no son válidos o no. La existencia del directorio tiene prioridad sobre la invalidez del nombre de ruta.¿Significa esto que los nombres de ruta que residen en directorios no existentes no son validables? Sí, a menos que modifiquemos esos nombres de ruta para que residan en directorios existentes. Sin embargo, ¿es eso factible incluso con seguridad? ¿La modificación de un nombre de ruta no debería impedirnos validar el nombre de ruta original?
Para responder a esta pregunta, recuerde que los nombres de ruta sintácticamente correctos en el
ext4
sistema de archivos no contienen componentes de ruta (A) que contengan bytes nulos o (B) de más de 255 bytes de longitud. Por lo tanto, unext4
nombre de ruta es válido si y solo si todos los componentes de la ruta en ese nombre de ruta son válidos. Esto es cierto para la mayoría de los sistemas de archivos de interés del mundo real .¿Nos ayuda realmente esa percepción pedante? Si. Reduce el problema mayor de validar el nombre de ruta completo de una sola vez al problema más pequeño de solo validar todos los componentes de la ruta en ese nombre de ruta. Cualquier nombre de ruta arbitrario es validable (independientemente de si ese nombre de ruta reside en un directorio existente o no) de una manera multiplataforma siguiendo el siguiente algoritmo:
/troldskog/faren/vild
en la lista['', 'troldskog', 'faren', 'vild']
)./troldskog
).os.stat()
oos.lstat()
. Si ese nombre de ruta y, por lo tanto, ese componente no es válido, se garantiza que esta llamada generará una excepción que exponga el tipo de invalidez en lugar de unaFileNotFoundError
excepción genérica . ¿Por qué? Porque ese nombre de ruta reside en un directorio existente. (La lógica circular es circular).¿Existe un directorio garantizado? Sí, pero normalmente solo uno: el directorio superior del sistema de archivos raíz (como se definió anteriormente).
Pasar nombres de ruta que residen en cualquier otro directorio (y por lo tanto no se garantiza que exista)
os.stat()
oos.lstat()
invita a condiciones de carrera, incluso si ese directorio se probó previamente para su existencia. ¿Por qué? Debido a que no se puede evitar que los procesos externos eliminen simultáneamente ese directorio después de que se haya realizado esa prueba, pero antes de que se pase ese nombre de ruta aos.stat()
oos.lstat()
. ¡Libera a los perros de la locura que devora la mente!También existe un beneficio secundario sustancial del enfoque anterior: la seguridad. (No se tiene que agradable?) Específicamente:
El enfoque anterior evita esto validando únicamente los componentes de la ruta de un nombre de ruta con el directorio raíz del sistema de archivos raíz. (Si incluso eso es obsoleto, lento o inaccesible, tiene problemas más grandes que la validación del nombre de ruta).
¿Perdió? Excelente. Vamos a empezar. (Se asume Python 3. Consulte "¿Qué es la esperanza frágil para 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://docs.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?
Hecho. No entrecerre los ojos ante ese código. ( Muerde. )
Pregunta # 2: ¿Posiblemente existencia o capacidad de creación de nombre de ruta no válido, eh?
Probar la existencia o capacidad de creación de nombres de ruta posiblemente inválidos es, dada la solución anterior, en su mayoría trivial. La pequeña clave aquí es llamar a la función previamente definida antes de probar la ruta pasada:
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
Hecho y hecho. Excepto que no del todo.
Pregunta # 3: Posiblemente existencia de nombre de ruta no válido o capacidad de escritura en Windows
Existe una advertencia. Por supuesto que sí.
Como admite la
os.access()
documentación oficial :Para sorpresa de nadie, Windows es el sospechoso habitual aquí. Gracias al uso extensivo de las listas de control de acceso (ACL) en los sistemas de archivos NTFS, el modelo simplista de bits de permiso POSIX se asigna mal a la realidad subyacente de Windows. Si bien esto (posiblemente) no es culpa de Python, podría ser una preocupación para las aplicaciones compatibles con Windows.
Si este es usted, se busca una alternativa más robusta. Si la ruta pasada no existe, en su lugar, intentamos crear un archivo temporal garantizado para ser eliminado inmediatamente en el directorio principal de esa ruta, una prueba de capacidad de creación más portátil (aunque costosa):
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
Sin embargo, tenga en cuenta que incluso esto puede no ser suficiente.
Gracias al Control de acceso de usuario (UAC), el siempre inimitable Windows Vista y todas las iteraciones posteriores del mismo mienten descaradamente sobre los permisos que pertenecen a los directorios del sistema. Cuando los usuarios que no son administradores intentan crear archivos en los directorios canónicos
C:\Windows
o en losC:\Windows\system32
directorios, UAC permite superficialmente al usuario hacerlo mientras en realidad aísla todos los archivos creados en una "Tienda virtual" en el perfil de ese usuario. (¿Quién podría haber imaginado que engañar a los usuarios tendría consecuencias perjudiciales a largo plazo?)Esto es Loco. Esto es Windows.
Pruébalo
¿Nos atrevemos? Es hora de probar las pruebas anteriores.
Dado que NULL es el único carácter prohibido en los nombres de ruta en los sistemas de archivos orientados a UNIX, aprovechemos eso para demostrar la cruda y dura verdad, ignorando las travesuras de Windows que no son ignorables, que francamente me aburren y me enojan en igual medida:
>>> 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
Más allá de la cordura. Más allá del dolor. Encontrará preocupaciones sobre la portabilidad de Python.
fuente
is_
. Este es mi defecto de carácter. No obstante, debidamente anotado: no se puede complacer a todo el mundo y, a veces, no se puede complacer a nadie. ;)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
Tenga en cuenta que
path.exists
puede fallar por más razones además dethe file is not there
que tenga que hacer pruebas más precisas, como comprobar si el directorio que lo contiene existe, etc.Después de mi discusión con el OP, resultó que el problema principal parece ser que el nombre del archivo puede contener caracteres que no están permitidos por el sistema de archivos. Por supuesto, deben eliminarse, pero el OP quiere mantener tanta legibilidad humana como lo permita el sistema de archivos.
Lamentablemente, no conozco ninguna buena solución para esto. Sin embargo, la respuesta de Cecil Curry analiza más de cerca la detección del problema.
fuente
or can be created
bueno, no leí eso de tu pregunta. La lectura de los permisos dependerá de la plataforma hasta cierto punto.os.path.exists(filePath)
técnicamente genera excepciones en nombres de ruta no válidos, esas excepciones deberían detectarse y diferenciarse explícitamente de otras excepciones no relacionadas. Además, la misma llamada regresaFalse
en rutas existentes para las que el usuario actual no tiene permisos de lectura. En resumen, maldad.Con Python 3, ¿qué tal:
try: with open(filename, 'x') as tempfile: # OSError if file exists or is invalid pass except OSError: # handle error here
Con la opción 'x' tampoco tenemos que preocuparnos por las condiciones de la carrera. Consulte la documentación aquí .
Ahora, esto creará un archivo temporal de muy corta duración si aún no existe, a menos que el nombre no sea válido. Si puedes vivir con eso, simplifica mucho las cosas.
fuente
open(filename,'r') #2nd argument is r and not w
abrirá el archivo o dará un error si no existe. Si hay un error, puede intentar escribir en la ruta, si no puede, obtendrá un segundo error
try: open(filename,'r') return True except IOError: try: open(filename, 'w') return True except IOError: return False
También eche un vistazo aquí sobre los permisos en Windows
fuente
tempfile.TemporaryFile()
que destruirá automáticamente el archivo temporal cuando salga del alcance.os.path.join
, así que no tengo problemas de escape. Además, realmente no tengo problemas de permisos de directorio . Tengo problemas con el nombre del directorio (y el nombre del archivo) .filename
contiene caracteres no válidos. He editado la respuestaintente
os.path.exists
esto buscará la ruta y regresaráTrue
si existe yFalse
si no.fuente