2010-06-29 16 views
19

Tengo que ocuparme de un directorio de aproximadamente 2 millones de xml para procesar.Cómo listar un directorio de 2 millones de archivos en java sin tener una excepción de "falta de memoria"

Ya he resuelto el proceso de distribución del trabajo entre máquinas e hilos utilizando colas y todo va bien.

Pero ahora el gran problema es el cuello de botella de leer el directorio con los 2 millones de archivos para llenar las colas de forma incremental.

He intentado usar el método File.listFiles(), pero me da una excepción java out of memory: heap space. ¿Algunas ideas?

+1

Lo sentimos, pero ¿qué sistema operativo no es compatible con eso? ¿Vivir en 1950? Sé de varias herramientas que explotan, por ejemplo, en Windows (el explorador se pone extremadamente lento), pero el sistema de archivos lo admite. – TomTom

+5

@TomTom: FAT32 (antiguo, pero de ninguna manera era 1950, y aún bastante común) tiene un límite de archivos por directorio de 65k. –

+0

Pero suponiendo que alguien use eso es negligente, excepto por dispositivos que no lo soportan, y luego el problema, adivinen qué, no sería un "problema al listar los archivos". – TomTom

Respuesta

11

Antes que nada, ¿tiene alguna posibilidad de usar Java 7? Ahí tienes un FileVisitor y el Files.walkFileTree, que probablemente debería funcionar dentro de tus limitaciones de memoria.

De lo contrario, la única manera que se me ocurre es utilizar File.listFiles(FileFilter filter) con un filtro que siempre devuelve false (asegurando que toda la gama de archivos nunca se guarda en la memoria), pero que las capturas de los archivos que se procesan a lo largo del manera, y tal vez los coloca en una cola de productor/consumidor o escribe los nombres de archivo en el disco para un recorrido posterior.

Alternativamente, si el control de los nombres de los archivos, o si se nombran de alguna manera agradable, se podía procesar los archivos en trozos usando un filtro que acepta nombres de archivo en el formulario file0000000 - filefile0001000 continuación file0001000 - filefile0002000 y así en.

Si los nombres son no llamado de una manera agradable de esta manera, usted podría tratar de filtrarlas basado en el hash en clave del nombre de archivo, que se supone que es bastante distribuido de manera uniforme sobre el conjunto de los enteros.


Actualización: suspiro. Probablemente no funcionará. Acaba de tener un vistazo a la implementación listFiles:

public File[] listFiles(FilenameFilter filter) { 
    String ss[] = list(); 
    if (ss == null) return null; 
    ArrayList v = new ArrayList(); 
    for (int i = 0 ; i < ss.length ; i++) { 
     if ((filter == null) || filter.accept(this, ss[i])) { 
      v.add(new File(ss[i], this)); 
     } 
    } 
    return (File[])(v.toArray(new File[v.size()])); 
} 

por lo que es probable fallar en la primera línea de todos modos ... Una especie de decepcionante. Creo que su mejor opción es colocar los archivos en diferentes directorios.

Btw, ¿podría dar un ejemplo de un nombre de archivo? ¿Son "adivinables"? Al igual que

for (int i = 0; i < 100000; i++) 
    tryToOpen(String.format("file%05d", i)) 
+0

Java 7 no es una opción en este momento. Actualmente estoy probando la opción de filtro. Afortunadamente, los archivos tienen una jerarquía escrita en el nombre del archivo. Entonces esta opción podría funcionar – Fgblanch

+1

aioobe efectivamente no funcionó. He encontrado que los nombres de los archivos son "adivinables" :) así que lo haré al revés: Genere los nombres de los archivos y luego vaya a la carpeta e intente acceder a ellos. Muchas gracias por su ayuda – Fgblanch

1

Al puño podrías intentar aumentar la memoria de tu JVM con el paso -Xmx1024m p. Ej.

+0

Tengo la sensación de que esto no solucionará el problema, y ​​la JVM simplemente se quedará sin memoria * levemente * más tarde. – Piskvor

+0

@Piskvor Si es así, creo que no hay forma de resolver este problema. Lo que sea que use para analizar el sistema de archivos de os necesitará una cantidad determinada de bytes, con 2 millones de archivos, esto puede volverse demasiado rápido. – InsertNickHere

+0

no necesita guardar todos sus datos en la RAM al mismo tiempo. – Piskvor

2

¿Por qué almacena 2 millones de archivos en el mismo directorio de todos modos? Me imagino que ya ralentiza el acceso terriblemente en el nivel del sistema operativo.

Definitivamente quisiera tenerlos divididos en subdirectorios (por ejemplo, por fecha/hora de creación) ya antes del procesamiento. Pero si no es posible por alguna razón, ¿podría hacerse durante el procesamiento? P.ej. mueva 1000 archivos en cola para Process1 en Directory1, otros 1000 archivos para Process2 en Directory2, etc. Luego, cada proceso/thread ve solo el (número limitado de) archivos divididos para él.

+0

Bucear es un problema en sí mismo. Estoy pensando en eso también en las funciones de OS bash. No es posible hacerlo mientras se procesa porque la excepción se produce al intentar enumerar el directorio mediante programación. – Fgblanch

0

Por favor, publique el rastro de la pila completa de la excepción OOM para identificar dónde está el cuello de botella, así como un programa corto y completo de Java que muestre el comportamiento que ve.

Lo más probable es que recopile las dos millones de entradas en la memoria y no quepan. ¿Se puede aumentar el espacio de montón?

8

Uso File.list() en lugar de File.listFiles() - String los objetos que devuelve consumen menos memoria que los File objetos, y (lo más importante, dependiendo de la ubicación del directorio) que no contienen el nombre de ruta completo.

A continuación, construya File objetos según sea necesario al procesar el resultado.

Sin embargo, esto tampoco funcionará para directorios arbitrariamente grandes. Es una mejor idea general organizar sus archivos en una jerarquía de directorios para que ningún directorio tenga más de unos miles de entradas.

0

Si los nombres de archivo siguen ciertas reglas, puede usar File.list(filter) en lugar de File.listFiles para obtener partes manejables de la lista de archivos.

-3

Prueba de esto, se trabaja para mí, pero yo no tenía tantos documentos ...

File dir = new File("directory"); 
String[] children = dir.list(); 
if (children == null) { 
    //Either dir does not exist or is not a directory 
    System.out.print("Directory doesn't exist\n"); 
} 
else { 
    for (int i=0; i<children.length; i++) { 
    // Get filename of file or directory 
    String filename = children[i]; 
} 
+0

Es directamente lo que no funciona para asker, tiene muchos archivos –

9

Si Java 7 no es una opción, este truco funciona (para UNIX):

Process process = Runtime.getRuntime().exec(new String[]{"ls", "-f", "/path"}); 
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 
String line; 
while (null != (line = reader.readLine())) { 
    if (line.startsWith(".")) 
     continue; 
    System.out.println(line); 
} 

El parámetro -f acelerarlo (de man ls):

-f  do not sort, enable -aU, disable -lst 
+1

No es un truco sino una forma de lidiar con la limitada API de Java;) Pero debería agregar compatibilidad con otros sistemas operativos, y sería prima;) –

2

Ya que estás en Windows, parece como si sh Debería haber usado ProcessBuilder para comenzar algo así como "cmd/k dir/b target_directory", capturar el resultado de eso y enrutarlo a un archivo. A continuación, puede procesar ese archivo una línea a la vez, leyendo los nombres de los archivos y tratando con ellos.

¿Mejor tarde que nunca? ;)

5

En caso de que pueda usar Java 7, esto se puede hacer de esta manera y no tendrá esos problemas de falta de memoria.

Path path = FileSystems.getDefault().getPath("C:\\path\\with\\lots\\of\\files"); 
     Files.walkFileTree(path, new FileVisitor<Path>() { 
      @Override 
      public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 
       return FileVisitResult.CONTINUE; 
      } 

      @Override 
      public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 
       // here you have the files to process 
       System.out.println(file); 
       return FileVisitResult.CONTINUE; 
      } 

      @Override 
      public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 
       return FileVisitResult.TERMINATE; 
      } 

      @Override 
      public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 
       return FileVisitResult.CONTINUE; 
      } 
     }); 
-1

Puede usar listFiles con un FilenameFilter especial. La primera vez que FilenameFilter se envía a listFiles, acepta los primeros 1000 archivos y luego los guarda como visitados.

La próxima vez que FilenameFilter se envíe a listFiles, ignora los primeros 1000 archivos visitados y devuelve los siguientes 1000, y así sucesivamente hasta que se complete.

+0

La primera línea en listFiles (incluso con FilenameFilter) crea una matriz de cadenas, cada cadena un nombre de archivo en el directorio. Además, señalado por @aioobe. – gjain

3

Puede hacerlo con la biblioteca Apache FileUtils. Sin problema de memoria Lo revisé con visualvm.

Iterator<File> it = FileUtils.iterateFiles(folder, null, true); 
    while (it.hasNext()) 
    { 
    File fileEntry = (File) it.next(); 
    } 

Espero que ayude. bye

+1

FileUtils (marcado con 2.4) internamente también utiliza File # list(), por lo que aparecerá el mismo problema con los grandes directorios. Tenga en cuenta que #iterateFiles() simplemente devuelve el .iterator() del resultado de #listFiles(). – ankon

0

Como primer enfoque, puede tratar de modificar algunas configuraciones de memoria JVM, p. aumente el tamaño del montón como se sugirió o incluso use la opción AggressiveHeap. Teniendo en cuenta la gran cantidad de archivos, esto puede no ser de ayuda, entonces sugeriría solucionar el problema. Cree varios archivos con nombres de archivo en cada uno, digamos 500k nombres de archivo por archivo y lea de ellos.

0

Me enfrenté al mismo problema cuando desarrollé la aplicación de escaneo de malware.Mi solución es ejecutar el comando de shell para listar todos los archivos. Es más rápido que los métodos recursivos navegar por carpeta por carpeta.

ver más acerca de comandos shell aquí: http://adbshell.com/commands/adb-shell-ls

 Process process = Runtime.getRuntime().exec("ls -R /"); 
     BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream())); 

     //TODO: Read the stream to get a list of file path. 
0

Esto también requiere Java 7, pero es más simple que la respuesta Files.walkFileTree si lo que desea es mostrar el contenido de un directorio y no a pie todo el árbol:

Path dir = Paths.get("/some/directory"); 
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { 
    for (Path path : stream) { 
     handleFile(path.toFile()); 
    } 
} catch (IOException e) { 
    handleException(e); 
} 

La implementación de DirectoryStream es específica de la plataforma y nunca llama File.list ni nada parecido, en lugar de utilizar las llamadas al sistema Unix o Windows que repetir un directorio de una entrada a la vez.

Cuestiones relacionadas