Liczba linii w pliku w Javie


213

Używam dużych plików danych, czasem muszę tylko znać liczbę wierszy w tych plikach, zwykle otwieram je i czytam wiersz po wierszu, aż dojdę do końca pliku

Zastanawiałem się, czy jest na to lepszy sposób

Odpowiedzi:


237

To najszybsza wersja, jaką do tej pory znalazłem, około 6 razy szybsza niż readLines. W przypadku pliku dziennika 150 MB zajmuje to 0,35 sekundy, w porównaniu do 2,40 sekundy w przypadku użycia readLines (). Dla zabawy polecenie wc -l linuksa zajmuje 0,15 sekundy.

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

EDYCJA, 9 1/2 lat później: praktycznie nie mam doświadczenia w Javie, ale tak czy inaczej próbowałem porównać ten kod z LineNumberReaderrozwiązaniem poniżej, ponieważ przeszkadzało mi, że nikt tego nie zrobił. Wydaje się, że szczególnie w przypadku dużych plików moje rozwiązanie jest szybsze. Chociaż wydaje się, że optymalizacja zajmuje sporo czasu. Grałem trochę z kodem i stworzyłem nową wersję, która jest konsekwentnie najszybsza:

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 uruchamia się dla pliku tekstowego 1,3 GB, oś y w sekundach. Wykonałem 100 przebiegów z tym samym plikiem i zmierzyłem każdy z nich System.nanoTime(). Widać, że countLinesOldma kilka wartości odstających i countLinesNewnie ma go, a chociaż jest tylko nieco szybszy, różnica jest istotna statystycznie. LineNumberReaderjest wyraźnie wolniejszy.

Wykres porównawczy


5
BufferedInputStream powinien robić dla ciebie buforowanie, więc nie rozumiem, jak użycie tablicy pośrednich bajtów [] przyspieszy. Zresztą raczej nie powinieneś robić nic lepszego niż wielokrotne używanie readLine () (ponieważ zostanie to zoptymalizowane przez API).
wds

54
Zamierzasz zamknąć ten InputStream, kiedy skończysz, prawda?
zginać

5
Jeśli buforowanie pomogło, byłoby tak, ponieważ BufferedInputStream buforuje domyślnie 8K. Zwiększ bajt [] do tego rozmiaru lub większego i możesz upuścić BufferedInputStream. np. spróbuj 1024 * 1024 bajtów.
Peter Lawrey

8
Dwie rzeczy: (1) Definicja terminatora linii w źródle Java to znak powrotu karetki, znak końca linii lub znak końca linii, po którym następuje znak końca linii. Twoje rozwiązanie nie będzie działać dla CR używanego jako terminator linii. To prawda, że ​​jedynym systemem operacyjnym, który według mnie używa CR jako domyślnego terminatora linii, jest Mac OS przed Mac OS X. (2) Twoje rozwiązanie zakłada kodowanie znaków, takie jak US-ASCII lub UTF-8. Liczba linii może być niedokładna w przypadku kodowania takiego jak UTF-16.
Nathan Ryan,

2
Niesamowity kod ... dla pliku tekstowego 400 MB zajęło to tylko sekundę. Dzięki bardzo @martinus
user3181500

199

Wdrożyłem inne rozwiązanie problemu, uważam, że jest bardziej wydajne w liczeniu wierszy:

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
}

LineNumberReader„s lineNumberpole jest liczbą całkowitą ... nie będzie to tylko owinąć plików dłużej niż Integer.MAX_VALUE? Po co męczyć się tutaj tak długo?
epb

1
Dodanie jednego do liczby jest w rzeczywistości nieprawidłowe. wc -lzlicza znaki nowego wiersza w pliku. Działa to, ponieważ każda linia kończy się nową linią, w tym ostatnią linią w pliku. Każda linia ma znak nowej linii, w tym puste linie, stąd liczba znaków nowej linii == liczba linii w pliku. Teraz lineNumberzmienna w FileNumberReaderreprezentuje również liczbę znaków nowego wiersza, które widzimy. Zaczyna się od zera, zanim zostanie znaleziona nowa linia, i jest zwiększana z każdym znakiem nowej linii. Więc nie dodawaj go do numeru linii.
Alexander Torstling

1
@PB_MLT: Chociaż masz rację, że plik z pojedynczą linią bez nowej linii byłby zgłaszany jako 0 linii, tak wc -lteż raportuje ten rodzaj pliku. Zobacz także stackoverflow.com/questions/729692/…
Alexander Torstling

@PB_MLT: Problem występuje odwrotnie, jeśli plik składa się wyłącznie z nowej linii. Sugerowane przez ciebie algo zwróci 0 i wc -lzwróci 1. Doszedłem do wniosku, że wszystkie metody mają wady i zaimplementowałem jedną w oparciu o to, jak chciałbym się zachowywać, zobacz moją drugą odpowiedź tutaj.
Alexander Torstling

3
Głosowałem za odrzuceniem tej odpowiedzi, ponieważ wygląda na to, że nikt z was jej nie
porównał

30

Zaakceptowana odpowiedź ma wyłączony jeden błąd dla plików wieloliniowych, które nie kończą się na nowej linii. Plik jednowierszowy kończący się bez nowej linii zwraca 1, ale plik dwuliniowy kończący się bez nowej linii również zwraca 1. Oto implementacja zaakceptowanego rozwiązania, które to rozwiązuje. Kontrole EndWithoutNewLine są marnotrawstwem dla wszystkiego oprócz odczytu końcowego, ale powinny być trywialne pod względem czasu w porównaniu do ogólnej funkcji.

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
Dobry chwyt Nie jestem pewien, dlaczego nie zmodyfikowałeś tylko zaakceptowanej odpowiedzi i zanotowałeś komentarz w komentarzu. Większość ludzi nie będzie czytać tak daleko.
Ryan,

@Ryan, edytowanie 4-letniej akceptowanej odpowiedzi z ponad 90 głosami nie było w porządku.
DMulligan,

@AFinkelstein, czuję, że to, co czyni to miejsce tak wielka, że można edytować górny głosowało odpowiedź.
Sebastian

3
To rozwiązanie nie obsługuje powrotu karetki (\ r) i powrotu karetki, a następnie podania wiersza (\ r \ n)
Simon Brandhof - SonarSource

@ Simon Brandhof, nie jestem pewien, dlaczego zwrot karetki byłby liczony jako kolejna linia? „\ N” jest linią powrotną karetki, więc ktokolwiek pisze „\ r \ n”, czegoś nie rozumie ... Poza tym szuka char po char, więc jestem pewien, że ktoś użyłby „\ r \ n "nadal będzie łapać" \ n "i liczyć linię. Tak czy inaczej, myślę, że dobrze to powiedział. Istnieje jednak wiele scenariuszy, w których nie jest to wystarczający sposób na uzyskanie liczby wierszy.
nckbrz

22

Z , możesz używać strumieni:

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

1
Kod zawiera błędy. Proste, ale bardzo wolne ... Spróbuj spojrzeć na moją odpowiedź poniżej (powyżej).
Ernestas Gruodis,

12

Odpowiedź z powyższą metodą count () dała mi błędne informacje o wierszach, jeśli plik nie miał nowej linii na końcu pliku - nie udało się zliczyć ostatniej linii w pliku.

Ta metoda działa dla mnie lepiej:

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

W takim przypadku nie ma potrzeby używania LineNumberReader, wystarczy użyć BufferedReader, w takim przypadku będziesz mieć elastyczność w używaniu długiego typu danych cnt.
Syed Aqeel Ashiq

[INFO] Błąd PMD: xx: 19 Reguła: EmptyWhileStmt Priorytet: 3 Unikaj pustych instrukcji while.
Chhorn Elit

8

Wiem, że to stare pytanie, ale zaakceptowane rozwiązanie nie do końca pasuje do tego, czego potrzebowałem. Udoskonaliłem go tak, aby akceptował różne terminatory linii (zamiast tylko przejścia do nowego wiersza) i używał określonego kodowania znaków (zamiast ISO-8859- n ). Wszystko w jednej metodzie (odpowiednio refaktor):

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

To rozwiązanie jest porównywalne pod względem prędkości do przyjętego rozwiązania, około 4% wolniej w moich testach (chociaż testy czasowe w Javie są notorycznie niewiarygodne).


8

Przetestowałem powyższe metody zliczania linii i oto moje obserwacje dotyczące różnych metod testowanych w moim systemie

Rozmiar pliku: 1,6 Gb Metody:

  1. Za pomocą skanera : ok. 35s
  2. Korzystanie z BufferedReader : ok. 5s
  3. Przy użyciu Java 8 : 5s ok
  4. Korzystanie z LineNumberReader : ok. 5s

Co więcej, podejście Java8 wydaje się całkiem przydatne:

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

Testowane na JDK8_u31. Ale w rzeczywistości wydajność jest niska w porównaniu do tej metody:

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

Testowany i bardzo szybki.


To nie jest poprawne Przeprowadziłem kilka eksperymentów ze swoim kodem, a metoda jest zawsze wolniejsza. Stream<String> - Time consumed: 122796351 Stream<String> - Num lines: 109808 Method - Time consumed: 12838000 Method - Num lines: 1Liczba wierszy jest nawet błędna
pomyśl o

Testowałem na maszynie 32-bitowej. Może na 64-bitach byłyby inne wyniki. I była to różnica 10 razy lub więcej, jak pamiętam. Czy możesz zamieścić tekst, aby gdzieś policzyć wiersz? Możesz użyć Notatnika2, aby zobaczyć podział linii dla wygody.
Ernestas Gruodis

To może być różnica.
aw-think

Jeśli zależy ci na wydajności, nie powinieneś używać a, BufferedInputStreamjeśli i tak chcesz czytać w swoim buforze. Poza tym, nawet jeśli twoja metoda może mieć niewielką przewagę wydajnościową, traci elastyczność, ponieważ nie obsługuje już \rterminatorów pojedynczych linii (stary MacOS) i nie obsługuje każdego kodowania.
Holger

4

Prosty sposób za pomocą skanera

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

Doszedłem do wniosku, że wc -l: metoda liczenia znaków nowej linii jest w porządku, ale zwraca nieintuicyjne wyniki w plikach, w których ostatni wiersz nie kończy się znakiem nowego wiersza.

I rozwiązanie @ er.vikas oparte na LineNumberReader, ale dodanie jednego do liczby wierszy zwróciło nieintuicyjne wyniki w plikach, w których ostatni wiersz kończy się znakiem nowej linii.

Dlatego stworzyłem algo, które działa w następujący sposób:

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

I wygląda to tak:

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

Jeśli chcesz uzyskać intuicyjne wyniki, możesz tego użyć. Jeśli chcesz tylko wc -lkompatybilności, użyj prostego rozwiązania @ er.vikas, ale nie dodawaj jednego do wyniku i ponów próbę:

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

2

Co powiesz na użycie klasy Process z kodu Java? A następnie odczytanie wyniku polecenia.

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

Trzeba jednak spróbować. Opublikuje wyniki.


1

Jeśli nie masz żadnych struktur indeksu, nie obejdziesz się odczytywania pełnego pliku. Ale możesz to zoptymalizować, unikając czytania linii po linii i użyj wyrażenia regularnego, aby dopasować wszystkie terminatory linii.


Brzmi jak fajny pomysł. Ktoś próbował tego i ma regexp?
willcodejavaforfood

1
Wątpię, żeby to był dobry pomysł: będzie musiał przeczytać cały plik na raz (martinus tego unika), a wyrażenia regularne są nadmiernie zabójcze (i wolniejsze) dla takiego użycia (proste wyszukiwanie ustalonych znaków).
PhiLho,

@ will: a co z / \ n /? @PhiLo: Regex Executors to maszyny o wysokiej wydajności. Z wyjątkiem zastrzeżenia dotyczącego odczytu wszystkiego do pamięci, nie sądzę, że ręczne wdrożenie może być szybsze.
David Schmitt

1

To zabawne rozwiązanie działa naprawdę dobrze!

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

W systemach uniksowych użyj wcpolecenia w wierszu polecenia.


@IndmH, twoja druga sugestia po prostu liczy liczbę wpisów w bieżącym katalogu. Nie to, co było zamierzone? (lub poproszony przez OP)
Archetypal Paul

@IndMH: tak zresztą robi wc (czytanie pliku, liczenie końca linii).
PhiLho,

@PhiLho Aby policzyć linie, należy użyć przełącznika -l. (Prawda? - minęło trochę czasu)
Iain Holder

@Paul - masz oczywiście 100% racji. Moją jedyną obroną jest to, że umieściłem to przed moją kawą. Jestem teraz ostry jak guzik. : D
Iain Holder

0

Jedynym sposobem, aby dowiedzieć się, ile wierszy jest w pliku, jest ich policzenie. Możesz oczywiście utworzyć metrykę na podstawie danych, podając średnią długość jednej linii, a następnie uzyskać rozmiar pliku i podzielić go za pomocą śr. długość, ale to nie będzie dokładne.


1
Interesujące zdanie negatywne, bez względu na to, jakiego narzędzia wiersza poleceń używasz, wszystkie ZRÓB TO SAM RZECZ, tylko wewnętrznie. Nie ma magicznego sposobu na określenie liczby linii, należy je policzyć ręcznie. Jasne, że można go zapisać jako metadane, ale to zupełnie inna historia ...
Esko

0

Najlepszy kod zoptymalizowany dla plików wieloliniowych nieposiadających znaku nowej linii („\ n”) w 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

Skaner z wyrażeniem regularnym:

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

Nie zarejestrowałem tego.


-2

jeśli tego użyjesz

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

nie możesz biegać do dużych liczb, lubi 100 000 wierszy, ponieważ return from reader.getLineNumber is int. potrzebujesz długiego typu danych do przetworzenia maksymalnej liczby wierszy ..


14
intMoże pomieścić maksymalnie do wartości, w przybliżeniu, 2 mld. Jeśli ładujesz plik zawierający ponad 2 miliardy linii, masz problem z przepełnieniem. To powiedziawszy, jeśli ładujesz nieindeksowany plik tekstowy z ponad dwoma miliardami linii, prawdopodobnie masz inne problemy.
Adam Norberg,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.