Python,如何实现类似.gitignore的行为

17 投票
3 回答
5784 浏览
提问于 2025-04-18 16:44

我想列出当前目录(.)下的所有文件,包括所有子目录里的文件,同时排除一些文件,就像.gitignore文件的工作方式一样(http://git-scm.com/docs/gitignore)。

通过使用fnmatch(https://docs.python.org/2/library/fnmatch.html),我可以用模式来“过滤”文件。

ignore_files = ['*.jpg', 'foo/', 'bar/hello*']
matches = []
for root, dirnames, filenames in os.walk('.'):
  for filename in fnmatch.filter(filenames, '*'):
      matches.append(os.path.join(root, filename))

我该如何“过滤”,以获取所有不符合我“ignore_files”中一个或多个元素的文件呢?

谢谢!

3 个回答

-1
matches.extend([fn for fn if not filename in ignore_files])

对于简单的文件名,这个方法应该能解决问题。如果是要忽略的模式,可以使用类似下面的:

def reject(filename, filter):
    """ Takes a filename and a filter to reject files that match."""
    if len(filter)==0:
         return False
    else:
         return fnmatch.fnmach(filename, filter[0]) or reject(filename, filter[1:])

matches.extend([os.path.join(root, fn) for fn in filenames if not reject(fn, ignore_files)])

上面的代码在使用os.walk生成文件名列表时,会检查所有的过滤条件,确保没有一个条件匹配到文件名。过滤条件会一直检查,直到没有剩下的条件或者找到第一个匹配的文件名为止,所以这个过程应该会很快。

你也可以尝试类似下面的代码:

filenames = set(filenames)  # convert to a set
for filter in ignore_files:
   filenames = filenames - set(fnmatch.filter(filenames, filter)) # remove the matches
matches.extend([os.path.join(root, fn) for fn in filenames])  # Add to matches
0

还有另外一种方法,具体可以参考这个链接:https://gitpython.readthedocs.io/en/stable/

import os

from git import Repo


def list_files_in_directory(directory_path):
    if os.path.exists(os.path.join(directory_path, ".git")):
        repo = Repo(directory_path)
        return repo.git.ls_files().splitlines()
    else:
        file_list = []
        for root, dirs, files in os.walk(directory_path):
            for file in files:
                file_list.append(
                    os.path.relpath(os.path.join(root, file), directory_path)
                )
        return file_list
18

你走在正确的道路上:如果你想使用 fnmatch 风格的模式,应该用 fnmatch.filter 来处理它们。

不过,这里有三个问题,让事情变得不那么简单。

首先,你想应用多个过滤器。那该怎么做呢?可以多次调用 filter

for ignore in ignore_files:
    filenames = fnmatch.filter(filenames, ignore)

第二,你其实想做的是 反向filter:返回那些 匹配的名字。文档中是这样解释的:

这和 [n for n in names if fnmatch(n, pattern)] 是一样的,但实现得更高效。

所以,要做相反的事情,只需要加上一个 not

for ignore in ignore_files:
    filenames = [n for n in filenames if not fnmatch(n, ignore)]

最后,你想要过滤的是部分路径名,而不仅仅是文件名,但你在过滤后才进行 join。所以要调换一下顺序:

filenames = [os.path.join(root, filename) for filename in filenames]
for ignore in ignore_files:
    filenames = [n for n in filenames if not fnmatch(n, ignore)]
matches.extend(filenames)

这里有几种方法可以改进这个过程。

你可以考虑使用生成器表达式,而不是列表推导式(用圆括号代替方括号),这样如果你有很大的文件名列表,就可以使用懒加载的方式,避免重复构建庞大的列表,节省时间和空间。

此外,如果你反转循环的顺序,可能会更容易理解,像这样:

filenames = (n for n in filenames 
             if not any(fnmatch(n, ignore) for ignore in ignore_files))

最后,如果你担心性能问题,可以对每个表达式使用 fnmatch.translate,把它们转成等效的正则表达式,然后合并成一个大的正则表达式并编译,接着用这个替代围绕 fnmatch 的循环。如果你的模式比简单的 *.jpg 更复杂,这可能会变得棘手,我不建议这样做,除非你真的发现了性能瓶颈。不过如果你需要这样做,我在 StackOverflow 上见过至少一个人花了很多精力去处理所有边界情况,所以不妨搜索一下,而不是自己尝试写。

撰写回答