Eu tenho um caminho (incluindo o diretório e o nome do arquivo).
Preciso testar se o nome do arquivo é válido, por exemplo, se o sistema de arquivos me permite criar um arquivo com esse nome.
O nome do arquivo contém alguns caracteres Unicode .
É seguro presumir que o segmento de diretório do caminho é válido e acessível ( eu estava tentando tornar a pergunta mais aplicável e, aparentemente, fui longe demais ).
Não quero muito escapar de nada, a menos que seja necessário .
Eu postaria alguns dos personagens de exemplo com os quais estou lidando, mas aparentemente eles são removidos automaticamente pelo sistema de troca de pilha. De qualquer forma, eu quero manter as entidades Unicode padrão como ö
, e apenas escapar coisas que são inválidas em um nome de arquivo.
Aqui está o problema. Pode (ou não) já haver um arquivo no destino do caminho. Preciso manter esse arquivo, se ele existir, e não criar um arquivo, se não existir.
Basicamente eu quero verificar se eu poderia escrever para um caminho sem realmente abrir o caminho para escrever (e a criação do arquivo / arquivo automático clobbering que normalmente implica).
Assim sendo:
try:
open(filename, 'w')
except OSError:
# handle error here
Não é aceitável, porque sobrescreverá o arquivo existente, que não quero mexer (se estiver lá), ou criará o referido arquivo se não estiver.
Eu sei que posso fazer:
if not os.access(filePath, os.W_OK):
try:
open(filePath, 'w').close()
os.unlink(filePath)
except OSError:
# handle error here
Mas isso criará o arquivo no filePath
, o que eu teria que fazer os.unlink
.
No final das contas, parece que está gastando 6 ou 7 linhas para fazer algo que deveria ser tão simples quanto os.isvalidpath(filePath)
ou similar.
Como um aparte, eu preciso que isso seja executado em (pelo menos) Windows e MacOS, então eu gostaria de evitar coisas específicas da plataforma.
``
fonte
os.path.isabs(PATH)
, mas isso não abrange o caminho relativo :-(.Respostas:
tl; dr
Chame a
is_path_exists_or_creatable()
função definida abaixo.Estritamente Python 3. É assim que funcionamos.
Um conto de duas perguntas
A questão de "Como faço para testar a validade do nome do caminho e, para nomes de caminho válidos, a existência ou capacidade de escrita desses caminhos?" são claramente duas questões distintas. Ambos são interessantes, e nenhum recebeu uma resposta genuinamente satisfatória aqui ... ou, bem, em qualquer lugar que eu pudesse pesquisar.
a resposta de vikki provavelmente é a que mais chega, mas tem as desvantagens notáveis de:
Vamos consertar tudo isso.
Pergunta # 0: O que é a validade do nome do caminho novamente?
Antes de jogar nossos frágeis trajes de carne nos moshpits crivados de dor, provavelmente deveríamos definir o que queremos dizer com "validade do nome do caminho". O que define a validade, exatamente?
Por "validade de nome de caminho", queremos dizer a correção sintática de um nome de caminho com respeito ao sistema de arquivos raiz do sistema atual - independentemente de esse caminho ou seus diretórios pais existirem fisicamente. Um nome de caminho está sintaticamente correto sob esta definição se estiver em conformidade com todos os requisitos sintáticos do sistema de arquivos raiz.
Por "sistema de arquivos raiz", queremos dizer:
/
).%HOMEDRIVE%
letra da unidade com sufixo de dois pontos contendo a instalação atual do Windows (normalmente, mas não necessariamenteC:
).O significado de "correção sintática", por sua vez, depende do tipo de sistema de arquivos raiz. Para
ext4
(e para a maioria, mas não todos os sistemas de arquivos compatíveis com POSIX), um nome de caminho está sintaticamente correto se e somente se esse nome de caminho:\x00
em Python). Este é um requisito difícil para todos os sistemas de arquivos compatíveis com POSIX.'a'*256
em Python). Um componente do caminho é uma subsequência mais longo de um caminho não contendo qualquer/
caracter (por exemplo,bergtatt
,ind
,i
, efjeldkamrene
em nome do caminho/bergtatt/ind/i/fjeldkamrene
).Correção sintática. Sistema de arquivos raiz. É isso aí.
Pergunta no. 1: Como agora devemos fazer a validade do nome do caminho?
Validar nomes de caminho em Python é surpreendentemente não intuitivo. Estou totalmente de acordo com o Fake Name aqui: o
os.path
pacote oficial deve fornecer uma solução pronta para o uso para isso. Por razões desconhecidas (e provavelmente incomuns), isso não acontece. Felizmente, desenrolando sua própria solução ad-hoc não é que arrasador ...OK, na verdade é. É cabeludo; é desagradável; provavelmente gargalha enquanto borbulha e ri enquanto brilha. Mas o que você vai fazer? Nuthin '.
Em breve desceremos ao abismo radioativo do código de baixo nível. Mas primeiro, vamos falar de uma loja de alto nível. O padrão
os.stat()
e asos.lstat()
funções levantam as seguintes exceções quando passam nomes de caminho inválidos:FileNotFoundError
.WindowsError
cujowinerror
atributo é123
(ou seja,ERROR_INVALID_NAME
).'\x00'
), instâncias deTypeError
.OSError
cujoerrcode
atributo é:errno.ERANGE
,. (Isso parece ser um bug no nível do sistema operacional, também conhecido como "interpretação seletiva" do padrão POSIX.)errno.ENAMETOOLONG
,.Crucialmente, isso implica que apenas nomes de caminhos residentes em diretórios existentes são validáveis. As funções
os.stat()
eos.lstat()
levantamFileNotFoundError
exceções genéricas quando nomes de caminho passados residem em diretórios não existentes, independentemente de esses nomes de caminho serem inválidos ou não. A existência do diretório tem precedência sobre a invalidade do nome do caminho.Isso significa que os nomes de caminhos que residem em diretórios não existentes não são validáveis? Sim - a menos que modifiquemos esses nomes de caminho para residir em diretórios existentes. No entanto, isso é ao menos viável com segurança? A modificação de um nome de caminho não deveria nos impedir de validar o nome do caminho original?
Para responder a esta pergunta, lembre-se de que os nomes de caminho sintaticamente corretos no
ext4
sistema de arquivos não contêm componentes de caminho (A) contendo bytes nulos ou (B) com mais de 255 bytes de comprimento. Portanto, umext4
nome de caminho é válido se e somente se todos os componentes do caminho nesse nome de caminho são válidos. Isso é verdade para a maioria dos sistemas de arquivos de interesse do mundo real .Esse insight pedante realmente nos ajuda? Sim. Isso reduz o problema maior de validar o nome do caminho completo de uma só vez para o problema menor de apenas validar todos os componentes do caminho naquele nome de caminho. Qualquer nome de caminho arbitrário é validável (independentemente de esse nome de caminho residir em um diretório existente ou não) em uma plataforma cruzada, seguindo o seguinte algoritmo:
/troldskog/faren/vild
na lista['', 'troldskog', 'faren', 'vild']
)./troldskog
).os.stat()
ouos.lstat()
. Se esse nome de caminho e, portanto, esse componente for inválido, essa chamada certamente levantará uma exceção expondo o tipo de invalidade em vez de umaFileNotFoundError
exceção genérica . Por quê? Porque esse nome de caminho reside em um diretório existente. (A lógica circular é circular.)Existe um diretório garantido para existir? Sim, mas normalmente apenas um: o diretório superior do sistema de arquivos raiz (conforme definido acima).
Passar nomes de caminho que residam em qualquer outro diretório (e, portanto, não há garantia de existência) para
os.stat()
ouos.lstat()
convida condições de corrida, mesmo se esse diretório tiver sido testado anteriormente. Por quê? Porque os processos externos não podem ser impedidos de remover simultaneamente aquele diretório depois que o teste foi executado, mas antes que o nome do caminho seja passado paraos.stat()
ouos.lstat()
. Liberte os cães da loucura arrebatadora!Também existe um benefício colateral substancial para a abordagem acima: segurança. (Não é que bom?) Especificamente:
A abordagem acima evita isso validando apenas os componentes do caminho de um nome de caminho em relação ao diretório raiz do sistema de arquivos raiz. (Se mesmo isso estiver desatualizado, lento ou inacessível, você terá problemas maiores do que a validação de nome de caminho.)
Perdido? Ótimo. Vamos começar. (Python 3 assumido. Consulte "What Is Fragile Hope for 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?
Feito. Não aperte os olhos para esse código. ( Morde. )
Pergunta # 2: Possivelmente existência de nome de caminho ou capacidade de criação inválida, hein?
Testar a existência ou capacidade de criação de nomes de caminhos possivelmente inválidos é, dada a solução acima, trivial. A pequena chave aqui é chamar a função definida anteriormente antes de testar o caminho aprovado:
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
Feito e feito. Exceto não exatamente.
Pergunta nº 3: Possibilidade de existência de nome de caminho inválido ou capacidade de gravação no Windows
Existe uma ressalva. Claro que sim.
Conforme admite a
os.access()
documentação oficial :Para surpresa de ninguém, o Windows é o suspeito de sempre aqui. Graças ao uso extensivo de Listas de Controle de Acesso (ACL) em sistemas de arquivos NTFS, o modelo simplista de bits de permissão POSIX mapeia mal para a realidade subjacente do Windows. Embora isso (indiscutivelmente) não seja culpa do Python, pode ser uma preocupação para aplicativos compatíveis com o Windows.
Se este for você, uma alternativa mais robusta é desejada. Se o caminho passado não existir, tentamos criar um arquivo temporário com garantia de exclusão imediata no diretório pai desse caminho - um teste de capacidade de criação mais portátil (se caro):
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
Observe, entretanto, que mesmo isso pode não ser suficiente.
Graças ao User Access Control (UAC), o sempre inimitável Windows Vista e todas as suas iterações subsequentes mentem descaradamente sobre as permissões relativas aos diretórios do sistema. Quando usuários não administradores tentam criar arquivos em diretórios
C:\Windows
ou canônicosC:\Windows\system32
, o UAC permite superficialmente que o usuário faça isso enquanto, na verdade, isola todos os arquivos criados em uma "Loja Virtual" no perfil do usuário. (Quem poderia imaginar que enganar os usuários teria consequências prejudiciais a longo prazo?)Isso é loucura. Este é o Windows.
Prove
Ousamos? É hora de testar os testes acima.
Visto que NULL é o único caractere proibido em nomes de caminho em sistemas de arquivos orientados para UNIX, vamos aproveitar isso para demonstrar a verdade nua e crua - ignorando travessuras não ignoráveis do Windows, que francamente me aborrecem e irritam em 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
Além da sanidade. Além da dor. Você encontrará questões de portabilidade do Python.
fonte
is_
. Esta é a minha falha de caráter. No entanto, devidamente observado: você não pode agradar a todos, e às vezes você não pode agradar a ninguém. ;)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
Observe que
path.exists
pode falhar por mais motivos do que apenasthe file is not there
então você pode ter que fazer testes mais refinados, como testar se o diretório que o contém existe e assim por diante.Após minha discussão com o OP, descobri que o principal problema parece ser que o nome do arquivo pode conter caracteres que não são permitidos pelo sistema de arquivos. É claro que eles precisam ser removidos, mas o OP deseja manter o máximo de legibilidade humana que o sistema de arquivos permitir.
Infelizmente, não conheço nenhuma boa solução para isso. No entanto, a resposta de Cecil Curry examina mais de perto a detecção do problema.
fonte
or can be created
bem, eu não li isso da sua pergunta. A leitura das permissões dependerá da plataforma até certo ponto.os.path.exists(filePath)
tecnicamente levanta exceções em nomes de caminho inválidos, essas exceções precisam ser capturadas explicitamente e diferenciadas de outras exceções não relacionadas. Além disso, a mesma chamada retornaFalse
em caminhos existentes para os quais o usuário atual não tem permissão de leitura. Em suma, maldade.Com Python 3, que tal:
try: with open(filename, 'x') as tempfile: # OSError if file exists or is invalid pass except OSError: # handle error here
Com a opção 'x', também não precisamos nos preocupar com as condições da corrida. Veja a documentação aqui .
Agora, isso IRÁ criar um arquivo temporário de duração muito curta se ele ainda não existir - a menos que o nome seja inválido. Se você pode viver com isso, simplifica muito as coisas.
fonte
open(filename,'r') #2nd argument is r and not w
irá abrir o arquivo ou dar um erro se ele não existir. Se houver um erro, você pode tentar escrever no caminho; se não puder, obterá um segundo erro
try: open(filename,'r') return True except IOError: try: open(filename, 'w') return True except IOError: return False
Também dê uma olhada aqui sobre as permissões no Windows
fonte
tempfile.TemporaryFile()
que destruirá automaticamente o arquivo temporário quando ele sair do escopo.os.path.join
, então não tenho problemas de escape de `\`. Além disso, não estou tendo problemas de permissão de diretório . Estou tendo problemas de nome de diretório (e nome de arquivo) .filename
contiver caracteres inválidos. Eu editei a respostatente
os.path.exists
isso irá verificar o caminho e retornarTrue
se existe eFalse
se não.fonte