在使用HFS +的OSX上的python中,如何获得现有文件名的正确大小写?

时间:2013-01-25 03:55:01

标签: python macos filesystems hfs+

我正在存储有关OSX HFS +文件系统上存在的文件的数据。我后来想迭代存储的数据并确定每个文件是否仍然存在。出于我的目的,我关心文件名区分大小写,所以如果文件名的大小写已经改变,我会认为该文件不再存在。

我开始尝试

os.path.isfile(filename)

但是在HFS +上正常安装OSX时,即使文件名大小写不匹配,也会返回True。我正在寻找一种方法来编写一个isfile()函数,即使在文件系统不关心的情况下也会关注它。

os.path.normcase()和os.path.realpath()都会返回文件名,无论我传递给它们。

编辑:

我现在有两个功能似乎适用于限制为ASCII的文件名。我不知道unicode或其他角色会如何影响这个。

第一个是基于omz和Alex L给出的答案。

def does_file_exist_case_sensitive1a(fname):
    if not os.path.isfile(fname): return False
    path, filename = os.path.split(fname)
    search_path = '.' if path == '' else path
    for name in os.listdir(search_path):
        if name == filename : return True
    return False

第二种可能效率更低。

def does_file_exist_case_sensitive2(fname):
    if not os.path.isfile(fname): return False
    m = re.search('[a-zA-Z][^a-zA-Z]*\Z', fname)
    if m:
        test = string.replace(fname, fname[m.start()], '?', 1)
        print test
        actual = glob.glob(test)
        return len(actual) == 1 and actual[0] == fname
    else:
        return True  # no letters in file, case sensitivity doesn't matter

以下是DSM的第三个答案。

def does_file_exist_case_sensitive3(fname):
    if not os.path.isfile(fname): return False
    path, filename = os.path.split(fname)
    search_path = '.' if path == '' else path
    inodes = {os.stat(x).st_ino: x for x in os.listdir(search_path)}
    return inodes[os.stat(fname).st_ino] == filename

如果我在一个目录中有数千个文件,我不希望这些表现良好。我仍然希望能有更高效的东西。

我在测试这些内容时注意到的另一个缺点是他们只检查案例匹配的文件名。如果我传递一个包含目录名的路径,到目前为止这些函数都没有检查目录名的大小写。

6 个答案:

答案 0 :(得分:3)

这是一个疯狂的想法。免责声明:我不太了解文件系统是否足以考虑边缘情况,因此将其视为恰好可行的东西。一旦。

>>> !ls
A.txt   b.txt
>>> inodes = {os.stat(x).st_ino: x for x in os.listdir(".")}
>>> inodes
{80827580: 'A.txt', 80827581: 'b.txt'}
>>> inodes[os.stat("A.txt").st_ino]
'A.txt'
>>> inodes[os.stat("a.txt").st_ino]
'A.txt'
>>> inodes[os.stat("B.txt").st_ino]
'b.txt'
>>> inodes[os.stat("b.txt").st_ino]
'b.txt'

答案 1 :(得分:2)

您可以使用os.listdir之类的内容,并检查列表中是否包含您要查找的文件名。

答案 2 :(得分:2)

从omz的帖子开始 - 这样的事情可能有用:

import os

def getcase(filepath):
    path, filename = os.path.split(filepath)
    for fname in os.listdir(path):
        if filename.lower() == fname.lower():
            return os.path.join(path, fname)

print getcase('/usr/myfile.txt')

答案 3 :(得分:2)

此答案通过提供改编自Alex L's answer函数来补充现有答案:

  • 也适用于非ASCII字符
  • 处理 所有路径组件(不仅仅是最后一个)
  • 使用Python 2.x和3.x
  • 作为奖励,也适用于Windows(有更好的 Windows特定的解决方案 - 请参阅https://stackoverflow.com/a/2114975/45375 - 但这里的功能是跨平台的,不需要额外的包)
import os, unicodedata

def gettruecasepath(path): # IMPORTANT: <path> must be a Unicode string
  if not os.path.lexists(path): # use lexists to also find broken symlinks
    raise OSError(2, u'No such file or directory', path)
  isosx = sys.platform == u'darwin'
  if isosx: # convert to NFD for comparison with os.listdir() results
    path = unicodedata.normalize('NFD', path)
  parentpath, leaf = os.path.split(path)
  # find true case of leaf component
  if leaf not in [ u'.', u'..' ]: # skip . and .. components
    leaf_lower = leaf.lower() # if you use Py3.3+: change .lower() to .casefold()
    found = False
    for leaf in os.listdir(u'.' if parentpath == u'' else parentpath):
      if leaf_lower == leaf.lower(): # see .casefold() comment above
          found = True
          if isosx:
            leaf = unicodedata.normalize('NFC', leaf) # convert to NFC for return value
          break
    if not found:
      # should only happen if the path was just deleted
      raise OSError(2, u'Unexpectedly not found in ' + parentpath, leaf_lower)
  # recurse on parent path
  if parentpath not in [ u'', u'.', u'..', u'/', u'\\' ] and \
                not (sys.platform == u'win32' and 
                     os.path.splitdrive(parentpath)[1] in [ u'\\', u'/' ]):
      parentpath = gettruecasepath(parentpath) # recurse
  return os.path.join(parentpath, leaf)


def istruecasepath(path): # IMPORTANT: <path> must be a Unicode string
  return gettruecasepath(path) == unicodedata.normalize('NFC', path)
  • gettruecasepath()获取存储在指定路径(绝对路径或相对路径)的文件系统中的大小写精确表示形式(如果存在):

    • 输入路径必须 Unicode 字符串:
      • Python 3.x:字符串本身就是Unicode - 无需额外操作。
      • Python 2.x:literals:前缀u;例如,u'Motörhead'; str变量:转换为例如strVar.decode('utf8')
    • 返回的字符串是NFC中的Unicode字符串(撰写普通形式)。即使在OSX上也会返回NFC,其中文件系统(HFS +)以NFD(分解正常形式)存储名称。
      返回NFC,因为它比NFD和Python更常见 没有认识到等效的NFC和NFD字符串(概念上)相同。请参阅下面的背景信息。
    • 返回的路径保留输入路径的结构(相对于绝对的组件,例如...),除了多个路径分隔符已折叠,并且在Windows上,返回path始终使用\作为路径分隔符。
    • 在Windows上,驱动器/ UNC共享组件(如果存在)保持原样。
    • 如果路径不存在,或者您无权访问该路径,则会引发OSError异常。
    • 如果您在case- 敏感文件系统上使用此功能,例如,在带有ext4的Linux上,它会有效地降级以指示输入路径是否存在于指定的确切情况中。
  • istruecasepath()使用gettruecasepath()来比较存储在文件系统中的路径的输入路径。

警告:由于这些函数需要检查输入路径的每个级别(如指定)的所有目录条目,因此它们将 - 不可预测的因此,作为性能将对应于检查的目录包含的项目数。继续阅读以获取背景信息。

背景

原生API支持(缺乏支持)

很奇怪OSX和Windows都没有提供直接解决此问题的本机API方法。

虽然on Windows you can cleverly combine two API methods to solve the problem,但在OSX上,我无法理解 - 在上面使用的路径的每个级别上目录内容的缓慢枚举 - 不可预测 - 慢速枚举。

Unicode正常形式:NFC与NFD

HFS +(OSX&#39;文件系统)以分解的 Unicode格式(NFD)存储文件名,这会在大多数编程语言中将这些名称与内存中的Unicode字符串进行比较时导致问题,通常采用编组 Unicode格式(NFC)。

例如,您在源代码中指定为文字的非ASCII字符ü的路径将表示为单个 Unicode代码点,U+00FC;这是 NFC 的一个例子:&#39; C&#39;代表撰写,因为字母基础字母u及其变音符¨(组合分音符)形成字母。

相比之下,如果您使用ü作为HFS + 文件名的一部分,则会将其翻译为 NFD 形式,从而导致 2 Unicode代码点:基本字母uU+0075),后跟合并分类广告(̈U+0308)作为单独的 codepoint; &#39; D&#39;代表分解,因为该字符被分解为基本字母及其相关的变音符号。

尽管Unicode标准认为这两个表示(规范)等效,但大多数编程语言(包括Python)都 不能识别这种等价。
对于Python,您必须使用unicodedata.normalize()在比较之前将两个字符串转换为相同的表单。

(旁注:Unicode 普通表格与Unicode 编码分开,但不同数量的Unicode代码点通常也会影响字节的数量ü(NFC)需要 2个字节以UTF-8(U+00FC - > 0xC3 0xBC),而双码点ü(NFD)需要 3个字节U+0075 - &gt; 0x75和{{1} } - &gt; U+0308))。

答案 4 :(得分:1)

这个答案只是一个概念证明,因为它不会尝试转义特殊字符,处理非ASCII字符或处理文件系统编码问题。

从好的方面来说,答案不涉及在Python中循环遍历文件,而是正确处理检查导致最终路径段的目录名称。

此建议基于以下观察(至少在使用bash时),当且仅当/my/path存在具有该确切框的情况时,以下命令才会发现路径/my/path没有错误。

$ ls /[m]y/[p]ath

(如果任何路径部分中没有括号,那么该部分对套管中的更改不敏感。)

以下是基于此想法的示例函数:

import os.path
import subprocess

def does_exist(path):
    """Return whether the given path exists with the given casing.

    The given path should begin with a slash and not end with a trailing
    slash.  This function does not attempt to escape special characters
    and does not attempt to handle non-ASCII characters, file system
    encodings, etc.
    """
    parts = []
    while True:
        head, tail = os.path.split(path)
        if tail:
            parts.append(tail)
            path = head
        else:
            assert head == '/'
            break
    parts.reverse()
    # For example, for path "/my/path", pattern is "/[m]y/[p]ath".
    pattern = "/" + "/".join(["[%s]%s" % (p[0], p[1:]) for p in parts])
    cmd = "ls %s" % pattern
    return_code = subprocess.call(cmd, shell=True)
    return not return_code

答案 5 :(得分:-2)

您也可以尝试打开该文件。

    try:open('test', 'r')
    except IOError: print 'File does not exist'