在OSX的HFS+下,如何用Python获取现有文件名的正确大小写?

9 投票
6 回答
1081 浏览
提问于 2025-04-17 13:37

我在存储一些关于OSX HFS+文件系统上文件的数据。之后我想要遍历这些存储的数据,看看每个文件是否仍然存在。对我来说,文件名的大小写很重要,所以如果文件名的大小写发生了变化,我就会认为这个文件不再存在。

我开始尝试

os.path.isfile(filename)

但是在正常安装的OSX HFS+上,即使文件名的大小写不匹配,这个方法也会返回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 个回答

4

我有一个疯狂的想法。需要说明的是:我对文件系统的了解远远不够,无法考虑到所有特殊情况,所以请把这仅仅当作一次偶然成功的事情来看待。

>>> !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'
5

接着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')
6

这个回答补充了现有的内容,提供了一些函数,这些函数是根据Alex L的回答改编的,具有以下特点:

  • 可以处理非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:字面量前加u;例如,u'Motörhead';字符串变量:使用strVar.decode('utf8')进行转换
    • 返回的字符串是NFC(组合规范形式)的Unicode字符串。即使在OSX上,文件系统(HFS+)存储的名称是NFD(分解规范形式),也会返回NFC。
      返回NFC是因为它比NFD更常见,而Python不将等价的NFC和NFD字符串视为(概念上)相同。有关背景信息,请参见下文。
    • 返回的路径保留了输入路径的结构(相对路径与绝对路径,像...这样的组件),只是多个路径分隔符会被合并,并且在Windows上,返回的路径始终使用\作为路径分隔符。
    • 在Windows上,如果存在驱动器/ UNC共享组件,则保持原样。
    • 如果路径不存在,或者您没有权限访问它,将抛出OSError异常。
    • 如果在区分大小写的文件系统上使用此函数,例如在使用ext4的Linux上,它实际上会退化为指示输入路径是否存在于指定的确切大小写中。
  • istruecasepath()使用gettruecasepath()来比较输入路径与文件系统中存储的路径。

注意:由于这些函数需要检查输入路径每个级别的所有目录条目(如上所述),因此它们会很 - 速度不可预测,因为性能与所检查目录中包含的项目数量有关。有关背景信息,请继续阅读。


背景

本地API支持(缺乏)

奇怪的是,OSX和Windows都没有提供一个本地API方法来直接解决这个问题。

虽然在Windows上可以巧妙地结合两个API方法来解决问题,但在OSX上,我所知道的没有替代方案,除了在每个检查路径级别上不可预测的慢速枚举目录内容。

Unicode规范形式:NFC与NFD

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

例如,您在源代码中指定的包含非ASCII字符ü的路径将表示为单个Unicode代码点U+00FC;这是NFC的一个例子:'C'代表组合,因为字母基础字母u和它的变音符号¨(一个组合的变音符号)形成一个单一字母。

相反,如果您将ü用作HFS+文件名的一部分,它将转换为NFD形式,这会导致2个Unicode代码点:基础字母uU+0075),后面跟着组合变音符号(̈U+0308)作为单独的代码点;'D'代表分解,因为字符被分解为基础字母和其相关的变音符号。

尽管Unicode标准认为这两种表示形式(在规范上)是等价的,但大多数编程语言,包括Python,并不承认这种等价性。
在Python中,您必须使用unicodedata.normalize()将两个字符串转换为相同的形式,然后才能进行比较。

(附注:Unicode规范形式与Unicode编码是不同的,尽管不同数量的Unicode代码点通常也会影响编码每种形式所需的字节数量。在上面的例子中,单代码点ü(NFC)在UTF-8中需要2字节进行编码(U+00FC -> 0xC3 0xBC),而两个代码点ü(NFD)需要3字节U+0075 -> 0x75,和U+0308 -> 0xCC 0x88)。

撰写回答