如何使用递归查找文件?

1010
我想要递归列出一个目录中的所有文件。我当前有这样的目录结构:
  • src/main.c
  • src/dir/file1.c
  • src/another-dir/file2.c
  • src/another-dir/nested/files/file3.c

我尝试做了以下事情:

from glob import glob

glob(os.path.join('src','*.c'))

但这只会直接获取 src 子文件夹中的文件,例如我会得到 main.c 但不会得到 file1.cfile2.c 等。

from glob import glob

glob(os.path.join('src','*.c'))
glob(os.path.join('src','*','*.c'))
glob(os.path.join('src','*','*','*.c'))
glob(os.path.join('src','*','*','*','*.c'))

但这显然是有限和笨重的,我该如何正确地做到这一点?


在这种情况下 glob('src/**/*.c') 不能工作吗? - Likith Reddy
28个回答

1778

有几种方法:

pathlib.Path().rglob()

使用Python 3.5中引入的pathlib模块中的Path().rglob()函数。
from pathlib import Path

for path in Path('src').rglob('*.c'):
    print(path.name)

glob.glob()

如果您不想使用pathlib,请使用glob.glob()

from glob import glob

for filename in glob('src/**/*.c', recursive=True):
    print(filename)   

对于需要匹配以点(.)开头的文件,例如当前目录中的文件或Unix系统上的隐藏文件,请使用下面的os.walk()解决方案。

os.walk()

对于旧版本的Python,请使用os.walk()递归遍历目录,并使用fnmatch.filter()匹配简单表达式:

import fnmatch
import os

matches = []
for root, dirnames, filenames in os.walk('src'):
    for filename in fnmatch.filter(filenames, '*.c'):
        matches.append(os.path.join(root, filename))

这个版本应该会更快,具体取决于你有多少文件,因为pathlib模块比os.walk()多一些开销。


4
Python版本低于2.2的话,可以使用os.path.walk()函数,但它比os.walk()函数稍微有些麻烦。 - John La Rooy
37
@gnibbler 我知道这是一个旧评论,但我的评论只是为了让人们知道os.path.walk()已经被弃用并在Python 3中被删除。 - Pedro Cunha
6
可能在这个问题所提及的特定情况下这样做可以奏效,但很容易想象有人想要对'a*.c'等查询使用它,所以我认为保留当前有点慢的答案是值得的。 - Johan Dahlin
4
就我的情况而言,使用glob查找1万多个文件比使用os.walk要慢得多,因此出于这个原因,我选择了后者的解决方案。 - Godsmith
4
对于Python 3.4版本,pathlib.Path('src').glob('**/*.c')应该可行。 - CivFan
显示剩余10条评论

206
对于 Python 版本大于等于 3.5,你可以使用 `**` 和 `recursive=True`,例如:
import glob
for f in glob.glob('/path/**/*.c', recursive=True):
    print(f)

如果递归为True(默认为False),模式**将匹配任何文件和零个或多个目录和子目录。如果模式后跟os.sep,则只匹配目录和子目录。

Python 3 演示


4
这种方法比使用pathlib.Path('./path/').glob('**/*') 更好,因为它可以在大小为0的文件夹中运行。 - Charles Walker
4
在Python 3.9.1中,默认情况下递归被设置为False。 - PYB
1
在Python 3.8.*中,默认情况下recursive也被设置为False - rayryeng

121

与其他解决方案类似,但使用fnmatch.fnmatch而不是glob,因为os.walk已经列出了文件名:

import os, fnmatch


def find_files(directory, pattern):
    for root, dirs, files in os.walk(directory):
        for basename in files:
            if fnmatch.fnmatch(basename, pattern):
                filename = os.path.join(root, basename)
                yield filename


for filename in find_files('src', '*.c'):
    print 'Found C source:', filename

此外,使用生成器可以让你在找到每个文件时立即处理它,而不是先找到所有的文件然后再处理它们。


92

我修改了glob模块,使其支持**来进行递归式的globbing,例如:

>>> import glob2
>>> all_header_files = glob2.glob('src/**/*.c')

https://github.com/miracle2k/python-glob2/

如果你想让用户使用 ** 通配符,而仅仅使用 os.walk() 并不太够用时,python-glob2是一个有用的工具。


2
我们是否可以在找到第一个匹配项后停止搜索?也许可以将其作为生成器使用,而不是返回每个可能结果的列表?另外,这是深度优先搜索还是广度优先搜索?我认为更喜欢广度优先搜索,因为可以先找到靠近根的文件。非常感谢你制作了这个模块并提供在 GitHub/pip 上。+1 - ArtOfWarfare
23
在Python 3.5中,官方的glob模块新增了**语法。 - ArtOfWarfare
1
@ArtOfWarfare 好吧,没问题。这对于小于3.5的版本仍然有用。 - cs95
6
要在官方的glob模块中使用**来启用递归式全局匹配,请执行以下操作:glob(path, recursive=True) - winklerrr

78

从Python 3.4开始,可以使用新的pathlib模块中Path类的 glob() 方法,支持**通配符。例如:

from pathlib import Path

for file_path in Path('src').glob('**/*.c'):
    print(file_path) # do whatever you need with these files

更新:从Python 3.5开始,glob.glob()也支持相同的语法。


3
确实如此,它将在Python 3.5中实现。原计划在Python 3.4中实现,但由于疏忽而被遗漏。 - taleinat
此语法现在受到glob.glob()的支持,自Python 3.5起。 - taleinat
请注意,您还可以使用pathlib.PurePath.relative_to来获取相对路径。有关更多上下文,请参见我的答案 - pjgranahan

42
import os
import fnmatch


def recursive_glob(treeroot, pattern):
    results = []
    for base, dirs, files in os.walk(treeroot):
        goodfiles = fnmatch.filter(files, pattern)
        results.extend(os.path.join(base, f) for f in goodfiles)
    return results

fnmatch 提供了与 glob 完全相同的模式,因此这真是一个非常接近语义的 glob.glob 优秀替代品。另外,迭代版本(例如生成器),也就是 glob.iglob 的替换版本,是一个非常简单的适应(只需在遍历匹配结果时即时 yield 中间结果,而不是 extend 单个结果列表并在最后返回)。


1
你认为使用我在编辑中建议的 recursive_glob(pattern, treeroot='.') 怎么样?这样,它可以被称为例如 recursive_glob('*.txt') 并直观地匹配 glob 的语法。 - Chris Redford
@ChrisRedford,我认为这是一个不太重要的问题。现在它与fnmatch.filter的“文件然后模式”参数顺序匹配,这大致与匹配单参数glob.glob的可能性一样有用。 - Alex Martelli

22

您需要使用os.walk来收集符合您要求的文件名。例如:

import os
cfiles = []
for root, dirs, files in os.walk('src'):
  for file in files:
    if file.endswith('.c'):
      cfiles.append(os.path.join(root, file))

17

这是一个使用嵌套列表推导式、os.walk和简单后缀匹配而不是glob的解决方案:

import os
cfiles = [os.path.join(root, filename)
          for root, dirnames, filenames in os.walk('src')
          for filename in filenames if filename.endswith('.c')]

可以压缩为一行代码:

import os;cfiles=[os.path.join(r,f) for r,d,fs in os.walk('src') for f in fs if f.endswith('.c')]

或者概括为一个函数:

import os

def recursive_glob(rootdir='.', suffix=''):
    return [os.path.join(looproot, filename)
            for looproot, _, filenames in os.walk(rootdir)
            for filename in filenames if filename.endswith(suffix)]

cfiles = recursive_glob('src', '.c')

如果您确实需要完整的 glob 样式模式,您可以参考Alex和Bruno的例子并使用fnmatch

import fnmatch
import os

def recursive_glob(rootdir='.', pattern='*'):
    return [os.path.join(looproot, filename)
            for looproot, _, filenames in os.walk(rootdir)
            for filename in filenames
            if fnmatch.fnmatch(filename, pattern)]

cfiles = recursive_glob('src', '*.c')

11
import os, glob

for each in glob.glob('path/**/*.c', recursive=True):
    print(f'Name with path: {each} \nName without path: {os.path.basename(each)}')
  • glob.glob('*.c') :匹配当前目录中所有以.c 结尾的文件。
  • glob.glob('*/*.c') :同上
  • glob.glob('**/*.c') :仅匹配当前目录下的所有子目录中以.c 结尾的文件,而不包括当前目录下的文件。
  • glob.glob('*.c',recursive=True) :同1
  • glob.glob('*/*.c',recursive=True) :同3
  • glob.glob('**/*.c',recursive=True) :匹配当前目录及其所有子目录中以.c 结尾的文件。

11

考虑使用 pathlib.rglob() 方法。

这个方法就像在给定的相对路径前面添加 "**/" 后调用 Path.glob() 方法:

import pathlib


for p in pathlib.Path("src").rglob("*.c"):
    print(p)

还可以参考@taleinat在这里发布的相关帖子,以及其他地方类似的帖子


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接