Nombre de lignes dans un fichier en Java


213

J'utilise d'énormes fichiers de données, parfois j'ai seulement besoin de connaître le nombre de lignes dans ces fichiers, généralement je les ouvre et les lis ligne par ligne jusqu'à ce que j'atteigne la fin du fichier

Je me demandais s'il y avait un moyen plus intelligent de le faire

Réponses:


237

C'est la version la plus rapide que j'ai trouvée jusqu'à présent, environ 6 fois plus rapide que readLines. Sur un fichier journal de 150 Mo, cela prend 0,35 seconde, contre 2,40 secondes lors de l'utilisation de readLines (). Juste pour le plaisir, la commande wc -l de linux prend 0,15 seconde.

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 1/2 ans plus tard: je n'ai pratiquement aucune expérience java, mais de toute façon j'ai essayé de comparer ce code avec la LineNumberReadersolution ci-dessous car cela me dérangeait que personne ne le fasse. Il semble que, surtout pour les fichiers volumineux, ma solution est plus rapide. Bien que cela semble prendre quelques exécutions jusqu'à ce que l'optimiseur fasse un travail décent. J'ai joué un peu avec le code et j'ai produit une nouvelle version qui est toujours la plus rapide:

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();
    }
}

Benchmark resuls pour un fichier texte de 1,3 Go, axe y en secondes. J'ai effectué 100 analyses avec le même fichier et mesuré chaque analyse avec System.nanoTime(). Vous pouvez voir qu'il countLinesOlda quelques valeurs aberrantes et countLinesNewn'en a aucune et bien que ce soit seulement un peu plus rapide, la différence est statistiquement significative. LineNumberReaderest clairement plus lent.

Graphique de référence


5
BufferedInputStream devrait faire la mise en mémoire tampon pour vous, donc je ne vois pas comment l'utilisation d'un tableau d'octets intermédiaires [] le rendra plus rapide. De toute façon, il est peu probable que vous fassiez beaucoup mieux que d'utiliser readLine () (car cela sera optimisé par l'API).
wds

54
Vous allez fermer ce InputStream lorsque vous en aurez terminé, n'est-ce pas?
bendin

5
Si la mise en mémoire tampon a aidé, ce serait parce que BufferedInputStream tamponne 8 Ko par défaut. Augmentez votre octet [] à cette taille ou plus et vous pouvez supprimer le BufferedInputStream. par exemple, essayez 1024 * 1024 octets.
Peter Lawrey

8
Deux choses: (1) La définition d'un terminateur de ligne dans la source Java est un retour chariot, un saut de ligne ou un retour chariot suivi d'un saut de ligne. Votre solution ne fonctionnera pas pour CR utilisé comme terminateur de ligne. Certes, le seul système d'exploitation dont je pense pouvoir utiliser CR comme terminateur de ligne par défaut est Mac OS avant Mac OS X. (2) Votre solution suppose un codage de caractères tel que US-ASCII ou UTF-8. Le nombre de lignes peut être inexact pour des encodages tels que UTF-16.
Nathan Ryan

2
Code génial ... pour un fichier texte de 400 Mo, cela n'a pris qu'une seconde. Merci beaucoup @martinus
user3181500

199

J'ai implémenté une autre solution au problème, je l'ai trouvée plus efficace pour compter les lignes:

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
}

LineNumberReaderLe lineNumberchamp de est un entier ... Ne sera-t-il pas simplement encapsulé pour des fichiers plus longs que Integer.MAX_VALUE? Pourquoi prendre la peine de sauter longtemps ici?
epb

1
Ajouter un au nombre est en fait incorrect. wc -lcompte le nombre de caractères de nouvelle ligne dans le fichier. Cela fonctionne car chaque ligne se termine par une nouvelle ligne, y compris la dernière ligne d'un fichier. Chaque ligne a un caractère de nouvelle ligne, y compris les lignes vides, d'où le nombre de caractères de nouvelle ligne == nombre de lignes dans un fichier. Maintenant, la lineNumbervariable dans FileNumberReaderreprésente également le nombre de caractères de nouvelle ligne vus. Il commence à zéro, avant que toute nouvelle ligne ait été trouvée, et est augmenté à chaque fois que la nouvelle ligne est vue. N'ajoutez donc pas un au numéro de ligne s'il vous plaît.
Alexander Torstling

1
@PB_MLT: Bien que vous ayez raison de dire qu'un fichier avec une seule ligne sans retour à la ligne serait signalé comme 0 ligne, c'est wc -légalement ainsi que ce type de fichier est signalé. Voir aussi stackoverflow.com/questions/729692/…
Alexander Torstling

@PB_MLT: Vous obtenez le problème opposé si le fichier est uniquement composé d'une nouvelle ligne. Votre algo suggéré retournerait 0 et wc -lretournerait 1. J'ai conclu que toutes les méthodes ont des défauts, et en ai implémenté une basée sur la façon dont je voudrais qu'elle se comporte, voir mon autre réponse ici.
Alexander Torstling

3
J'ai voté contre cette réponse, car il semble qu'aucun d'entre vous ne l'ait comparée
amstegraf

30

La réponse acceptée a une erreur de désactivation d'une pour les fichiers multi-lignes qui ne se terminent pas par un retour à la ligne. Un fichier d'une ligne se terminant sans une nouvelle ligne retournerait 1, mais un fichier de deux lignes se terminant sans une nouvelle ligne retournerait également 1. Voici une implémentation de la solution acceptée qui corrige cela. Les contrôles endsWithoutNewLine sont inutiles pour tout sauf la lecture finale, mais devraient être triviaux en termes de temps par rapport à la fonction globale.

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
Bonne prise. Je ne sais pas pourquoi vous n'avez pas simplement modifié la réponse acceptée et notez-le dans un commentaire. La plupart des gens ne liront pas si loin.
Ryan

@Ryan, il ne semblait pas juste de modifier une réponse acceptée de 4 ans avec plus de 90 votes positifs.
DMulligan

@AFinkelstein, je pense que c'est ce qui rend ce site si génial, que vous pouvez modifier la réponse la plus votée.
Sebastian

3
Cette solution ne gère pas le retour chariot (\ r) et le retour chariot suivi d'un saut de ligne (\ r \ n)
Simon Brandhof - SonarSource

@Simon Brandhof, je ne comprends pas pourquoi un retour chariot serait considéré comme une autre ligne? Un "\ n" est un fil de ligne de retour chariot, donc celui qui écrit "\ r \ n" ne comprend pas quelque chose ... De plus, il recherche char par char, donc je suis presque sûr que quelqu'un doit utiliser "\ r \ n "il attraperait toujours le" \ n "et compterait la ligne. Quoi qu'il en soit, je pense qu'il a très bien fait le point. Cependant, il existe de nombreux scénarios où ce n'est pas un moyen suffisant pour obtenir un nombre de lignes.
nckbrz

22

Avec , vous pouvez utiliser des flux:

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

1
Le code contient des erreurs. Simple, mais très lent ... Essayez de regarder ma réponse ci-dessous (ci-dessus).
Ernestas Gruodis

12

La réponse avec la méthode count () ci-dessus m'a donné des erreurs de calcul si un fichier n'avait pas de nouvelle ligne à la fin du fichier - il n'a pas pu compter la dernière ligne du fichier.

Cette méthode fonctionne mieux pour moi:

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;
}

Dans ce cas, il n'est pas nécessaire d'utiliser LineNumberReader, utilisez simplement BufferedReader, dans ce cas, vous aurez la possibilité d'utiliser un type de données long pour cnt.
Syed Aqeel Ashiq

[INFO] Échec PMD: xx: 19 Règle: EmptyWhileStmt Priorité: 3 Évitez les instructions while vides.
Chhorn Elit

8

Je sais que c'est une vieille question, mais la solution acceptée ne correspondait pas tout à fait à ce dont j'avais besoin. Donc, je l'ai affiné pour accepter divers terminateurs de ligne (plutôt que juste un saut de ligne) et pour utiliser un codage de caractères spécifié (plutôt que ISO-8859- n ). Méthode tout en un (refactoriser le cas échéant):

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;
}

Cette solution est comparable en vitesse à la solution acceptée, environ 4% plus lente dans mes tests (bien que les tests de synchronisation en Java soient notoirement peu fiables).


8

J'ai testé les méthodes de comptage des lignes ci-dessus et voici mes observations pour différentes méthodes testées sur mon système

Taille du fichier: 1,6 Go Méthodes:

  1. Utilisation du scanner : 35 s environ
  2. Utilisation de BufferedReader : 5 s environ
  3. Utilisation de Java 8 : 5 s environ
  4. Utilisation de LineNumberReader : 5 s environ

De plus, l' approche Java8 semble assez pratique:

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();
    }
}

Testé sur JDK8_u31. Mais en effet les performances sont lentes par rapport à cette méthode:

/**
 * 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;
    }
}

Testé et très rapide.


Ce n'est pas correct. Faites quelques expériences avec votre code et la méthode est toujours plus lente. Stream<String> - Time consumed: 122796351 Stream<String> - Num lines: 109808 Method - Time consumed: 12838000 Method - Num lines: 1Et le nombre de lignes est même faux aussi
aw-think

J'ai testé sur une machine 32 bits. Peut-être que sur 64 bits, les résultats seraient différents. Et c'était la différence 10 fois ou plus si je me souviens bien. Pourriez-vous publier le texte pour compter la ligne quelque part? Vous pouvez utiliser le Bloc-notes2 pour voir les sauts de ligne pour plus de commodité.
Ernestas Gruodis

Ça pourrait être la différence.
aw-think

Si vous vous souciez des performances, vous ne devriez pas utiliser BufferedInputStreamquand vous allez lire dans votre propre tampon. En outre, même si votre méthode peut avoir un léger avantage en termes de performances, elle perd de sa flexibilité, car elle ne prend plus en charge les \rterminateurs de ligne unique (ancien MacOS) et ne prend pas en charge tous les encodages.
Holger

4

Une manière simple d'utiliser 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

J'ai conclu que wc -l: s la méthode de comptage des sauts de ligne est correcte mais renvoie des résultats non intuitifs sur les fichiers où la dernière ligne ne se termine pas par une nouvelle ligne.

Et la solution @ er.vikas basée sur LineNumberReader mais en ajoutant un au nombre de lignes a renvoyé des résultats non intuitifs sur les fichiers où la dernière ligne se termine par un retour à la ligne.

J'ai donc fait un algo qui gère comme suit:

@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"));
}

Et cela ressemble à ceci:

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;
    }
}

Si vous voulez des résultats intuitifs, vous pouvez l'utiliser. Si vous voulez juste de la wc -lcompatibilité, utilisez simplement la solution @ er.vikas, mais n'en ajoutez pas une au résultat et réessayez de sauter:

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

2

Que diriez-vous d'utiliser la classe Process à partir du code Java? Et puis lire la sortie de la commande.

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);
}

Besoin d'essayer cependant. Publiera les résultats.


1

Si vous ne disposez d'aucune structure d'index, vous ne pourrez pas contourner la lecture du fichier complet. Mais vous pouvez l'optimiser en évitant de le lire ligne par ligne et d'utiliser une expression régulière pour faire correspondre tous les terminateurs de ligne.


Cela ressemble à une bonne idée. Quelqu'un l'a essayé et a une expression rationnelle pour cela?
willcodejavaforfood

1
Je doute que ce soit une si bonne idée: il faudra lire tout le fichier à la fois (martinus évite cela) et les regex sont exagérés (et plus lents) pour une telle utilisation (simple recherche de caractères fixes).
PhiLho

@will: qu'en est-il de / \ n /? @PhiLo: Les exécuteurs Regex sont des machines de performance hautement réglées. À l'exception de la mise en garde de tout lire en mémoire, je ne pense pas qu'une implémentation manuelle puisse être plus rapide.
David Schmitt

1

Cette solution amusante fonctionne vraiment bien en fait!

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

Sur les systèmes basés sur Unix, utilisez la wccommande sur la ligne de commande.


@IainmH, votre deuxième suggestion ne compte que le nombre d'entrées dans le répertoire actuel. Pas ce qui était prévu? (ou demandé par le PO)
L'Archétype Paul

@IainMH: c'est ce que wc fait quand même (lire le fichier, compter la fin de ligne).
PhiLho

@PhiLho Vous devez utiliser le commutateur -l pour compter les lignes. (N'est-ce pas? - ça fait un moment)
Iain Holder

@Paul - vous avez bien sûr 100% raison. Ma seule défense est que j'ai posté ça avant mon café. Je suis aussi tranchant qu'un bouton maintenant. : D
Iain Holder

0

Le seul moyen de savoir combien de lignes il y a dans le fichier est de les compter. Vous pouvez bien sûr créer une métrique à partir de vos données en vous donnant une longueur moyenne d'une ligne, puis obtenir la taille du fichier et la diviser avec avg. longueur mais ce ne sera pas précis.


1
Downvote intéressant, quel que soit l'outil de ligne de commande que vous utilisez, ils font tous la même chose de toute façon, uniquement en interne. Il n'y a aucun moyen magique de déterminer le nombre de lignes, elles doivent être comptées à la main. Bien sûr, il peut être enregistré en tant que métadonnées, mais c'est une toute autre histoire ...
Esko

0

Meilleur code optimisé pour les fichiers multi-lignes n'ayant pas de caractère de nouvelle ligne ('\ n') à l'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 avec 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;
}

Je ne l'ai pas chronométré.


-2

si vous l'utilisez

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;
}

vous ne pouvez pas exécuter de grandes lignes num, comme 100K lignes, car le retour de reader.getLineNumber est int. vous avez besoin d'un long type de données pour traiter un maximum de lignes.


14
Un intpeut contenir des valeurs allant jusqu'à environ 2 milliards de dollars. Si vous chargez un fichier de plus de 2 milliards de lignes, vous avez un problème de débordement. Cela dit, si vous chargez un fichier texte non indexé de plus de deux milliards de lignes, vous avez probablement d'autres problèmes.
Adam Norberg
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.