在OSX的HFS+下,如何用Python获取现有文件名的正确大小写?
我在存储一些关于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 个回答
我有一个疯狂的想法。需要说明的是:我对文件系统的了解远远不够,无法考虑到所有特殊情况,所以请把这仅仅当作一次偶然成功的事情来看待。
>>> !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'
接着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')
这个回答补充了现有的内容,提供了一些函数,这些函数是根据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上,它实际上会退化为指示输入路径是否存在于指定的确切大小写中。
- 输入路径必须是Unicode字符串:
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代码点:基础字母u
(U+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
)。