Numero di righe in un file in Java


213

Uso enormi file di dati, a volte ho solo bisogno di conoscere il numero di righe in questi file, di solito li apro e li leggo riga per riga fino a raggiungere la fine del file

Mi chiedevo se esiste un modo più intelligente per farlo

Risposte:


237

Questa è la versione più veloce che ho trovato finora, circa 6 volte più veloce di readLines. Su un file di registro da 150 MB, sono necessari 0,35 secondi, rispetto a 2,40 secondi quando si utilizza readLines (). Per divertimento, il comando wc -l di linux richiede 0,15 secondi.

public static int countLinesOld(String filename) throws IOException {
    InputStream is = new BufferedInputStream(new FileInputStream(filename));
    try {
        byte[] c = new byte[1024];
        int count = 0;
        int readChars = 0;
        boolean empty = true;
        while ((readChars = is.read(c)) != -1) {
            empty = false;
            for (int i = 0; i < readChars; ++i) {
                if (c[i] == '\n') {
                    ++count;
                }
            }
        }
        return (count == 0 && !empty) ? 1 : count;
    } finally {
        is.close();
    }
}

EDIT, 9 anni e mezzo dopo: non ho praticamente nessuna esperienza Java, ma comunque ho cercato di confrontare questo codice con la LineNumberReadersoluzione di seguito poiché mi ha dato fastidio che nessuno l'abbia fatto. Sembra che soprattutto per file di grandi dimensioni la mia soluzione sia più veloce. Anche se sembra richiedere alcune correzioni fino a quando l'ottimizzatore non fa un lavoro decente. Ho giocato un po 'con il codice e ho prodotto una nuova versione che è costantemente più veloce:

public static int countLinesNew(String filename) throws IOException {
    InputStream is = new BufferedInputStream(new FileInputStream(filename));
    try {
        byte[] c = new byte[1024];

        int readChars = is.read(c);
        if (readChars == -1) {
            // bail out if nothing to read
            return 0;
        }

        // make it easy for the optimizer to tune this loop
        int count = 0;
        while (readChars == 1024) {
            for (int i=0; i<1024;) {
                if (c[i++] == '\n') {
                    ++count;
                }
            }
            readChars = is.read(c);
        }

        // count remaining characters
        while (readChars != -1) {
            System.out.println(readChars);
            for (int i=0; i<readChars; ++i) {
                if (c[i] == '\n') {
                    ++count;
                }
            }
            readChars = is.read(c);
        }

        return count == 0 ? 1 : count;
    } finally {
        is.close();
    }
}

Risultati benchmark per un file di testo da 1,3 GB, asse y in secondi. Ho eseguito 100 corse con lo stesso file e misurato ogni corsa con System.nanoTime(). Puoi vedere che countLinesOldha alcuni valori anomali e countLinesNewnon ne ha e, sebbene sia solo un po 'più veloce, la differenza è statisticamente significativa. LineNumberReaderè chiaramente più lento.

Grafico di riferimento


5
BufferedInputStream dovrebbe fare il buffering per te, quindi non vedo come l'utilizzo di un array di byte intermedi [] lo renderà più veloce. È improbabile che tu faccia molto meglio che usare ripetutamente readLine () (poiché ciò sarà ottimizzato dall'API).
wds,

54
Chiuderai quel InputStream quando avrai finito, vero?
bendin,

5
Se il buffering fosse di aiuto sarebbe perché BufferedInputStream buffer 8K per impostazione predefinita. Aumenta il tuo byte [] a questa dimensione o più grande e puoi eliminare BufferedInputStream. ad esempio, provare 1024 * 1024 byte.
Peter Lawrey,

8
Due cose: (1) La definizione di un terminatore di riga nell'origine Java è un ritorno a capo, un avanzamento a capo o un ritorno a capo seguito da un avanzamento a capo. La tua soluzione non funzionerà per CR usato come terminatore di linea. Certo, l'unico sistema operativo di cui posso pensare che utilizza CR come terminatore di linea predefinito è Mac OS precedente a Mac OS X. (2) La tua soluzione assume una codifica di caratteri come US-ASCII o UTF-8. Il conteggio delle righe potrebbe non essere accurato per codifiche come UTF-16.
Nathan Ryan,

2
Codice fantastico ... per file di testo da 400 MB, ci è voluto solo un secondo. Grazie mille @martinus
user3181500

199

Ho implementato un'altra soluzione al problema, l'ho trovato più efficiente nel conteggio delle righe:

try
(
   FileReader       input = new FileReader("input.txt");
   LineNumberReader count = new LineNumberReader(input);
)
{
   while (count.skip(Long.MAX_VALUE) > 0)
   {
      // Loop just in case the file is > Long.MAX_VALUE or skip() decides to not read the entire file
   }

   result = count.getLineNumber() + 1;                                    // +1 because line index starts at 0
}

LineNumberReaderIl lineNumbercampo è un numero intero ... Non è sufficiente per i file più lunghi di Integer.MAX_VALUE? Perché preoccuparsi di saltare a lungo qui?
epb,

1
L'aggiunta di uno al conteggio non è effettivamente corretto. wc -lconta il numero di caratteri newline nel file. Funziona poiché ogni riga è terminata con una nuova riga, inclusa la riga finale in un file. Ogni riga ha un carattere di nuova riga, comprese le righe vuote, quindi il numero di caratteri di nuova riga == numero di righe in un file. Ora, la lineNumbervariabile in FileNumberReaderrappresenta anche il numero di caratteri newline visti. Inizia da zero, prima che sia stata trovata una nuova riga, e viene aumentata con ogni carattere newline visto. Quindi non aggiungerne uno al numero di riga, per favore.
Alexander Torstling,

1
@PB_MLT: Anche se hai ragione a dire che un file con una sola riga senza newline verrebbe segnalato come 0 righe, è così che wc -lriporta anche questo tipo di file. Vedere anche stackoverflow.com/questions/729692/...
Alexander Torstling

@PB_MLT: ottieni il problema opposto se il file è costituito esclusivamente da una nuova riga. Il tuo algoritmo suggerito restituirebbe 0 e wc -lrestituirebbe 1. Ho concluso che tutti i metodi hanno dei difetti e ne ho implementato uno in base al modo in cui vorrei che si comportasse, vedi qui la mia altra risposta.
Alexander Torstling,

3
Ho votato in giù questa risposta, perché sembra che nessuno di voi l'abbia
testata

30

La risposta accettata ha un errore di un errore per i file multilinea che non terminano con la nuova riga. Un file di una riga che termina senza una nuova riga restituisce 1, ma anche un file di due righe che termina senza una nuova riga restituisce 1. Ecco un'implementazione della soluzione accettata che risolve questo problema. Il fine senza controlli NewLine sono dispendiosi per tutto tranne che per la lettura finale, ma dovrebbe essere banale in termini di tempo rispetto alla funzione generale.

public int count(String filename) throws IOException {
    InputStream is = new BufferedInputStream(new FileInputStream(filename));
    try {
        byte[] c = new byte[1024];
        int count = 0;
        int readChars = 0;
        boolean endsWithoutNewLine = false;
        while ((readChars = is.read(c)) != -1) {
            for (int i = 0; i < readChars; ++i) {
                if (c[i] == '\n')
                    ++count;
            }
            endsWithoutNewLine = (c[readChars - 1] != '\n');
        }
        if(endsWithoutNewLine) {
            ++count;
        } 
        return count;
    } finally {
        is.close();
    }
}

6
Buona pesca. Non sono sicuro del motivo per cui non hai semplicemente modificato la risposta accettata e hai preso nota in un commento. Molte persone non leggeranno così lontano.
Ryan,

@Ryan, non era giusto modificare una risposta accettata di 4 anni con oltre 90 voti.
DMulligan,

@AFinkelstein, penso che sia ciò che rende questo sito così eccezionale, che puoi modificare la risposta più votata.
Sebastian,

3
Questa soluzione non gestisce il ritorno a capo (\ r) e il ritorno a capo seguito da un avanzamento riga (\ r \ n)
Simon Brandhof - Sonar Fonte

@Simon Brandhof, sono confuso sul perché un ritorno in carrozza verrebbe conteggiato come un'altra riga? Un "\ n" è un feed della riga di ritorno del carrello, quindi chiunque scriva "\ r \ n" non sta capendo qualcosa ... Inoltre sta cercando char con char, quindi sono abbastanza sicuro che qualcuno debba usare "\ r \ n "catturerebbe comunque" \ n "e conteggerà la linea. Ad ogni modo, penso che abbia chiarito bene il punto. Tuttavia, ci sono molti scenari in cui questo non è un modo sufficiente per ottenere un conteggio delle righe.
nckbrz,

22

Con , puoi usare gli stream:

try (Stream<String> lines = Files.lines(path, Charset.defaultCharset())) {
  long numOfLines = lines.count();
  ...
}

1
Il codice ha errori. Semplice, ma molto lento ... Prova a guardare la mia risposta di seguito (sopra).
Ernestas Gruodis,

12

La risposta con il metodo count () sopra mi ha dato errori di riga se un file non aveva una nuova riga alla fine del file - non è riuscito a contare l'ultima riga nel file.

Questo metodo funziona meglio per me:

public int countLines(String filename) throws IOException {
    LineNumberReader reader  = new LineNumberReader(new FileReader(filename));
int cnt = 0;
String lineRead = "";
while ((lineRead = reader.readLine()) != null) {}

cnt = reader.getLineNumber(); 
reader.close();
return cnt;
}

In questo caso, non è necessario utilizzare LineNumberReader, utilizzare semplicemente BufferedReader, in tal caso si avrà la flessibilità di utilizzare un tipo di dati lungo per cnt.
Syed Aqeel Ashiq,

[INFO] Errore PMD: xx: 19 Regola: EmptyWhileStmt Priorità: 3 Evitare istruzioni while vuote.
Chhorn Elit

8

So che questa è una vecchia domanda, ma la soluzione accettata non corrispondeva esattamente a ciò di cui avevo bisogno. Quindi, l'ho perfezionato per accettare vari terminatori di riga (anziché solo avanzamento di riga) e utilizzare una codifica di caratteri specificata (anziché ISO-8859- n ). Metodo tutto in uno (refactor a seconda dei casi):

public static long getLinesCount(String fileName, String encodingName) throws IOException {
    long linesCount = 0;
    File file = new File(fileName);
    FileInputStream fileIn = new FileInputStream(file);
    try {
        Charset encoding = Charset.forName(encodingName);
        Reader fileReader = new InputStreamReader(fileIn, encoding);
        int bufferSize = 4096;
        Reader reader = new BufferedReader(fileReader, bufferSize);
        char[] buffer = new char[bufferSize];
        int prevChar = -1;
        int readCount = reader.read(buffer);
        while (readCount != -1) {
            for (int i = 0; i < readCount; i++) {
                int nextChar = buffer[i];
                switch (nextChar) {
                    case '\r': {
                        // The current line is terminated by a carriage return or by a carriage return immediately followed by a line feed.
                        linesCount++;
                        break;
                    }
                    case '\n': {
                        if (prevChar == '\r') {
                            // The current line is terminated by a carriage return immediately followed by a line feed.
                            // The line has already been counted.
                        } else {
                            // The current line is terminated by a line feed.
                            linesCount++;
                        }
                        break;
                    }
                }
                prevChar = nextChar;
            }
            readCount = reader.read(buffer);
        }
        if (prevCh != -1) {
            switch (prevCh) {
                case '\r':
                case '\n': {
                    // The last line is terminated by a line terminator.
                    // The last line has already been counted.
                    break;
                }
                default: {
                    // The last line is terminated by end-of-file.
                    linesCount++;
                }
            }
        }
    } finally {
        fileIn.close();
    }
    return linesCount;
}

Questa soluzione è paragonabile in termini di velocità alla soluzione accettata, circa il 4% più lenta nei miei test (sebbene i test di cronometraggio in Java siano notoriamente inaffidabili).


8

Ho testato i metodi di cui sopra per contare le righe e qui ci sono le mie osservazioni per i metodi diversi testati sul mio sistema

Dimensione file: 1.6 Gb Metodi:

  1. Utilizzo dello scanner : circa 35 secondi
  2. Utilizzo di BufferedReader : circa 5 secondi
  3. Utilizzo di Java 8 : 5s circa
  4. Utilizzando LineNumberReader : circa 5 secondi

Inoltre Java8 Approach sembra abbastanza utile:

Files.lines(Paths.get(filePath), Charset.defaultCharset()).count()
[Return type : long]

5
/**
 * Count file rows.
 *
 * @param file file
 * @return file row count
 * @throws IOException
 */
public static long getLineCount(File file) throws IOException {

    try (Stream<String> lines = Files.lines(file.toPath())) {
        return lines.count();
    }
}

Testato su JDK8_u31. Ma in effetti le prestazioni sono lente rispetto a questo metodo:

/**
 * Count file rows.
 *
 * @param file file
 * @return file row count
 * @throws IOException
 */
public static long getLineCount(File file) throws IOException {

    try (BufferedInputStream is = new BufferedInputStream(new FileInputStream(file), 1024)) {

        byte[] c = new byte[1024];
        boolean empty = true,
                lastEmpty = false;
        long count = 0;
        int read;
        while ((read = is.read(c)) != -1) {
            for (int i = 0; i < read; i++) {
                if (c[i] == '\n') {
                    count++;
                    lastEmpty = true;
                } else if (lastEmpty) {
                    lastEmpty = false;
                }
            }
            empty = false;
        }

        if (!empty) {
            if (count == 0) {
                count = 1;
            } else if (!lastEmpty) {
                count++;
            }
        }

        return count;
    }
}

Testato e molto veloce.


Questo non è corretto Hai fatto alcuni esperimenti con il tuo codice e il metodo è sempre più lento. Stream<String> - Time consumed: 122796351 Stream<String> - Num lines: 109808 Method - Time consumed: 12838000 Method - Num lines: 1E anche il numero di righe è sbagliato
aw-think,

Ho testato su una macchina a 32 bit. Forse a 64 bit sarebbero risultati diversi .. Ed è stata la differenza 10 volte o più come ricordo. Potresti pubblicare il testo per contare la linea da qualche parte? È possibile utilizzare Notepad2 per visualizzare le interruzioni di riga per comodità.
Ernestas Gruodis,

Questa potrebbe essere la differenza.
aw-think,

Se ti preoccupi delle prestazioni, non dovresti usare a BufferedInputStreamquando hai intenzione di leggere nel tuo buffer comunque. Inoltre, anche se il tuo metodo potrebbe avere un leggero vantaggio in termini di prestazioni, perde flessibilità, poiché non supporta più i \rterminatori a linea singola (vecchio MacOS) e non supporta tutte le codifiche.
Holger,

4

Un modo semplice con Scanner

static void lineCounter (String path) throws IOException {

        int lineCount = 0, commentsCount = 0;

        Scanner input = new Scanner(new File(path));
        while (input.hasNextLine()) {
            String data = input.nextLine();

            if (data.startsWith("//")) commentsCount++;

            lineCount++;
        }

        System.out.println("Line Count: " + lineCount + "\t Comments Count: " + commentsCount);
    }

3

Ho concluso che wc -l: il metodo di conteggio delle nuove righe va bene ma restituisce risultati non intuitivi su file in cui l'ultima riga non termina con una nuova riga.

E la soluzione @ er.vikas basata su LineNumberReader ma l'aggiunta di uno al conteggio delle righe ha restituito risultati non intuitivi sui file in cui l'ultima riga termina con newline.

Ho quindi creato un algo che gestisce come segue:

@Test
public void empty() throws IOException {
    assertEquals(0, count(""));
}

@Test
public void singleNewline() throws IOException {
    assertEquals(1, count("\n"));
}

@Test
public void dataWithoutNewline() throws IOException {
    assertEquals(1, count("one"));
}

@Test
public void oneCompleteLine() throws IOException {
    assertEquals(1, count("one\n"));
}

@Test
public void twoCompleteLines() throws IOException {
    assertEquals(2, count("one\ntwo\n"));
}

@Test
public void twoLinesWithoutNewlineAtEnd() throws IOException {
    assertEquals(2, count("one\ntwo"));
}

@Test
public void aFewLines() throws IOException {
    assertEquals(5, count("one\ntwo\nthree\nfour\nfive\n"));
}

E sembra così:

static long countLines(InputStream is) throws IOException {
    try(LineNumberReader lnr = new LineNumberReader(new InputStreamReader(is))) {
        char[] buf = new char[8192];
        int n, previousN = -1;
        //Read will return at least one byte, no need to buffer more
        while((n = lnr.read(buf)) != -1) {
            previousN = n;
        }
        int ln = lnr.getLineNumber();
        if (previousN == -1) {
            //No data read at all, i.e file was empty
            return 0;
        } else {
            char lastChar = buf[previousN - 1];
            if (lastChar == '\n' || lastChar == '\r') {
                //Ending with newline, deduct one
                return ln;
            }
        }
        //normal case, return line number + 1
        return ln + 1;
    }
}

Se vuoi risultati intuitivi, puoi usarlo. Se vuoi solo la wc -lcompatibilità, usa semplicemente la soluzione @ er.vikas, ma non aggiungerne una al risultato e riprovare a saltare:

try(LineNumberReader lnr = new LineNumberReader(new FileReader(new File("File1")))) {
    while(lnr.skip(Long.MAX_VALUE) > 0){};
    return lnr.getLineNumber();
}

2

Che ne dici di usare la classe Process dal codice Java? E poi leggendo l'output del comando.

Process p = Runtime.getRuntime().exec("wc -l " + yourfilename);
p.waitFor();

BufferedReader b = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
int lineCount = 0;
while ((line = b.readLine()) != null) {
    System.out.println(line);
    lineCount = Integer.parseInt(line);
}

Devo provarlo però. Pubblicherà i risultati.


1

Se non si dispone di alcuna struttura di indice, non si potrà evitare la lettura del file completo. Ma puoi ottimizzarlo evitando di leggerlo riga per riga e utilizzare un regex per abbinare tutti i terminatori di riga.


Sembra un'idea chiara. Qualcuno l'ha provato e ha una regexp per questo?
Willcodejavaforfood,

1
Dubito che sia una buona idea: dovrà leggere l'intero file in una sola volta (martinus lo evita) e le regex sono eccessive (e più lente) per tale utilizzo (semplice ricerca di caratteri fissi).
PhiLho,

@will: che dire di / \ n /? @PhiLo: gli Executors Regex sono macchine ad alte prestazioni ottimizzate. Tranne l'avvertenza di leggere tutto in memoria, non credo che un'implementazione manuale possa essere più veloce.
David Schmitt,

1

Questa soluzione divertente funziona davvero molto bene!

public static int countLines(File input) throws IOException {
    try (InputStream is = new FileInputStream(input)) {
        int count = 1;
        for (int aChar = 0; aChar != -1;aChar = is.read())
            count += aChar == '\n' ? 1 : 0;
        return count;
    }
}

0

Sui sistemi basati su Unix, utilizzare il wccomando dalla riga di comando.


@IainmH, il tuo secondo suggerimento conta solo il numero di voci nella directory corrente. Non è quello che era previsto? (o richiesto dall'OP)
L'archetipo Paul,

@IainMH: questo è ciò che fa comunque wc (lettura del file, conteggio del finale di riga).
PhiLho,

@PhiLho Dovresti usare l'opzione -l per contare le linee. (Non è vero? - È passato un po 'di tempo)
Iain Holder,

@Paul - hai ovviamente ragione al 100%. La mia unica difesa è che l'ho pubblicato prima del mio caffè. Sono affilato come un pulsante ora. : D
Iain Holder

0

L'unico modo per sapere quante righe ci sono nel file è contarle. Puoi ovviamente creare una metrica dai tuoi dati che ti dia una lunghezza media di una riga e quindi ottenere la dimensione del file e dividerla con avg. lunghezza ma non sarà preciso.


1
Interessante downvote, indipendentemente dallo strumento da riga di comando che stai usando, FANNO TUTTA LA STESSA COSA, solo internamente. Non esiste un modo magico per capire il numero di righe, devono essere contate a mano. Certo, può essere salvato come metadata ma è tutta un'altra storia ...
Esko,

0

Miglior codice ottimizzato per file a più righe senza carattere di nuova riga ('\ n') in EOF.

/**
 * 
 * @param filename
 * @return
 * @throws IOException
 */
public static int countLines(String filename) throws IOException {
    int count = 0;
    boolean empty = true;
    FileInputStream fis = null;
    InputStream is = null;
    try {
        fis = new FileInputStream(filename);
        is = new BufferedInputStream(fis);
        byte[] c = new byte[1024];
        int readChars = 0;
        boolean isLine = false;
        while ((readChars = is.read(c)) != -1) {
            empty = false;
            for (int i = 0; i < readChars; ++i) {
                if ( c[i] == '\n' ) {
                    isLine = false;
                    ++count;
                }else if(!isLine && c[i] != '\n' && c[i] != '\r'){   //Case to handle line count where no New Line character present at EOF
                    isLine = true;
                }
            }
        }
        if(isLine){
            ++count;
        }
    }catch(IOException e){
        e.printStackTrace();
    }finally {
        if(is != null){
            is.close();    
        }
        if(fis != null){
            fis.close();    
        }
    }
    LOG.info("count: "+count);
    return (count == 0 && !empty) ? 1 : count;
}

0

Scanner con regex:

public int getLineCount() {
    Scanner fileScanner = null;
    int lineCount = 0;
    Pattern lineEndPattern = Pattern.compile("(?m)$");  
    try {
        fileScanner = new Scanner(new File(filename)).useDelimiter(lineEndPattern);
        while (fileScanner.hasNext()) {
            fileScanner.next();
            ++lineCount;
        }   
    }catch(FileNotFoundException e) {
        e.printStackTrace();
        return lineCount;
    }
    fileScanner.close();
    return lineCount;
}

Non l'ho cronometrato.


-2

se lo usi

public int countLines(String filename) throws IOException {
    LineNumberReader reader  = new LineNumberReader(new FileReader(filename));
    int cnt = 0;
    String lineRead = "";
    while ((lineRead = reader.readLine()) != null) {}

    cnt = reader.getLineNumber(); 
    reader.close();
    return cnt;
}

non puoi correre su grandi numeri, ti piacciono le 100.000 righe, perché il ritorno da reader.getLineNumber è int. hai bisogno di un lungo tipo di dati per elaborare le righe massime.


14
Un intpuò contenere valori fino a circa 2 miliardi di euro. Se stai caricando un file con più di 2 miliardi di righe, hai un problema di overflow. Detto questo, se stai caricando un file di testo non indicizzato con più di due miliardi di righe, probabilmente hai altri problemi.
Adam Norberg,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.