node . js fs.Readdir递归目录搜索



对使用fs.readdir进行异步目录搜索有什么想法吗?我意识到我们可以引入递归,并使用下一个要读取的目录调用读取目录函数,但我有点担心它不是异步的…

任何想法?我已经看到了node-walk,它很好,但不像readdir那样只给我数组中的文件。尽管

查找类似…的输出

['file1.txt', 'file2.txt', 'dir/file3.txt']

基本上有两种方法可以做到这一点。在异步环境中,你会注意到有两种循环:串行和并行。串行循环在进入下一个迭代之前等待一个迭代完成-这保证了循环的每次迭代都按顺序完成。在并行循环中,所有的迭代都是同时开始的,一个迭代可以在另一个迭代之前完成,但是,它比串行循环要快得多。因此,在这种情况下,可能最好使用并行循环,因为只要它完成并返回结果(除非您希望它们按顺序),它以什么顺序完成并不重要。

一个平行循环应该是这样的:

var fs = require('fs');
var path = require('path');
var walk = function(dir, done) {
  var results = [];
  fs.readdir(dir, function(err, list) {
    if (err) return done(err);
    var pending = list.length;
    if (!pending) return done(null, results);
    list.forEach(function(file) {
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) {
        if (stat && stat.isDirectory()) {
          walk(file, function(err, res) {
            results = results.concat(res);
            if (!--pending) done(null, results);
          });
        } else {
          results.push(file);
          if (!--pending) done(null, results);
        }
      });
    });
  });
};

一个串行循环应该是这样的:

var fs = require('fs');
var path = require('path');
var walk = function(dir, done) {
  var results = [];
  fs.readdir(dir, function(err, list) {
    if (err) return done(err);
    var i = 0;
    (function next() {
      var file = list[i++];
      if (!file) return done(null, results);
      file = path.resolve(dir, file);
      fs.stat(file, function(err, stat) {
        if (stat && stat.isDirectory()) {
          walk(file, function(err, res) {
            results = results.concat(res);
            next();
          });
        } else {
          results.push(file);
          next();
        }
      });
    })();
  });
};

在你的主目录下进行测试(警告:如果你的主目录下有很多东西,结果列表将会很长):

walk(process.env.HOME, function(err, results) {
  if (err) throw err;
  console.log(results);
});
编辑:改进的例子。

这篇文章使用了node 8中最多的新特性,包括Promises, util/promisify, destructuring, async-await, map+reduce等等,让你的同事在试图弄清楚发生了什么时挠头。

8 +

节点

没有外部依赖。

const { promisify } = require('util');
const { resolve } = require('path');
const fs = require('fs');
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
async function getFiles(dir) {
  const subdirs = await readdir(dir);
  const files = await Promise.all(subdirs.map(async (subdir) => {
    const res = resolve(dir, subdir);
    return (await stat(res)).isDirectory() ? getFiles(res) : res;
  }));
  return files.reduce((a, f) => a.concat(f), []);
}
使用

getFiles(__dirname)
  .then(files => console.log(files))
  .catch(e => console.error(e));

节点10.10 +

为node 10+更新了更多的whizbang:

const { resolve } = require('path');
const { readdir } = require('fs').promises;
async function getFiles(dir) {
  const dirents = await readdir(dir, { withFileTypes: true });
  const files = await Promise.all(dirents.map((dirent) => {
    const res = resolve(dir, dirent.name);
    return dirent.isDirectory() ? getFiles(res) : res;
  }));
  return Array.prototype.concat(...files);
}

注意,从节点11.15.0开始,您可以使用files.flat()而不是Array.prototype.concat(...files)来平整化文件数组。

节点11 +

如果你想让所有人都大惑不解,你可以使用以下使用异步迭代器的版本。除了非常酷之外,它还允许消费者一次提取结果,使其更适合真正大的目录。

const { resolve } = require('path');
const { readdir } = require('fs').promises;
async function* getFiles(dir) {
  const dirents = await readdir(dir, { withFileTypes: true });
  for (const dirent of dirents) {
    const res = resolve(dir, dirent.name);
    if (dirent.isDirectory()) {
      yield* getFiles(res);
    } else {
      yield res;
    }
  }
}

用法发生了变化,因为返回类型现在是async迭代器而不是promise

;(async () => {
  for await (const f of getFiles('.')) {
    console.log(f);
  }
})()

如果有人感兴趣,我在这里写了更多关于异步迭代器的内容:https://qwtel.com/posts/software/async-generators-in-the-wild/

以防有人觉得有用,我还把同步版本放在一起。

var walk = function(dir) {
    var results = [];
    var list = fs.readdirSync(dir);
    list.forEach(function(file) {
        file = dir + '/' + file;
        var stat = fs.statSync(file);
        if (stat && stat.isDirectory()) { 
            /* Recurse into a subdirectory */
            results = results.concat(walk(file));
        } else { 
            /* Is a file */
            results.push(file);
        }
    });
    return results;
}

提示:过滤时使用更少的资源。在此函数本身中筛选。例如:用下面的代码替换results.push(file);。根据需要调整:

    file_type = file.split(".").pop();
    file_name = file.split(/(\|/)/g).pop();
    if (file_type == "json") results.push(file);

A。查看文件模块。它有一个叫做walk的函数:

文件。步行(开始,回调)

导航一个文件树,为每个目录调用callback,传入(null, dirPath, dirs, files).

这可能是给你的!是的,它是异步的。但是,我认为如果需要的话,您必须自己聚合完整的路径。

B。另一种选择,甚至是我最喜欢的一种:使用unix find。为什么要再做一件已经被编程好的事情呢?也许不完全是你需要的,但仍然值得一试:

var execFile = require('child_process').execFile;
execFile('find', [ 'somepath/' ], function(err, stdout, stderr) {
  var file_list = stdout.split('n');
  /* now you've got a list with full path file names */
});

Find有一个很好的内置缓存机制,使得后续搜索非常快,只要只有几个文件夹被更改。

我建议使用node-glob来完成这项任务。

var glob = require( 'glob' );  
glob( 'dirname/**/*.js', function( err, files ) {
  console.log( files );
});

另一个不错的npm包是glob.

npm install glob

它非常强大,应该能满足你所有的递归需求。

编辑:

实际上我对glob不是很满意,所以我创建了readdirp。

我非常有信心,它的API使查找文件和目录递归和应用特定的过滤器非常容易。

阅读它的文档以更好地了解它的功能,并通过:

npm install readdirp

简洁、现代、高效:

import {readdir} from 'node:fs/promises'
import {join} from 'node:path'
const walk = async (dirPath) => Promise.all(
  await readdir(dirPath, { withFileTypes: true }).then((entries) => entries.map((entry) => {
    const childPath = join(dirPath, entry.name)
    return entry.isDirectory() ? walk(childPath) : childPath
  })),
)

特别感谢Function的提示:{withFileTypes: true} .


这会自动保留源目录的树结构(您可能需要)。例如:

const allFiles = await walk('src')

allFiles将是TREE,如下所示:

[
  [
    'src/client/api.js',
    'src/client/http-constants.js',
    'src/client/index.html',
    'src/client/index.js',
    [ 'src/client/res/favicon.ico' ],
    'src/client/storage.js'
  ],
  [ 'src/crypto/keygen.js' ],
  'src/discover.js',
  [
    'src/mutations/createNewMutation.js',
    'src/mutations/newAccount.js',
    'src/mutations/transferCredit.js',
    'src/mutations/updateApp.js'
  ],
  [
    'src/server/authentication.js',
    'src/server/handlers.js',
    'src/server/quick-response.js',
    'src/server/server.js',
    'src/server/static-resources.js'
  ],
  [ 'src/util/prompt.js', 'src/util/safeWriteFile.js' ],
  'src/util.js'
]

如果你愿意,可以把它平放:

allFiles.flat(Number.POSITIVE_INFINITY)
[
  'src/client/api.js',
  'src/client/http-constants.js',
  'src/client/index.html',
  'src/client/index.js',
  'src/client/res/favicon.ico',
  'src/client/storage.js',
  'src/crypto/keygen.js',
  'src/discover.js',
  'src/mutations/createNewMutation.js',
  'src/mutations/newAccount.js',
  'src/mutations/transferCredit.js',
  'src/mutations/updateApp.js',
  'src/server/authentication.js',
  'src/server/handlers.js',
  'src/server/quick-response.js',
  'src/server/server.js',
  'src/server/static-resources.js',
  'src/util/prompt.js',
  'src/util/safeWriteFile.js',
  'src/util.js'
]

如果你想使用npm包,扳手是很好的选择。

var wrench = require("wrench");
var files = wrench.readdirSyncRecursive("directory");
wrench.readdirRecursive("directory", function (error, files) {
    // live your dreams
});

编辑(2018):
最近读过的人:作者在2015年弃用了这个包:

wrench.js已弃用,并且已经有一段时间没有更新了。我强烈建议使用fs-extra来执行任何额外的文件系统操作。

Async

const fs = require('fs')
const path = require('path')
const readdir = (p, done, a = [], i = 0) => fs.readdir(p, (e, d = []) =>
  d.map(f => readdir(a[a.push(path.join(p, f)) - 1], () =>
    ++i == d.length && done(a), a)).length || done(a))
readdir(__dirname, console.log)

const fs = require('fs')
const path = require('path')
const readdirSync = (p, a = []) => {
  if (fs.statSync(p).isDirectory())
    fs.readdirSync(p).map(f => readdirSync(a[a.push(path.join(p, f)) - 1], a))
  return a
}
console.log(readdirSync(__dirname))

异步读

function readdir (currentPath, done, allFiles = [], i = 0) {
  fs.readdir(currentPath, function (e, directoryFiles = []) {
    if (!directoryFiles.length)
      return done(allFiles)
    directoryFiles.map(function (file) {
      var joinedPath = path.join(currentPath, file)
      allFiles.push(joinedPath)
      readdir(joinedPath, function () {
        i = i + 1
        if (i == directoryFiles.length)
          done(allFiles)}
      , allFiles)
    })
  })
}
readdir(__dirname, console.log)

注意:两个版本都将遵循符号链接(与原始fs.readdir相同)

With recursive

var fs = require('fs')
var path = process.cwd()
var files = []
var getFiles = function(path, files){
    fs.readdirSync(path).forEach(function(file){
        var subpath = path + '/' + file;
        if(fs.lstatSync(subpath).isDirectory()){
            getFiles(subpath, files);
        } else {
            files.push(path + '/' + file);
        }
    });     
}

调用

getFiles(path, files)
console.log(files) // will log all files in directory

我喜欢上面chjj的答案,如果没有这个开始,我就无法创建我的并行循环版本。

var fs = require("fs");
var tree = function(dir, done) {
  var results = {
        "path": dir
        ,"children": []
      };
  fs.readdir(dir, function(err, list) {
    if (err) { return done(err); }
    var pending = list.length;
    if (!pending) { return done(null, results); }
    list.forEach(function(file) {
      fs.stat(dir + '/' + file, function(err, stat) {
        if (stat && stat.isDirectory()) {
          tree(dir + '/' + file, function(err, res) {
            results.children.push(res);
            if (!--pending){ done(null, results); }
          });
        } else {
          results.children.push({"path": dir + "/" + file});
          if (!--pending) { done(null, results); }
        }
      });
    });
  });
};
module.exports = tree;

我也创建了一个Gist。欢迎评论。我仍然在NodeJS领域起步,所以这是我希望学习更多的一种方式。

Vanilla ES6 + async/await + small &读

我没有找到我在这个线程寻找的答案;在不同的答案中有一些相似的元素,但我只想要一些简单易懂的东西。

为了将来对任何人(也就是几个月后的我自己)有所帮助,我最终使用的是:

const { readdir } = require('fs/promises');
const { join } = require('path');
const readdirRecursive = async dir => {
  const files = await readdir( dir, { withFileTypes: true } );
  const paths = files.map( async file => {
    const path = join( dir, file.name );
    if ( file.isDirectory() ) return await readdirRecursive( path );
    return path;
  } );
  return ( await Promise.all( paths ) ).flat( Infinity );
}
module.exports = {
  readdirRecursive,
}

使用node-dir生成您想要的输出

var dir = require('node-dir');
dir.files(__dirname, function(err, files) {
  if (err) throw err;
  console.log(files);
  //we have an array of files now, so now we can iterate that array
  files.forEach(function(path) {
    action(null, path);
  })
});

这是一个简单的同步递归解

const fs = require('fs')
const getFiles = path => {
    const files = []
    for (const file of fs.readdirSync(path)) {
        const fullPath = path + '/' + file
        if(fs.lstatSync(fullPath).isDirectory())
            getFiles(fullPath).forEach(x => files.push(file + '/' + x))
        else files.push(file)
    }
    return files
}

用法:

const files = getFiles(process.cwd())
console.log(files)

你可以异步地写它,但是没有必要。只要确保输入目录存在并且可以访问即可。

基于现代承诺的读目录递归版本:

const fs = require('fs');
const path = require('path');
const readDirRecursive = async (filePath) => {
    const dir = await fs.promises.readdir(filePath);
    const files = await Promise.all(dir.map(async relativePath => {
        const absolutePath = path.join(filePath, relativePath);
        const stat = await fs.promises.lstat(absolutePath);
        return stat.isDirectory() ? readDirRecursive(absolutePath) : absolutePath;
    }));
    return files.flat();
}

qwtel的答案变体,在TypeScript

import { resolve } from 'path';
import { readdir } from 'fs/promises';
async function* getFiles(dir: string): AsyncGenerator<string> {
    const entries = await readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
        const res = resolve(dir, entry.name);
        if (entry.isDirectory()) {
            yield* getFiles(res);
        } else {
            yield res;
        }
    }
}

使用async/await,这应该可以工作:

const FS = require('fs');
const readDir = promisify(FS.readdir);
const fileStat = promisify(FS.stat);
async function getFiles(dir) {
    let files = await readDir(dir);
    let result = files.map(file => {
        let path = Path.join(dir,file);
        return fileStat(path).then(stat => stat.isDirectory() ? getFiles(path) : path);
    });
    return flatten(await Promise.all(result));
}
function flatten(arr) {
    return Array.prototype.concat(...arr);
}

你可以用bluebird。promise or this:

/**
 * Returns a function that will wrap the given `nodeFunction`. Instead of taking a callback, the returned function will return a promise whose fate is decided by the callback behavior of the given node function. The node function should conform to node.js convention of accepting a callback as last argument and calling that callback with error as the first argument and success value on the second argument.
 *
 * @param {Function} nodeFunction
 * @returns {Function}
 */
module.exports = function promisify(nodeFunction) {
    return function(...args) {
        return new Promise((resolve, reject) => {
            nodeFunction.call(this, ...args, (err, data) => {
                if(err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            })
        });
    };
};

Node 8+内置Promisify

查看我的另一个生成器方法的答案,它可以更快地给出结果。

基于简单的异步承诺


const fs = require('fs/promises');
const getDirRecursive = async (dir) => {
    try {
        const items = await fs.readdir(dir);
        let files = [];
        for (const item of items) {
            if ((await fs.lstat(`${dir}/${item}`)).isDirectory()) files = [...files, ...(await getDirRecursive(`${dir}/${item}`))];
            else files.push({file: item, path: `${dir}/${item}`, parents: dir.split("/")});
        }
        return files;
    } catch (e) {
        return e
    }
};

用法:await getDirRecursive("./public");

我最近编写了这个代码,并认为在这里分享它是有意义的。该代码使用了async库。

var fs = require('fs');
var async = require('async');
var scan = function(dir, suffix, callback) {
  fs.readdir(dir, function(err, files) {
    var returnFiles = [];
    async.each(files, function(file, next) {
      var filePath = dir + '/' + file;
      fs.stat(filePath, function(err, stat) {
        if (err) {
          return next(err);
        }
        if (stat.isDirectory()) {
          scan(filePath, suffix, function(err, results) {
            if (err) {
              return next(err);
            }
            returnFiles = returnFiles.concat(results);
            next();
          })
        }
        else if (stat.isFile()) {
          if (file.indexOf(suffix, file.length - suffix.length) !== -1) {
            returnFiles.push(filePath);
          }
          next();
        }
      });
    }, function(err) {
      callback(err, returnFiles);
    });
  });
};

你可以这样使用:

scan('/some/dir', '.ext', function(err, files) {
  // Do something with files that ends in '.ext'.
  console.log(files);
});

一个名为Filehound的库是另一个选择。它将递归地搜索给定目录(默认情况下是工作目录)。它支持各种过滤器、回调、承诺和同步搜索。

例如,在当前工作目录中搜索所有文件(使用回调):

const Filehound = require('filehound');
Filehound.create()
.find((err, files) => {
    if (err) {
        return console.error(`error: ${err}`);
    }
    console.log(files); // array of files
});

或承诺并指定一个特定的目录:

const Filehound = require('filehound');
Filehound.create()
.paths("/tmp")
.find()
.each(console.log);

查阅文档了解更多用例和用法示例:https://github.com/nspragg/filehound

声明:我是作者。

查看final-fs库。它提供了一个readdirRecursive函数:

ffs.readdirRecursive(dirPath, true, 'my/initial/path')
    .then(function (files) {
        // in the `files` variable you've got all the files
    })
    .otherwise(function (err) {
        // something went wrong
    });

v20.1版本中可用的最短本地解决方案:

import fs from 'node:fs'
const results = fs.promises.readdir('/tmp', { recursive: true })
fs.readdirfs.readdirSync函数也支持

recursive选项。

独立承诺实现

我在这个例子中使用when.js承诺库。

var fs = require('fs')
, path = require('path')
, when = require('when')
, nodefn = require('when/node/function');
function walk (directory, includeDir) {
    var results = [];
    return when.map(nodefn.call(fs.readdir, directory), function(file) {
        file = path.join(directory, file);
        return nodefn.call(fs.stat, file).then(function(stat) {
            if (stat.isFile()) { return results.push(file); }
            if (includeDir) { results.push(file + path.sep); }
            return walk(file, includeDir).then(function(filesInDir) {
                results = results.concat(filesInDir);
            });
        });
    }).then(function() {
        return results;
    });
};
walk(__dirname).then(function(files) {
    console.log(files);
}).otherwise(function(error) {
    console.error(error.stack || error);
});

我包含了一个可选参数includeDir,如果设置为true,它将包含文件清单中的目录。

klaw和klaw-sync值得考虑。这些都是node-fs-extra的一部分。

对于Node 10.3+,这里有一个For -await解决方案:

#!/usr/bin/env node
const FS = require('fs');
const Util = require('util');
const readDir = Util.promisify(FS.readdir);
const Path = require('path');
async function* readDirR(path) {
    const entries = await readDir(path,{withFileTypes:true});
    for(let entry of entries) {
        const fullPath = Path.join(path,entry.name);
        if(entry.isDirectory()) {
            yield* readDirR(fullPath);
        } else {
            yield fullPath;
        }
    }
}
async function main() {
    const start = process.hrtime.bigint();
    for await(const file of readDirR('/mnt/home/media/Unsorted')) {
        console.log(file);
    }
    console.log((process.hrtime.bigint()-start)/1000000n);
}
main().catch(err => {
    console.error(err);
});

这个解决方案的好处是您可以立即开始处理结果;例如,读取我的媒体目录中的所有文件需要12秒,但如果我这样做,我可以在几毫秒内获得第一个结果。

这是另一个实现。以上解决方案都没有任何限制,因此,如果您的目录结构很大,它们都将崩溃并最终耗尽资源。

var async = require('async');
var fs = require('fs');
var resolve = require('path').resolve;
var scan = function(path, concurrency, callback) {
    var list = [];
    var walker = async.queue(function(path, callback) {
        fs.stat(path, function(err, stats) {
            if (err) {
                return callback(err);
            } else {
                if (stats.isDirectory()) {
                    fs.readdir(path, function(err, files) {
                        if (err) {
                            callback(err);
                        } else {
                            for (var i = 0; i < files.length; i++) {
                                walker.push(resolve(path, files[i]));
                            }
                            callback();
                        }
                    });
                } else {
                    list.push(path);
                    callback();
                }
            }
        });
    }, concurrency);
    walker.push(path);
    walker.drain = function() {
        callback(list);
    }
};

使用50的并发性工作得很好,并且几乎与小型目录结构的简单实现一样快。

递归readdir模块具有此功能。

我修改了Trevor Senior的承诺基于答案的工作与蓝鸟

var fs = require('fs'),
    path = require('path'),
    Promise = require('bluebird');
var readdirAsync = Promise.promisify(fs.readdir);
var statAsync = Promise.promisify(fs.stat);
function walkFiles (directory) {
    var results = [];
    return readdirAsync(directory).map(function(file) {
        file = path.join(directory, file);
        return statAsync(file).then(function(stat) {
            if (stat.isFile()) {
                return results.push(file);
            }
            return walkFiles(file).then(function(filesInDir) {
                results = results.concat(filesInDir);
            });
        });
    }).then(function() {
        return results;
    });
}
//use
walkDir(__dirname).then(function(files) {
    console.log(files);
}).catch(function(e) {
    console.error(e); {
});

为了好玩,这里有一个基于流的版本,可以与highland.js流库一起工作。本文由Victor Vu共同撰写。

###
  directory >---m------> dirFilesStream >---------o----> out
                |                                 |
                |                                 |
                +--------< returnPipe <-----------+
  legend: (m)erge  (o)bserve
 + directory         has the initial file
 + dirListStream     does a directory listing
 + out               prints out the full path of the file
 + returnPipe        runs stat and filters on directories
###
_ = require('highland')
fs = require('fs')
fsPath = require('path')
directory = _(['someDirectory'])
mergePoint = _()
dirFilesStream = mergePoint.merge().flatMap((parentPath) ->
  _.wrapCallback(fs.readdir)(parentPath).sequence().map (path) ->
    fsPath.join parentPath, path
)
out = dirFilesStream
# Create the return pipe
returnPipe = dirFilesStream.observe().flatFilter((path) ->
  _.wrapCallback(fs.stat)(path).map (v) ->
    v.isDirectory()
)
# Connect up the merge point now that we have all of our streams.
mergePoint.write directory
mergePoint.write returnPipe
mergePoint.end()
# Release backpressure.  This will print files as they are discovered
out.each H.log
# Another way would be to queue them all up and then print them all out at once.
# out.toArray((files)-> console.log(files))

使用Promises (Q)在函数式中解决这个问题:

var fs = require('fs'),
    fsPath = require('path'),
    Q = require('q');
var walk = function (dir) {
  return Q.ninvoke(fs, 'readdir', dir).then(function (files) {
    return Q.all(files.map(function (file) {
      file = fsPath.join(dir, file);
      return Q.ninvoke(fs, 'lstat', file).then(function (stat) {
        if (stat.isDirectory()) {
          return walk(file);
        } else {
          return [file];
        }
      });
    }));
  }).then(function (files) {
    return files.reduce(function (pre, cur) {
      return pre.concat(cur);
    });
  });
};

它返回一个数组的promise,所以你可以这样使用:

walk('/home/mypath').then(function (files) { console.log(files); });

最新更新