Python,如何实现类似.gitignore的行为
我想列出当前目录(.)下的所有文件,包括所有子目录里的文件,同时排除一些文件,就像.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 个回答
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
还有另外一种方法,具体可以参考这个链接: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
你走在正确的道路上:如果你想使用 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 上见过至少一个人花了很多精力去处理所有边界情况,所以不妨搜索一下,而不是自己尝试写。