В Python, как я могу получить путь к файлу в правильном регистре?

Windows использует имена файлов без учета регистра, поэтому я могу открыть один и тот же файл любым из них:

r"c:\windows\system32\desktop.ini"
r"C:\WINdows\System32\DESKTOP.ini"
r"C:\WiNdOwS\SyStEm32\DeSkToP.iNi"

и т. д. Учитывая любой из этих путей, как я могу найти истинный случай? Я хочу, чтобы они все производили:

r"C:\Windows\System32\desktop.ini"

os.path.normcase этого не делает, он просто пишет все строчными буквами. os.path.abspath возвращает абсолютный путь, но каждый из них уже является абсолютным, поэтому он не меняет ни один из них. os.path.realpath используется только для разрешения символических ссылок, которых нет в Windows, поэтому это то же самое, что и abspath в Windows.

Есть ли простой способ сделать это?


person Ned Batchelder    schedule 11.09.2010    source источник
comment
Похоже, это дубликат stackoverflow.com/questions/2113822/, где есть ответ.   -  person Ned Batchelder    schedule 12.09.2010
comment
Windows использует имена файлов без учета регистра — нет, это не так. Windows сохраняет этот регистр имен файлов. То есть: Сами имена файлов определяются с регистром, и этот регистр сохраняется, если, например, вы копируете файл. Просто некоторые операции в Windows игнорируют регистр.   -  person gwideman    schedule 11.05.2020


Ответы (12)


Вот простое решение только для stdlib:

import glob
def get_actual_filename(name):
    name = "%s[%s]" % (name[:-1], name[-1])
    return glob.glob(name)[0]
person Ethan Furman    schedule 20.08.2011
comment
Мне это нравится: это заставляет glob сделать за меня os.walk! - person Ned Batchelder; 21.08.2011
comment
Он исправляет только имя файла, а не предыдущие подкаталоги. Я добавляю еще один ответ на основе этого stackoverflow.com/a/14742779/1355726 - person xvorsx; 07.02.2013
comment
Это не работает. Кроме того, это приведет к сбою для имен файлов с символами в них, которые являются токенами glob. Если бы он запускал сканирование каталогов, оно, вероятно, тоже было бы патологически медленным... - person Glenn Maynard; 11.05.2016

Ответ Неда GetLongPathName не совсем работает (по крайней мере, не для меня). Вам нужно вызвать GetLongPathName для возвращаемого значения GetShortPathname. Использование pywin32 для краткости (решение ctypes будет похоже на решение Неда):

>>> win32api.GetLongPathName(win32api.GetShortPathName('stopservices.vbs'))
'StopServices.vbs'
person Paul Moore    schedule 24.09.2010
comment
См. мой комментарий к stackoverflow.com/a/2114975/179715; это не гарантирует работу, если генерация коротких имен файлов отключена. - person jamesdlin; 16.01.2016
comment
Если вам важен полный путь, обратите внимание, что это не приведет к преобразованию буквы диска в более типичный верхний регистр. - person Eric Smith; 09.03.2016
comment
Примечание. Для этого решения, зависящего от файловой системы, проверьте, отключено ли создание коротких имен файлов на томах Windows (fsutil.exe 8dot3name query C:) — что [рекомендуется] [1] по крайней мере для файловых систем, критически важных для производительности, когда 16-битные приложения больше не полагаются на это: (fsutil.exe behavior set disable8dot3 1) [1]: technet.microsoft.com/ en-us/library/ff633453%28v=ws.10%29.aspx - person kxr; 25.08.2016

Этот объединяет, сокращает и исправляет несколько подходов: только стандартная библиотека; преобразует все части пути (кроме буквы диска); относительные или абсолютные пути; диск буквенный или нет; толерантный:

def casedpath(path):
    r = glob.glob(re.sub(r'([^:/\\])(?=[/\\]|$)', r'[\1]', path))
    return r and r[0] or path

А этот дополнительно обрабатывает пути UNC:

def casedpath_unc(path):
    unc, p = os.path.splitunc(path)
    r = glob.glob(unc + re.sub(r'([^:/\\])(?=[/\\]|$)', r'[\1]', p))
    return r and r[0] or path
person kxr    schedule 05.02.2016
comment
Это единственный в этой теме, который работал для меня. Спасибо! - person MaVCArt; 17.08.2016

Итан отвечает исправить только имя файла, а не имена подпапок в пути. Вот мое предположение:

def get_actual_filename(name):
    dirs = name.split('\\')
    # disk letter
    test_name = [dirs[0].upper()]
    for d in dirs[1:]:
        test_name += ["%s[%s]" % (d[:-1], d[-1])]
    res = glob.glob('\\'.join(test_name))
    if not res:
        #File not found
        return None
    return res[0]
person xvorsx    schedule 07.02.2013

В этой ветке python-win32 есть ответ, который не не требуют сторонних пакетов или обхода дерева:

import ctypes

def getLongPathName(path):
    buf = ctypes.create_unicode_buffer(260)
    GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
    rv = GetLongPathName(path, buf, 260)
    if rv == 0 or rv > 260:
        return path
    else:
        return buf.value
person Ned Batchelder    schedule 12.09.2010
comment
Это может привести к сбою, потому что path должен быть Unicode для GetLongPathNameW. Попробуйте заменить path в вызове GetLongPathName(path, buf, 260) на unicode(path). - person Attila; 06.03.2013
comment
Это не работает. GetLongPathName расширяет только короткие имена файлов, поэтому, если вы дадите ему C:\Progra~1, вы получите C:\Program Files, но если вы дадите ему C:\PROGRAM FILES, это уже будет длинный путь, поэтому он не изменит его. . - person Glenn Maynard; 11.05.2016

В Python 3 вы можете использовать pathlib resolve():

>>> from pathlib import Path

>>> str(Path(r"C:\WiNdOwS\SyStEm32\DeSkToP.iNi").resolve())
r'C:\Windows\System32\desktop.ini'
person TheAl_T    schedule 04.03.2019
comment
Примечание: иногда вы можете получить совершенно другой путь в этом случае, поскольку он разрешает ссылки (например, если у вас есть диск subst, это может привести к разрешению его туда, куда указывает subst). - person Fabio Zadrozny; 11.08.2020

Поскольку определение «истинного случая» в файловых системах NTFS (или VFAT) действительно странно, кажется, что лучший способ — пройти по пути и сопоставить с os.listdir().

Да, это кажется надуманным решением, но пути NTFS тоже. У меня нет машины с DOS, чтобы проверить это.

person msw    schedule 11.09.2010
comment
Это не простое решение, которого я боялся... :( - person Ned Batchelder; 11.09.2010

Я предпочитаю подход Итана и xvorsx. AFAIK, следующее также не повредит на других платформах:

import os.path
from glob import glob

def get_actual_filename(name):
    sep = os.path.sep
    parts = os.path.normpath(name).split(sep)
    dirs = parts[0:-1]
    filename = parts[-1]
    if dirs[0] == os.path.splitdrive(name)[0]:
        test_name = [dirs[0].upper()]
    else:
        test_name = [sep + dirs[0]]
    for d in dirs[1:]:
        test_name += ["%s[%s]" % (d[:-1], d[-1])]
    path = glob(sep.join(test_name))[0]
    res = glob(sep.join((path, filename)))
    if not res:
        #File not found
        return None
    return res[0]
person Dobedani    schedule 21.05.2015

Основано на паре приведенных выше примеров listdir/walk, но поддерживает пути UNC.

def get_actual_filename(path):
    orig_path = path
    path = os.path.normpath(path)

    # Build root to start searching from.  Different for unc paths.
    if path.startswith(r'\\'):
        path = path.lstrip(r'\\')
        path_split = path.split('\\')
        # listdir doesn't work on just the machine name
        if len(path_split) < 3:
            return orig_path
        test_path = r'\\{}\{}'.format(path_split[0], path_split[1])
        start = 2
    else:
        path_split = path.split('\\')
        test_path = path_split[0] + '\\'
        start = 1

    for i in range(start, len(path_split)):
        part = path_split[i]
        if os.path.isdir(test_path):
            for name in os.listdir(test_path):
                if name.lower() == part.lower():
                    part = name
                    break
            test_path = os.path.join(test_path, part)
        else:
            return orig_path
    return test_path
person Brendan Abel    schedule 23.07.2015

Я бы использовал os.walk, но я думаю, что для diskw со многими каталогами это может занять много времени:

fname = "g:\\miCHal\\ZzZ.tXt"
if not os.path.exists(fname):
    print('No such file')
else:
    d, f = os.path.split(fname)
    dl = d.lower()
    fl = f.lower()
    for root, dirs, files in os.walk('g:\\'):
        if root.lower() == dl:
            fn = [n for n in files if n.lower() == fl][0]
            print(os.path.join(root, fn))
            break
person Michał Niklas    schedule 11.09.2010

Я просто боролся с той же проблемой. Я не уверен, но я думаю, что предыдущие ответы не охватывают все случаи. Моя реальная проблема заключалась в том, что корпус буквы диска отличался от того, который видел система. Вот мое решение, которое также проверяет правильность регистра букв диска (используя win32api):

  def get_case_sensitive_path(path):
      """
      Get case sensitive path based on not - case sensitive path.
      
      Returns:
         The real absolute path.
         
      Exceptions:
         ValueError if the path doesn't exist.
      
      Important note on Windows: when starting command line using
      letter cases different from the actual casing of the files / directories,
      the interpreter will use the invalid cases in path (e. g. os.getcwd()
      returns path that has cases different from actuals).
      When using tools that are case - sensitive, this will cause a problem.
      Below code is used to get path with exact the same casing as the
      actual. 
      See http://stackoverflow.com/questions/2113822/python-getting-filename-case-as-stored-in-windows
      """
      drive, path = os.path.splitdrive(os.path.abspath(path))
      path = path.lstrip(os.sep)
      path = path.rstrip(os.sep)
      folders = []
      
      # Make sure the drive number is also in the correct casing.
      drives = win32api.GetLogicalDriveStrings()
      drives = drives.split("\000")[:-1]
      # Get the list of the form C:, d:, E: etc.
      drives = [d.replace("\\", "") for d in drives]
      # Now get a lower case version for comparison.
      drives_l = [d.lower() for d in drives]
      # Find the index of matching item.
      idx = drives_l.index(drive.lower())
      # Get the drive letter with the correct casing.
      drive = drives[idx]

      # Divide path into components.
      while 1:
          path, folder = os.path.split(path)
          if folder != "":
              folders.append(folder)
          else:
              if path != "":
                  folders.append(path)
              break

      # Restore their original order.
      folders.reverse()

      if len(folders) > 0:
          retval = drive + os.sep

          for folder in folders:
              found = False
              for item in os.listdir(retval):
                  if item.lower() == folder.lower():
                      found = True
                      retval = os.path.join(retval, item)
                      break
              if not found:
                  raise ValueError("Path not found: '{0}'".format(retval))

      else:
          retval = drive + os.sep

      return retval
person lutecki    schedule 06.10.2015

Я искал еще более простую версию, чем «трюк с глобами», поэтому я сделал это, в котором используется только os.listdir().

def casedPath(path):
    path = os.path.normpath(path).lower()
    parts = path.split(os.sep)
    result = parts[0].upper()
    # check that root actually exists
    if not os.path.exists(result):
        return
    for part in parts[1:]:
        actual = next((item for item in os.listdir(result) if item.lower() == part), None)
        if actual is None:
            # path doesn't exist
            return
        result += os.sep + actual
    return result

редактировать: кстати, он отлично работает. Не уверен, что ожидается возврат None, когда путь не существует, но мне нужно было такое поведение. Вместо этого, я думаю, это может вызвать ошибку.

person Paul    schedule 12.03.2020
comment
Это ответ или комментарий? - person Death Waltz; 12.03.2020
comment
ммм своего рода ответ, я думаю :) Я пришел сюда, чтобы найти более простое решение, чем мое, но на самом деле мое оказалось в порядке :D. Интересно, будет ли os.scandir более эффективным, но я думаю, это зависит от того, как быстро будет найдено подходящее имя. - person Paul; 13.03.2020