【问题标题】:Python, how to implement something like .gitignore behaviorPython,如何实现类似 .gitignore 的行为
【发布时间】:2014-10-03 11:22:07
【问题描述】:

我需要列出当前目录 (.) 中的所有文件(包括所有子目录),并排除一些文件作为 .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”中的一个或多个元素不匹配的所有文件?

谢谢!

【问题讨论】:

  • 我不需要所有的规范,只需要从文件列表中排除一些使用模式的文件。
  • 你标记这个正则表达式有什么原因吗?这些显然是代码中的全局样式模式,而不是正则表达式。虽然您可以将它们转换为正则表达式(使用fnmatch.translate),但您有理由相信您可能需要这样做吗?如果是这样,这个原因应该是你的问题。

标签: python pattern-matching gitignore glob fnmatch


【解决方案1】:

您走在正确的轨道上:如果您想使用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 将它们转换为等效的正则表达式,然后将它们合并成一个大的正则表达式并编译它,并使用它而不是围绕@987654337 循环@。如果允许您的模式比*.jpg 更复杂,这可能会变得很棘手,除非您确实在这里确定了性能瓶颈,否则我不会推荐它。但是,如果您需要这样做,我已经看到至少一个关于 SO 的问题,其中有人付出了很多努力来敲定所有边缘情况,因此请搜索而不是尝试自己编写。

【讨论】:

  • 非常感谢,这是我现在的代码(端到端):ignore_files = ['foo', '/foo'] 匹配 = [] 用于根目录、目录名、 os.walk('.') 中的文件名:fnmatch.filter(filenames, '') 中的文件名:filename = os.path.join(root, filename)[2:] matches.append(filename) for ignore_files: matches_ = [n for n in matches if not fnmatch.filter([n], ignore)] 可以更好吗?谢谢
  • @fj123x:你真的不能在 cmets 中发布代码,因为它们会吃掉所有的格式。发布一个新问题,将其编辑到您现有的问题中,或者将其粘贴到 pastebin.com 之类的地方并在此处发布链接。
  • @fj123x:但是一个简短的评论:没有理由永远fnmatch.filter(filenames, '*')。所有文件名都与* 匹配,因此只返回filenames 的副本。
  • 这并不能像**/a/ba/b/**a/**/b 那样真正处理.gitignore rules,它似乎也不能处理简单的foo。例如 .gitignore 中的 foo 将匹配 fooa/foo 但 fnmatch 将在 a/foo 上失败
  • Yea, doesn't seem to work at all 虽然也许我错过了什么。
【解决方案2】:
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

【讨论】:

  • 仅当ignore_files 是简单文件名列表时,.gitignorefnmatch 才允许全局模式,它非常有用。 OP 的示例中甚至还有一个 (*.jpg)。
  • 误导了命名 - ignore_files 而不是 ignore_patterns
  • 请注意,某些模式匹配路径,而不仅仅是文件名。
  • 你能解释一下matches.extend([fn for fn if not reject(filename, ignore_files)])的用法吗?您没有在句子中使用“in”,文件名是什么?谢谢
  • @fj123x:在for fnif not 之间缺少in filenames。您需要添加它才能完成这项工作。但正如 delnan 指出的那样,无论如何这对你不起作用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-16
  • 2021-08-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多