c# - obtener - ¿Hay una forma más rápida de encontrar todos los archivos en un directorio y todos los subdirectorios?
listar directorios c# (12)
Estoy escribiendo un programa que necesita buscar un directorio y todos sus subdirectorios para los archivos que tienen una cierta extensión. Esto se usará tanto en una unidad local como de red, por lo que el rendimiento es un problema.
Aquí está el método recursivo que estoy usando ahora:
private void GetFileList(string fileSearchPattern, string rootFolderPath, List<FileInfo> files)
{
DirectoryInfo di = new DirectoryInfo(rootFolderPath);
FileInfo[] fiArr = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
files.AddRange(fiArr);
DirectoryInfo[] diArr = di.GetDirectories();
foreach (DirectoryInfo info in diArr)
{
GetFileList(fileSearchPattern, info.FullName, files);
}
}
Podría establecer SearchOption en AllDirectories y no utilizar un método recursivo, pero en el futuro querré insertar algún código para notificar al usuario qué carpeta se está escaneando actualmente.
Mientras estoy creando una lista de objetos FileInfo, ahora todo lo que realmente me importa son las rutas a los archivos. Tendré una lista de archivos existente, que quiero comparar con la nueva lista de archivos para ver qué archivos se agregaron o eliminaron. ¿Hay alguna forma más rápida de generar esta lista de rutas de archivos? ¿Hay algo que pueda hacer para optimizar esta búsqueda de archivos al consultar los archivos en una unidad de red compartida?
Actualización 1
Traté de crear un método no recursivo que hiciera lo mismo encontrando primero todos los subdirectorios y luego escaneando iterativamente cada directorio para buscar archivos. Este es el método:
public static List<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
DirectoryInfo rootDir = new DirectoryInfo(rootFolderPath);
List<DirectoryInfo> dirList = new List<DirectoryInfo>(rootDir.GetDirectories("*", SearchOption.AllDirectories));
dirList.Add(rootDir);
List<FileInfo> fileList = new List<FileInfo>();
foreach (DirectoryInfo dir in dirList)
{
fileList.AddRange(dir.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly));
}
return fileList;
}
Actualización 2
De acuerdo, realicé algunas pruebas en una carpeta local y una remota, y ambas tienen muchos archivos (~ 1200). Aquí están los métodos en los que realicé las pruebas. Los resultados están abajo.
- GetFileListA () : solución no recursiva en la actualización anterior. Creo que es equivalente a la solución de Jay.
- GetFileListB () : Método recursivo de la pregunta original
- GetFileListC () : Obtiene todos los directorios con el método estático Directory.GetDirectories (). Luego obtiene todas las rutas de archivos con el método estático Directory.GetFiles (). Rellena y devuelve una lista
- GetFileListD () : la solución de Marc Gravell utiliza una cola y devuelve IEnumberable. Completé una lista con el IEnumerable resultante
- DirectoryInfo.GetFiles : no se creó ningún método adicional. Crear una instancia de DirectoryInfo desde la ruta de la carpeta raíz. Se llama GetFiles usando SearchOption.AllDirectories
- Directory.GetFiles : no se creó ningún método adicional. Llamado al método estático de GetFiles del directorio usando SearchOption.AllDirectories
Method Local Folder Remote Folder GetFileListA() 00:00.0781235 05:22.9000502 GetFileListB() 00:00.0624988 03:43.5425829 GetFileListC() 00:00.0624988 05:19.7282361 GetFileListD() 00:00.0468741 03:38.1208120 DirectoryInfo.GetFiles 00:00.0468741 03:45.4644210 Directory.GetFiles 00:00.0312494 03:48.0737459
. . .so parece que Marc es el más rápido.
Buena pregunta
Jugué un poco y, aprovechando los bloques de iteradores y LINQ, parece que he mejorado la implementación revisada en aproximadamente un 40%
Me gustaría que lo pruebes usando tus métodos de sincronización y en tu red para ver cómo es la diferencia.
Aquí está la carne de eso
private static IEnumerable<FileInfo> GetFileList(string searchPattern, string rootFolderPath)
{
var rootDir = new DirectoryInfo(rootFolderPath);
var dirList = rootDir.GetDirectories("*", SearchOption.AllDirectories);
return from directoriesWithFiles in ReturnFiles(dirList, searchPattern).SelectMany(files => files)
select directoriesWithFiles;
}
private static IEnumerable<FileInfo[]> ReturnFiles(DirectoryInfo[] dirList, string fileSearchPattern)
{
foreach (DirectoryInfo dir in dirList)
{
yield return dir.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
}
}
Considere dividir el método actualizado en dos iteradores:
private static IEnumerable<DirectoryInfo> GetDirs(string rootFolderPath)
{
DirectoryInfo rootDir = new DirectoryInfo(rootFolderPath);
yield return rootDir;
foreach(DirectoryInfo di in rootDir.GetDirectories("*", SearchOption.AllDirectories));
{
yield return di;
}
yield break;
}
public static IEnumerable<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
var allDirs = GetDirs(rootFolderPath);
foreach(DirectoryInfo di in allDirs())
{
var files = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
foreach(FileInfo fi in files)
{
yield return fi;
}
}
yield break;
}
Además, además del escenario específico de la red, si pudieras instalar un servicio pequeño en ese servidor al que pudieras llamar desde una máquina cliente, te acercarías mucho más a tus resultados de "carpeta local", porque la búsqueda podría ejecuta en el servidor y solo devuelve los resultados a ti. Este sería su mayor aumento de velocidad en el escenario de carpeta de red, pero puede no estar disponible en su situación. He estado usando un programa de sincronización de archivos que incluye esta opción: una vez que instalé el servicio en mi servidor, el programa se hizo MUCHO más rápido para identificar los archivos que eran nuevos, eliminados y no sincronizados.
DirectoryInfo parece proporcionar mucha más información de la que necesita, intente conectar un comando dir y analizar la información desde allí.
Es horrible, y el motivo por el cual el trabajo de búsqueda de archivos es horrible en las plataformas de Windows es porque MS cometió un error, que parecen no estar dispuestos a corregir. Debería poder usar SearchOption.AllDirectories Y todos obtendríamos la velocidad que queremos. Pero no puede hacer eso, porque GetDirectories necesita una devolución de llamada para que pueda decidir qué hacer con los directorios a los que no tiene acceso. MS olvidó o no pensó en probar la clase en sus propias computadoras.
Entonces, todos nos quedamos con los bucles recursivos sin sentido.
Dentro de C # / Managed C ++ tiene muy pocas opciones, estas son también las opciones que toma MS, porque sus codificadores tampoco han encontrado la forma de evitarlo.
Lo principal es con elementos de visualización, como TreeViews y FileViews, solo busca y muestra lo que los usuarios pueden ver. Hay una veintena de ayudantes en los controles, incluidos los factores desencadenantes, que le indican cuándo debe completar algunos datos.
En los árboles, comenzando desde el modo colapsado, buscar ese único directorio cuando el usuario lo abre en el árbol, es mucho más rápido que esperar a que se llene un árbol completo. Lo mismo en FileViews, me inclino por una regla del 10%, sin importar cuántos elementos quepan en el área de visualización, tengo otro 10% listo si el usuario se desplaza, es muy receptivo.
MS hace la búsqueda previa y el reloj de directorio. Una pequeña base de datos de directorios, archivos, esto significa que OnOpen your Trees, etc. tiene un buen punto de partida rápido, se cae un poco en la actualización.
Pero mezcle las dos ideas, tome sus directorios y archivos de la base de datos, pero realice una búsqueda de actualización cuando se expanda un nodo de árbol (solo ese nodo de árbol) y cuando se seleccione un directorio diferente en el árbol.
Pero la mejor solución es agregar su sistema de búsqueda de archivos como un servicio. Los MS ya tienen esto, pero hasta donde sé, no tenemos acceso a él, sospecho que es porque es inmune a los errores de "acceso fallido al directorio". Al igual que con el MS, si tiene un servicio que se ejecuta en el nivel Admin, debe tener cuidado de no regalar su seguridad solo por el bien de un poco de velocidad adicional.
Esto toma 30 segundos para obtener 2 millones de nombres de archivo que cumplen con el filtro. La razón por la que esto es tan rápido es porque solo estoy realizando 1 enumeración. Cada enumeración adicional afecta el rendimiento. La longitud variable está abierta a su interpretación y no necesariamente relacionada con el ejemplo de enumeración.
if (Directory.Exists(path))
{
files = Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)
.Where(s => s.EndsWith(".xml") || s.EndsWith(".csv"))
.Select(s => s.Remove(0, length)).ToList(); // Remove the Dir info.
}
La respuesta breve de cómo mejorar el rendimiento de ese código es: No puedes.
El rendimiento real alcanzado por su experiencia es la latencia real del disco o la red, por lo que no importa en qué sentido lo invierta, debe verificar e iterar a través de cada elemento del archivo y recuperar listados de directorios y archivos. (Eso es, por supuesto, excluyendo modificaciones de hardware o controladores para reducir o mejorar la latencia del disco, pero muchas personas ya están pagando mucho dinero para resolver esos problemas, por lo que ignoraremos ese lado por el momento)
Dadas las restricciones originales, ya hay varias soluciones publicadas que envuelven el proceso de iteración de forma más o menos elegante (Sin embargo, dado que supongo que estoy leyendo desde un único disco duro, el paralelismo NO ayudará a transponer más rápidamente un árbol de directorios, y incluso puede aumentar ese tiempo ya que ahora tiene dos o más hilos que luchan por datos en diferentes partes de la unidad mientras intenta buscar hacia atrás y cuarto) reduce el número de objetos creados, etc. Sin embargo, si evaluamos cómo será la función consumido por el desarrollador final hay algunas optimizaciones y generalizaciones que podemos idear.
En primer lugar, podemos retrasar la ejecución del rendimiento devolviendo un IEnumerable, yield return logra esto compilando en un enumerador de máquina de estado dentro de una clase anónima que implementa IEnumerable y se devuelve cuando se ejecuta el método. La mayoría de los métodos en LINQ se escriben para retrasar la ejecución hasta que se realice la iteración, por lo que el código en una selección o en SelectMany no se realizará hasta que se itere IEnumerable. El resultado final de la ejecución retrasada solo se siente si necesita tomar un subconjunto de los datos en un momento posterior, por ejemplo, si solo necesita los primeros 10 resultados, demorar la ejecución de una consulta que devuelve varios miles de resultados no lo hará. itere a través de los 1000 resultados completos hasta que necesite más de diez.
Ahora, dado que desea hacer una búsqueda de subcarpetas, también puedo inferir que puede ser útil si puede especificar esa profundidad, y si lo hago también generaliza mi problema, pero también necesita una solución recursiva. Luego, más tarde, cuando alguien decide que ahora necesita buscar profundamente en dos directorios porque aumentamos la cantidad de archivos y decidimos agregar otra capa de categorización , puede simplemente hacer una pequeña modificación en lugar de volver a escribir la función.
A la luz de todo eso, aquí está la solución que surgió que proporciona una solución más general que algunas de las anteriores:
public static IEnumerable<FileInfo> BetterFileList(string fileSearchPattern, string rootFolderPath)
{
return BetterFileList(fileSearchPattern, new DirectoryInfo(rootFolderPath), 1);
}
public static IEnumerable<FileInfo> BetterFileList(string fileSearchPattern, DirectoryInfo directory, int depth)
{
return depth == 0
? directory.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly)
: directory.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly).Concat(
directory.GetDirectories().SelectMany(x => BetterFileList(fileSearchPattern, x, depth - 1)));
}
En una nota lateral, algo más que no ha sido mencionado por nadie hasta el momento es la seguridad y los permisos de los archivos. Actualmente, no hay solicitudes de verificación, manejo o permisos, y el código arrojará excepciones de permisos de archivos si encuentra un directorio para el que no tiene acceso.
Los métodos BCL son portátiles, por así decirlo. Si me mantengo 100% administrado, creo que lo mejor que puede hacer es llamar a GetDirectories / Folders mientras comprueba los derechos de acceso (o posiblemente no verificar los derechos y tener otro hilo listo cuando el primero demore demasiado, una señal de que se trata de lanzar una excepción de acceso no autorizado; esto podría evitarse con filtros de excepción que usan VB o hasta el día de hoy inédito c #).
Si quieres más rápido que GetDirectories, tienes que llamar a win32 (findsomethingEx, etc.) que proporciona indicadores específicos que permiten ignorar posibles IO innecesarios al atravesar las estructuras de MFT. Además, si el disco es un recurso compartido de red, puede haber una gran aceleración con un enfoque similar, pero esta vez evitando también excesivos viajes de ida y vuelta en red.
Ahora, si tiene admin y usa ntfs y tiene mucha prisa con millones de archivos, la manera más rápida de pasar por ellos (asumiendo el óxido en rotación donde mata la latencia del disco) es el uso de mft y journaling en combinación, esencialmente reemplazando el servicio de indexación con uno que está dirigido a su necesidad específica. Si solo necesita encontrar nombres de archivo y no tamaños (o tamaños también, pero luego debe almacenarlos en caché y usar el diario para notar los cambios), este enfoque podría permitir la búsqueda prácticamente instantánea de decenas de millones de archivos y carpetas si se implementan de manera ideal. Puede haber uno o dos paywares que se hayan molestado con esto. Hay muestras de ambos MFT (DiscUtils) y lectura de diario (google) en C # alrededor. Solo tengo unos 5 millones de archivos y el solo uso de NTFSSearch es lo suficientemente bueno para esa cantidad, ya que toma entre 10 y 20 segundos buscarlos. Con la lectura del diario agregada, bajaría a <3 segundos por esa cantidad.
Me inclinaría a devolver un IEnumerable <> en este caso: dependiendo de cómo consuma los resultados, podría ser una mejora, además de que reduce su huella de parámetro en 1/3 y evita pasar inadvertidamente por esa lista.
private IEnumerable<FileInfo> GetFileList(string fileSearchPattern, string rootFolderPath)
{
DirectoryInfo di = new DirectoryInfo(rootFolderPath);
var fiArr = di.GetFiles(fileSearchPattern, SearchOption.TopDirectoryOnly);
foreach (FileInfo fi in fiArr)
{
yield return fi;
}
var diArr = di.GetDirectories();
foreach (DirectoryInfo di in diArr)
{
var nextRound = GetFileList(fileSearchPattern, di.FullnName);
foreach (FileInfo fi in nextRound)
{
yield return fi;
}
}
yield break;
}
Otra idea sería BackgroundWorker
objetos de BackgroundWorker
para trol a través de directorios. No querría un hilo nuevo para cada directorio, pero podría crearlos en el nivel superior (primer paso a través de GetFileList()
), de modo que si ejecuta en su disco C:/
, con 12 directorios, cada uno de esos directorios ser buscado por un hilo diferente, que luego recurrirá a través de subdirectorios. Tendrá un hilo pasando por C:/Windows
mientras que otro pasa por C:/Program Files
. Hay muchas variables sobre cómo esto afectará el rendimiento: tendría que probarlo para ver.
Para fines de búsqueda de archivos y directorios, me gustaría ofrecer el uso de la biblioteca .NET de subprocesos múltiples que posee amplias oportunidades de búsqueda. Toda la información sobre la biblioteca que puede encontrar en GitHub: https://github.com/VladPVS/FastSearchLibrary
Si desea descargarlo, puede hacerlo aquí: https://github.com/VladPVS/FastSearchLibrary/releases
Funciona muy rápido Compruébalo tú mismo!
Si tienes alguna pregunta, pregúntales.
Es un ejemplo demostrativo de cómo puedes usarlo:
class Searcher
{
private static object locker = new object();
private FileSearcher searcher;
List<FileInfo> files;
public Searcher()
{
files = new List<FileInfo>(); // create list that will contain search result
}
public void Startsearch()
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
// create tokenSource to get stop search process possibility
searcher = new FileSearcher(@"C:/", (f) =>
{
return Regex.IsMatch(f.Name, @".*[Dd]ragon.*.jpg$");
}, tokenSource); // give tokenSource in constructor
searcher.FilesFound += (sender, arg) => // subscribe on FilesFound event
{
lock (locker) // using a lock is obligatorily
{
arg.Files.ForEach((f) =>
{
files.Add(f); // add the next part of the received files to the results list
Console.WriteLine($"File location: {f.FullName}, /nCreation.Time: {f.CreationTime}");
});
if (files.Count >= 10) // one can choose any stopping condition
searcher.StopSearch();
}
};
searcher.SearchCompleted += (sender, arg) => // subscribe on SearchCompleted event
{
if (arg.IsCanceled) // check whether StopSearch() called
Console.WriteLine("Search stopped.");
else
Console.WriteLine("Search completed.");
Console.WriteLine($"Quantity of files: {files.Count}"); // show amount of finding files
};
searcher.StartSearchAsync();
// start search process as an asynchronous operation that doesn''t block the called thread
}
}
Es otro ejemplo:
***
List<string> folders = new List<string>
{
@"C:/Users/Public",
@"C:/Windows/System32",
@"D:/Program Files",
@"D:/Program Files (x86)"
}; // list of search directories
List<string> keywords = new List<string> { "word1", "word2", "word3" }; // list of search keywords
FileSearcherMultiple multipleSearcher = new FileSearcherMultiple(folders, (f) =>
{
if (f.CreationTime >= new DateTime(2015, 3, 15) &&
(f.Extension == ".cs" || f.Extension == ".sln"))
foreach (var keyword in keywords)
if (f.Name.Contains(keyword))
return true;
return false;
}, tokenSource, ExecuteHandlers.InCurrentTask, true);
***
Pruebe esta versión de bloque iterador que evita la recursión y los objetos de Info
:
public static IEnumerable<string> GetFileList(string fileSearchPattern, string rootFolderPath)
{
Queue<string> pending = new Queue<string>();
pending.Enqueue(rootFolderPath);
string[] tmp;
while (pending.Count > 0)
{
rootFolderPath = pending.Dequeue();
try
{
tmp = Directory.GetFiles(rootFolderPath, fileSearchPattern);
}
catch (UnauthorizedAccessException)
{
continue;
}
for (int i = 0; i < tmp.Length; i++)
{
yield return tmp[i];
}
tmp = Directory.GetDirectories(rootFolderPath);
for (int i = 0; i < tmp.Length; i++)
{
pending.Enqueue(tmp[i]);
}
}
}
Tenga en cuenta también que 4.0 tiene incorporadas versiones de bloque de iterador ( EnumerateFiles
, EnumerateFileSystemEntries
) que pueden ser más rápidas (acceso más directo al sistema de archivos, menos matrices)
Pruebe la programación paralela:
private string _fileSearchPattern;
private List<string> _files;
private object lockThis = new object();
public List<string> GetFileList(string fileSearchPattern, string rootFolderPath)
{
_fileSearchPattern = fileSearchPattern;
AddFileList(rootFolderPath);
return _files;
}
private void AddFileList(string rootFolderPath)
{
var files = Directory.GetFiles(rootFolderPath, _fileSearchPattern);
lock (lockThis)
{
_files.AddRange(files);
}
var directories = Directory.GetDirectories(rootFolderPath);
Parallel.ForEach(directories, AddFileList); // same as Parallel.ForEach(directories, directory => AddFileList(directory));
}
Puede usar foreach paralelo (.Net 4.0) o puede probar Poor Man''s Parallel.ForEach Iterator para .Net3.5. Eso puede acelerar tu búsqueda.