[python] 경로의 대상에 파일을 생성하지 않고 Python에서 경로가 유효한지 확인하십시오.

경로 (디렉토리 및 파일 이름 포함)가 있습니다.
파일 이름이 유효한지 테스트해야합니다. 예를 들어 파일 시스템에서 그러한 이름으로 파일을 만들 수 있는지 여부를 테스트해야합니다.
파일 이름 에는 일부 유니 코드 문자 가 있습니다.

경로의 디렉토리 세그먼트가 유효하고 액세스 가능하다고 가정하는 것이 안전합니다 ( 질문을 더 일반적으로 적용 할 수 있도록 노력했고 분명히 너무 멀리갔습니다 ).

난 아주 많이 나는하지 않는 한 아무것도 탈출하고 싶지 않아 에.

내가 다루는 예제 문자 중 일부를 게시했지만 스택 교환 시스템에 의해 자동으로 제거되는 것 같습니다. 어쨌든, 나는 같은 표준 유니 코드 엔티티를 유지 ö하고 파일 이름에서 유효하지 않은 것만 이스케이프하고 싶습니다 .


여기에 캐치가 있습니다. 경로의 대상에 이미 파일이있을 수도 있고 없을 수도 있습니다. 해당 파일이 있으면 보관하고, 없으면 파일을 만들지 않습니다.

기본적으로 실제로 쓰기위한 경로 를 열지 않고 경로에 쓸 있는지 확인하고 싶습니다 (그리고 일반적으로 수반되는 자동 파일 생성 / 파일 클로버 링).

이와 같이 :

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

여기에서

내가 만지고 싶지 않은 기존 파일을 덮어 쓰거나 (존재하는 경우) 그렇지 않은 경우 해당 파일을 생성하기 때문에 허용되지 않습니다.

나는 내가 할 수 있다는 것을 안다.

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

그러나 그러면 에서 파일 이 생성filePath 되고 os.unlink.

결국, 간단 os.isvalidpath(filePath)하거나 비슷한 일을하기 위해 6 줄 또는 7 줄을 쓰는 것처럼 보입니다 .


제쳐두고, (적어도) Windows 및 MacOS에서 실행하려면 이것이 필요하므로 플랫폼 특정 항목을 피하고 싶습니다.



답변

tl; dr

is_path_exists_or_creatable()아래 정의 된 함수를 호출하십시오 .

Strictly Python 3. 이것이 바로 우리가하는 방식입니다.

두 가지 질문에 대한 이야기

“경로 이름의 유효성과 유효한 경로 이름의 경우 해당 경로의 존재 또는 쓰기 가능성을 어떻게 테스트합니까?” 분명히 두 가지 질문입니다. 둘 다 잘, 흥미, 및도 여기에 진정으로 만족스러운 답변을받은 … 또는 어디서나 내가 grep을 할 수있다.

vikki대답은 아마도 가장 가깝지만 다음과 같은 현저한 단점이 있습니다.

  • 불필요 하게 파일 핸들 열고 ( … 그런 다음 안정적으로 닫지 못함 ).
  • 불필요하게 0 바이트 파일 쓰기 ( … 그런 다음 신뢰할 수있는 닫기 또는 삭제 실패 ).
  • 무시할 수없는 잘못된 경로 이름과 무시할 수있는 파일 시스템 문제를 구분하는 OS 관련 오류를 무시합니다. 당연히 이것은 Windows에서 중요합니다. ( 아래 참조. )
  • 테스트 할 경로 이름의 상위 디렉토리를 동시에 (제거) 이동하는 외부 프로세스로 인한 경쟁 조건을 무시합니다. ( 아래 참조. )
  • 부실, 느리거나 일시적으로 액세스 할 수없는 파일 시스템에있는이 경로 이름으로 인한 연결 시간 초과를 무시합니다. 이는 공공 서비스를 잠재적 인 DoS 기반 공격에 노출시킬 있습니다. ( 아래 참조. )

우리는 모든 것을 고칠 것입니다.

질문 # 0 : 경로 이름 유효성이 다시 무엇입니까?

우리의 연약한 고기 옷을 비단뱀이 가득한 고통의 모 쉬핏에 던지기 전에 우리는 아마도 “경로 이름 유효성”이라는 의미를 정의해야 할 것입니다. 타당성을 정확히 정의하는 것은 무엇입니까?

“경로 이름 유효성”이란 현재 시스템 의 루트 파일 시스템 에 대한 경로 이름 의 구문 적 정확성 을 의미 합니다. 해당 경로 또는 부모 디렉토리가 물리적으로 존재하는지 여부에 관계없이. 루트 파일 시스템의 모든 구문 요구 사항을 준수하는 경우 경로 이름은이 정의에서 구문 적으로 정확합니다.

“루트 파일 시스템”은 다음을 의미합니다.

  • POSIX 호환 시스템에서 파일 시스템은 루트 디렉토리 ( /)에 마운트됩니다 .
  • Windows에서 파일 시스템 %HOMEDRIVE%은 현재 Windows 설치를 포함하는 콜론 접미어가 붙은 드라이브 문자 인에 마운트됩니다 (일반적으로 반드시 그런 것은 아님 C:).

차례로 “구문 정확성”의 의미는 루트 파일 시스템의 유형에 따라 다릅니다. 의 경우 ext4(대부분 만 하지 모든 POSIX 호환) 파일 시스템, 경로 이름 구문이 올바른지 여부와 해당 경로의 경우 :

  • null 바이트를 포함하지 않습니다 (예 : \x00Python). 이것은 모든 POSIX 호환 파일 시스템에 대한 어려운 요구 사항입니다.
  • 255 바이트보다 긴 경로 구성 요소를 포함하지 않습니다 (예 : 'a'*256Python). 경로 성분이 전혀 포함하지 않는 경로의 최장 부분 문자열 /문자 (예를 들어, bergtatt, ind, i, 및 fjeldkamrene패스 명에 /bergtatt/ind/i/fjeldkamrene).

구문 정확성. 루트 파일 시스템. 그게 다야.

질문 # 1 : 이제 경로 이름 유효성을 어떻게 수행해야합니까?

Python에서 경로 이름의 유효성을 검사하는 것은 놀랍도록 직관적이지 않습니다. 나는 여기서 Fake Name 과 확고하게 동의 합니다. 공식 os.path패키지는 이에 대한 즉시 사용 가능한 솔루션을 제공해야합니다. 알 수없는 (아마도 설득력이없는) 이유 때문에 그렇지 않습니다. 다행스럽게도 자신의 임시 솔루션을 펼치는 것은 굉장한 일 이 아닙니다 …

네, 사실입니다. 털이 많아요. 끔찍하다. 그것은 아마 졸졸 울리고 빛날 때 낄낄 거린다. 하지만 어떻게 할 건가요? Nuthin ‘.

우리는 곧 저수준 코드의 방사능 심연으로 내려갈 것입니다. 하지만 먼저 높은 수준의 가게에 대해 이야기합시다. 표준 os.stat()os.lstat()함수는 잘못된 경로 이름을 전달할 때 다음 예외를 발생시킵니다.

  • 존재하지 않는 디렉토리에있는 경로 이름의 경우 FileNotFoundError.
  • 기존 디렉토리에있는 경로 이름의 경우 :
    • Windows에서 속성이 (예 🙂 WindowsError인 인스턴스 .winerror123ERROR_INVALID_NAME
    • 다른 모든 OS에서 :
    • null 바이트 (예 :)를 포함하는 경로 이름 '\x00'의 경우 TypeError.
    • 255 바이트보다 긴 경로 구성 요소를 포함하는 경로 이름 OSError의 경우 해당 errcode속성의 인스턴스 는 다음과 같습니다.
      • SunOS 및 * BSD OS 제품군에서 errno.ERANGE. (이는 OS 수준의 버그로 보이며 그렇지 않으면 POSIX 표준의 “선택적 해석”이라고합니다.)
      • 다른 모든 OS에서 errno.ENAMETOOLONG.

결정적으로 이것은 기존 디렉토리에있는 경로 이름 만 유효 함을 의미합니다 . os.stat()os.lstat()기능은 일반적인 인상 FileNotFoundError전달 경로 이름이 존재하지 않는 디렉토리에 거주하는 경우 그 패스가 무효인지 여부에 관계없이, 예외를. 디렉토리 존재는 경로 이름 무효보다 우선합니다.

이것은 존재 하지 않는 디렉토리에있는 경로 이름이 유효 하지 않음을 의미합니까 ? 예 – 기존 디렉토리에 상주하도록 해당 경로 이름을 수정하지 않는 한. 그러나 그것이 안전하게 가능합니까? 경로 이름을 수정하면 원래 경로 이름의 유효성을 검사 할 수 없습니까?

이 질문에 답하려면 ext4파일 시스템의 구문 상 올바른 경로 이름에 널 바이트를 포함하는 경로 구성 요소 (A) 또는 길이가 255 바이트를 초과하는 (B) 가 없다는 점 을 상기 해보십시오 . 따라서 ext4경로 이름은 해당 경로 이름의 모든 경로 구성 요소가 유효한 경우에만 유효합니다. 이것은 대부분의 실제 관심 파일 시스템 에 해당됩니다.

그 현학적 인 통찰력이 실제로 우리에게 도움이됩니까? 예. 한 번에 전체 경로 이름을 확인하는 더 큰 문제를 해당 경로 이름의 모든 경로 구성 요소 만 확인하는 작은 문제로 줄입니다. 임의의 경로 이름은 다음 알고리즘에 따라 크로스 플랫폼 방식으로 유효합니다 (경로 이름이 기존 디렉토리에 있는지 여부에 관계없이).

  1. 해당 경로 이름을 경로 구성 요소로 분할합니다 (예 : 경로 이름 /troldskog/faren/vild을 목록으로 ['', 'troldskog', 'faren', 'vild']).
  2. 이러한 각 구성 요소에 대해 :
    1. 해당 구성 요소와 함께 존재하는 디렉토리의 경로 이름을 새 임시 경로 이름 (예 :)에 결합 /troldskog합니다.
    2. 해당 경로 이름을 os.stat()또는에 전달하십시오 os.lstat(). 해당 경로 이름과 해당 구성 요소가 유효하지 않은 경우이 호출은 일반 FileNotFoundError예외가 아닌 무효 유형을 노출하는 예외를 발생시킵니다 . 왜? 해당 경로 이름이 기존 디렉토리에 있기 때문입니다. (순환 논리는 순환입니다.)

존재가 보장되는 디렉토리가 있습니까? 예, 그러나 일반적으로 단 하나 : 루트 파일 시스템의 최상위 디렉토리 (위에 정의 된대로).

에 (따라서 존재 보장되지과) 다른 디렉토리에있는 경로명 전달 os.stat()또는 os.lstat()디렉토리가 이전에 존재하는 테스트 된 경우에도 초대의 경쟁 조건을. 왜? 테스트가 수행 된 경로 이름이 또는에 전달 되기 전에 외부 프로세스가 해당 디렉토리를 동시에 제거하는 것을 방지 할 수 없기 때문 입니다. 마음을 사로 잡는 광기의 개들을 풀어주세요!os.stat()os.lstat()

위의 접근 방식에는 보안 이라는 실질적인 부수적 이점도 있습니다. (아닌가 하는 좋은?) 구체적으로 :

이러한 경로 이름을 단순히 DoS (서비스 거부) 공격 및 기타 블랙 햇 허니 건에 전달 os.stat()하거나 이에 os.lstat()취약하여 신뢰할 수없는 소스에서 임의의 경로 이름을 검증하는 전면 응용 프로그램 입니다. 악의적 인 사용자는 오래되었거나 느린 것으로 알려진 파일 시스템 (예 : NFS Samba 공유)에있는 경로 이름을 반복적으로 확인하려고 시도 할 수 있습니다. 이 경우 들어오는 경로 이름을 맹목적으로 스테이 팅하면 결국 연결 시간 초과로 실패하거나 실업을 견딜 수있는 미약 한 능력보다 더 많은 시간과 자원을 소비 할 수 있습니다.

위의 접근 방식은 루트 파일 시스템의 루트 디렉토리에 대해 경로 이름의 경로 구성 요소를 확인함으로써이를 방지합니다. (심지어 경우 의가 있다고 는 오래된, 느린, 또는 액세스, 당신은 경로 검증보다 큰 문제를 가지고있다.)

잃어버린? 큰. 의 시작하자. (Python 3이 가정합니다. “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?

끝난. 그 코드에 눈을 찡 그리지 마십시오. ( 물린다. )

질문 # 2 : 경로 이름의 존재 또는 창의성이 유효하지 않을 수 있습니다.

위의 솔루션을 고려할 때 유효하지 않은 경로 이름의 존재 또는 생성 가능성을 테스트하는 것은 대부분 사소한 일입니다. 여기서 작은 열쇠 는 전달 된 경로 테스트 하기 전에 이전에 정의 된 함수를 호출하는 것입니다 .

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

완료완료. 정답이 아닙니다.

질문 # 3 : Windows에서 유효하지 않은 경로 이름 존재 또는 쓰기 가능성

주의 사항이 있습니다. 물론 있습니다.

공식 os.access()문서에 따르면 다음과 같습니다.

참고 : I / O 작업 os.access()은 성공할 것이라고 표시 되더라도 실패 할 수 있습니다. 특히 일반적인 POSIX 권한 비트 모델을 벗어난 권한 의미를 가질 수있는 네트워크 파일 시스템의 작업의 경우 더욱 그렇습니다.

놀랍게도 Windows는 여기에서 일반적인 용의자입니다. NTFS 파일 시스템에서 ACL (액세스 제어 목록)을 광범위하게 사용했기 때문에 단순한 POSIX 권한 비트 모델은 기본 Windows 현실에 제대로 매핑되지 않습니다. 이것은 (논란의 여지가 있지만) Python의 잘못은 아니지만 Windows 호환 응용 프로그램에 대해서는 우려 할 수 있습니다.

이것이 당신이라면 더 강력한 대안이 필요합니다. 전달 된 경로가 존재 하지 않는 경우 대신 해당 경로의 상위 디렉토리에서 즉시 삭제되도록 보장되는 임시 파일을 생성하려고 시도합니다. 이는보다 이식성이 높은 (비용이 많이 드는 경우) 생성 가능성 테스트입니다.

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

그러나 이것 만으로는 충분하지 않을 수 있습니다.

UAC (사용자 액세스 제어) 덕분에 항상 최소화 할 수있는 Windows Vista와 그 이후의 모든 반복 은 시스템 디렉터리와 관련된 권한 대해 노골적으로 거짓말 을합니다. 관리자가 아닌 사용자가 표준 C:\Windows또는 C:\Windows\system32디렉토리에 파일을 만들려고 시도하면 UAC는 사용자가 실제로 그렇게 할 수 있도록 허용하고 실제로 생성 된 모든 파일을 해당 사용자 프로필의 “가상 저장소”로 격리합니다. (기만하는 사용자가 장기적으로 해로운 결과를 초래할 것이라고 누가 상상할 수 있었을까요?)

미친 짓이야. 이것은 Windows입니다.

증명

감히? 위의 테스트를 테스트 할 시간입니다.

NULL은 UNIX 기반 파일 시스템의 경로 이름에서 금지 된 유일한 문자이므로이를 활용하여 차갑고 엄격한 진실을 보여 줍시다. 솔직히 저를 괴롭 히고 분노 케하는 무시할 수없는 Windows 헛소리를 무시합니다.

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

정신 이상. 고통을 넘어. 파이썬 이식성 문제를 발견 할 것입니다.


답변

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

참고 path.exists단지보다 더 많은 이유로 실패 할 수 있습니다 the file is not there당신이 포함 된 디렉토리가 존재 등 경우 테스트와 같은 정밀한 테스트를 수행해야 할 수도 있으므로.


OP와의 토론 후 주요 문제는 파일 이름에 파일 시스템에서 허용하지 않는 문자가 포함될 수 있다는 것이 밝혀졌습니다. 물론 제거해야하지만 OP는 파일 시스템이 허용하는 한 많은 사람이 읽을 수 있도록 유지하려고합니다.

슬프게도 나는 이것에 대한 좋은 해결책을 모릅니다. 그러나 Cecil Curry의 대답 은 문제를 감지하는 데 면밀히 검토됩니다.


답변

Python 3을 사용하면 다음을 수행 할 수 있습니다.

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

‘x’옵션을 사용하면 경쟁 조건에 대해 걱정할 필요가 없습니다. 여기에서 설명서를 참조 하십시오 .

이제 이것은 이름이 유효하지 않은 경우가 아니라면 아직 존재하지 않는 경우 매우 짧은 임시 파일을 생성합니다. 그것으로 살 수 있다면 일이 많이 단순화됩니다.


답변

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

파일이 열리지 않으면 오류가 발생합니다. 오류가있는 경우 경로에 쓰기를 시도 할 수 있습니다. 그렇지 않으면 두 번째 오류가 발생합니다.

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

또한 여기 에서 창에 대한 권한에 대해 살펴보십시오.


답변

시도 os.path.exists하면 경로를 확인하고 True존재하는 False경우 반환 됩니다 .


답변