2010-11-08 20 views
30

Quiero leer las últimas n líneas de un archivo muy grande sin leer todo el archivo en ningún área de almacenamiento intermedio/memoria usando Java.Java: lea las últimas n líneas de un archivo ENORME

Miré alrededor de las API de JDK y Apache Commons I/O y no puedo encontrar una que sea adecuada para este propósito.

Estaba pensando en la forma en que cola o menos lo hace en UNIX. No creo que carguen todo el archivo y luego muestran las últimas líneas del archivo. Debería haber una forma similar de hacer lo mismo en Java también.

+2

Consulte también: [Java: leer con rapidez la última línea de un archivo de texto?] (Http://stackoverflow.com/questions/686231) – hippietrail

Respuesta

25

Si usa RandomAccessFile, puede usar length y seek para llegar a un punto específico cerca del final del archivo y luego seguir adelante desde allí.

Si no hay suficientes líneas, realice una copia de seguridad desde ese punto y vuelva a intentarlo. Una vez que haya descubierto dónde comienza la última línea N, puede buscar allí y simplemente leer e imprimir.

Se puede hacer una suposición inicial mejor basada en sus propiedades de datos. Por ejemplo, si se trata de un archivo de texto, es posible que las longitudes de línea no excedan un promedio de 132, entonces, para obtener las últimas cinco líneas, comience 660 caracteres antes del final. Luego, si te equivocaste, vuelve a intentarlo en 1320 (incluso puedes usar lo que aprendiste de los últimos 660 caracteres para ajustarlo, por ejemplo: si esos 660 caracteres son solo tres líneas, el siguiente intento podría ser 660/3 * 5, más tal vez un poco más por si acaso).

1

A RandomAccessFile permite buscar (http://download.oracle.com/javase/1.4.2/docs/api/java/io/RandomAccessFile.html). El método File.length devolverá el tamaño del archivo. El problema es determinar el número de líneas. Para esto, puede buscar hasta el final del archivo y leer hacia atrás hasta que haya alcanzado el número correcto de líneas.

18

RandomAccessFile es un buen lugar para comenzar, como se describe en las otras respuestas. Sin embargo, hay una advertencia importante .

Si su archivo no está codificado con una codificación de un byte por carácter, el método readLine() no le va a funcionar. Y readUTF() no funcionará bajo ninguna circunstancia. (Lee una cadena precedida de un recuento de caracteres ...)

En su lugar, deberá asegurarse de buscar marcadores de final de línea que respeten los límites de caracteres de la codificación. Para codificaciones de longitud fija (por ejemplo, sabores de UTF-16 o UTF-32), necesita extraer caracteres a partir de posiciones de bytes que son divisibles por el tamaño del carácter en bytes. Para codificaciones de longitud variable (por ejemplo, UTF-8), debe buscar un byte que debe ser el primer byte de un carácter.

En el caso de UTF-8, el primer byte de un carácter será 0xxxxxxx o 110xxxxx o 1110xxxx o 11110xxx. Cualquier otra cosa es un segundo/tercer byte o una secuencia UTF-8 ilegal. Ver The Unicode Standard, Version 5.2, Chapter 3.9, Tabla 3-7. Esto significa, como lo señala la discusión de comentarios, que cualquier byte 0x0A y 0x0D en una secuencia UTF-8 codificada adecuadamente representará un carácter LF o CR. Por lo tanto, contar los bytes es una estrategia de implementación válida (para UTF-8).

Después de haber identificado un límite de caracteres adecuado, puede simplemente llamar al new String(...) pasando la matriz de bytes, desplazamiento, recuento y codificación, y luego llamar repetidamente al String.lastIndexOf(...) para contar el final de las líneas.

+1

1 por mencionar la salvedad. Creo que para UTF-8 el problema puede simplificarse buscando '\ n' ... Al menos eso es lo que Jon Skeet parece implicar en su respuesta a una [pregunta relacionada] (http://stackoverflow.com/ preguntas/686231/quick-read-the-last-line-of-a-text-file) ... Parece que '\ n' solo puede aparecer como un carácter válido en UTF-8 y nunca en 'bytes adicionales'. .. –

+0

Sí, para UTF-8 es simple. UTF-8 codifica los caracteres como un solo byte (todos los caracteres ASCII) o como múltiples bytes (todos los demás caracteres Unicode). Afortunadamente para nosotros, newline es un carácter ASCII y en UTF-8, ningún carácter de múltiples bytes contiene bytes que también son caracteres ASCII válidos. Es decir, si escanea una matriz de bytes para la línea nueva ASCII y la encuentra, usted * sabe * es una línea nueva y no parte de ningún otro carácter de múltiples bytes. Escribí una [publicación de blog] (http://stijndewitt.wordpress.com/2014/08/09/max-bytes-in-a-utf-8-char/) que tiene una buena tabla que ilustra esto. –

+0

El problema es 1) codificaciones de caracteres donde el byte '0x0a' no es una nueva línea (por ejemplo, UTF-16), y 2) el hecho de que hay otros puntos de código separadores de línea Unicode; p.ej. '0x2028',' 0x2029' y '0x0085' –

0

Esta es la mejor manera que he encontrado para hacerlo.Simple y bastante rápido y eficiente con la memoria.

public static void tail(File src, OutputStream out, int maxLines) throws FileNotFoundException, IOException { 
    BufferedReader reader = new BufferedReader(new FileReader(src)); 
    String[] lines = new String[maxLines]; 
    int lastNdx = 0; 
    for (String line=reader.readLine(); line != null; line=reader.readLine()) { 
     if (lastNdx == lines.length) { 
      lastNdx = 0; 
     } 
     lines[lastNdx++] = line; 
    } 

    OutputStreamWriter writer = new OutputStreamWriter(out); 
    for (int ndx=lastNdx; ndx != lastNdx-1; ndx++) { 
     if (ndx == lines.length) { 
      ndx = 0; 
     } 
     writer.write(lines[ndx]); 
     writer.write("\n"); 
    } 

    writer.flush(); 
} 
+6

Dado que esto se lee a través del archivo completo, esto no se escalaría tan bien con archivos más grandes. – ChristopheD

+0

Además, esta función entra en un bucle sin fin para los archivos vacíos. – shak

+0

¿Por qué debería recorrer un archivo vacío? –

2

CircularFifoBuffer de apache commons. respuesta de una pregunta similar a How to read last 5 lines of a .txt file into java

Nótese que en Apache Commons Colecciones 4 esta clase parece haber sido renombrado a CircularFifoQueue

+0

Comprobé la clase que mencionaste, y aunque realmente se puede usar para hacer un seguimiento de las últimas 5 líneas en un archivo, creo que el desafío aquí no es hacer un seguimiento de las líneas, sino encontrar el punto en el archivo dónde comenzar a leer, y cómo llegar a ese punto. –

3

encontré RandomAccessFile y otras clases de búfer Reader demasiado lento para mí. Nada puede ser más rápido que tail -<#lines>. Esta fue la mejor solución para mí.

public String getLastNLogLines(File file, int nLines) { 
    StringBuilder s = new StringBuilder(); 
    try { 
     Process p = Runtime.getRuntime().exec("tail -"+nLines+" "+file); 
     java.io.BufferedReader input = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream())); 
     String line = null; 
    //Here we first read the next line into the variable 
    //line and then check for the EOF condition, which 
    //is the return value of null 
    while((line = input.readLine()) != null){ 
      s.append(line+'\n'); 
     } 
    } catch (java.io.IOException e) { 
     e.printStackTrace(); 
    } 
    return s.toString(); 
} 
+5

Ejecutar a 'tail' puede ser una propuesta muy costosa en sí misma, dependiendo de la cantidad de memoria que tenga. Y también es específico de Unix. – Gray

21

me pareció la forma más sencilla de hacerlo mediante el uso de ReversedLinesFileReaderapache commons-io API. Este método le dará la línea de abajo a arriba de un archivo y puede especificar el valor n_lines para especificar el número de línea.

import org.apache.commons.io.input.ReversedLinesFileReader; 


File file = new File("D:\\file_name.xml"); 
int n_lines = 10; 
int counter = 0; 
ReversedLinesFileReader object = new ReversedLinesFileReader(file); 
while(!object.readLine().isEmpty() && counter < n_lines) 
{ 
    System.out.println(object.readLine()); 
    counter++; 
} 
+5

Precaución: Cada vez que llame a 'readLine()', el cursor avanza.Por lo tanto, este código realmente se perderá cada dos líneas porque la salida de 'readLine()' en la declaración 'while' no se está capturando. – aapierce

+0

Me pregunto si este método es eficiente o no. – Forrest

+2

Este código es un poco defectuoso porque readLine() se llama dos veces. como lo menciona aapierce. Pero puntos completos para ReversedLinesFileReader – vinksharma

-1
int n_lines = 1000; 
    ReversedLinesFileReader object = new ReversedLinesFileReader(new File(path)); 
    String result=""; 
    for(int i=0;i<n_lines;i++){ 
     String line=object.readLine(); 
     if(line==null) 
      break; 
     result+=line; 
    } 
    return result; 
0

tuve un problema similar, pero yo no entendía a otras soluciones.

Utilicé esto. Espero que sea un código simple.

// String filePathName = (direction and file name). 
File f = new File(filePathName); 
long fileLength = f.length(); // Take size of file [bites]. 
long fileLength_toRead = 0; 
if (fileLength > 2000) { 
    // My file content is a table, I know one row has about e.g. 100 bites/characters. 
    // I used 1000 bites before file end to point where start read. 
    // If you don't know line length, use @paxdiablo advice. 
    fileLength_toRead = fileLength - 1000; 
} 
try (RandomAccessFile raf = new RandomAccessFile(filePathName, "r")) { // This row manage open and close file. 
    raf.seek(fileLength_toRead); // File will begin read at this bite. 
    String rowInFile = raf.readLine(); // First readed line usualy is not whole, I needn't it. 
    rowInFile = raf.readLine(); 
    while (rowInFile != null) { 
     // Here I can readed lines (rowInFile) add to String[] array or ArriyList<String>. 
     // Later I can work with rows from array - last row is sometimes empty, etc. 
     rowInFile = raf.readLine(); 
    } 
} 
catch (IOException e) { 
    // 
} 
Cuestiones relacionadas