Jak radzić sobie z dużymi plikami w Javie ?

Jak radzić sobie z dużymi plikami w Javie ?

W tym artykule zajmiemy się tematem radzenia sobie z dużymi plikami w Javie. Na pierwszy rzut oka temat może wydawać się skomplikowany, ale wystarczy znać kilka podstawowych zagadnień związanych z zapisem i odczytem plików, żeby przekonać się, że nie jest to takie trudne.

Jak zdefiniować czym jest duży plik?

Żeby rozpocząć rozważania na temat dużych plików, musimy ustalić jaką miarę przyjąć, powyżej której plik można uznać za „duży”. Trudno tu znaleźć jakąś sensowną definicję, ponieważ gdy przetwarzamy dużą ilość danych zawartych w plikach, to nie tylko wielkość pliku jest ważna, ale także to, ile tych plików jest. Dla jednych dużo to jest 1000 plików wielkości 5 MB (= 5GB). Dla innych jeden plik, który ma powyżej 1 GB.

Chodzi tu bardziej o skalę danych do przetworzenia, niż o samą wielkość plików i ich ilość.

Dla uproszczenia przyjmijmy, że każdy plik, którego nie da się otworzyć w „zwykłym” notatniku, to duży plik. Powiedzmy, że plik o wielkości ok. 1GB jest tutaj dobrym przykładem.

Jak edytować duży plik tekstowy ?

Otworzenie gigabajtowego pliku w zwykłym notatniku nie jest łatwym zadaniem, a właściwie jest niemożliwe. Jeśli spróbujesz otworzyć taki plik w notatniku windowsowym (Notepad), to otrzymasz następujący błąd:

A jeśli skorzystasz z bardziej zaawansowanego notatnika np. Notepad++, będzie podobnie:

Nawet jeśli twoim systemem operacyjnym jest linux i skorzystasz z jakiegoś notatnika linux’owego np. Gedit, to uzyskasz podobny efekt. Dlaczego tak się dzieje? Edytory te działają w taki sposób, ze próbują wczytać cały plik do pamięci, co w przypadku małych plików nie jest problemem, a nawet przyspiesza ich edycję. Natomiast duże pliki nie mieszczą się po prostu w pamięci, jaką mamy do dyspozycji w tych edytorach.

Co zrobić zatem w sytuacji, gdy mamy duży plik i musimy pozmieniać w nim niektóre rzeczy? Z pomocą przychodzą nam takie narzędzia jak np. vim. Jest to edytor, który działa z wiersza poleceń, dzięki czemu jest też on trochę wydajniejszy i lepiej radzi sobie z dużymi plikami. Jednak nadal będziesz mieć ten sam problem (wczytywanie całego pliku do pamięci). Jeżeli twój plik jest większy od pamięci RAM jaką posiada twój komputer to nie da się go odczytać w ten sposób.

Less is more, more or less

Kolejnym narzędziem, które może nam pomóc jest linuksowe polecenie less. Jest to narzędzie, które służy do przeglądania plików i rozmiar nie ma tutaj znaczenia, ponieważ less wczytuje tylko fragment pliku. Daje także całkiem duże możliwości wyszukiwania, ale sam jako taki nie pozwala na edycję dokumentów.

Stream Editor (sed)

Kolejnym narzędziem, które może nam pomóc jest polecenie linux’owe sed. Służy ono do edycji plików. Jest to narzędzie, które traktuje plik jako strumień, dzięki czemu jest w stanie przetworzyć bardzo duże pliki przy wykorzystaniu niewielkiej ilości pamięci.

Pozostałe przydatne narzędzia

Do tego mamy jeszcze dwa przydatne narzędzia linux’owe, które mogą pomóc przy pracy z dużymi plikami:

  • wc (word count) – narzędzie, które służy do zliczania liczby słów lub linii np. wc -l
  • grep – zaawansowane narzędzie do wyszukiwania w pliku/plikach

Wszystkie powyższe narzędzia: vim, less, wc, grep, sed, są dostępne na platformach Linux, Mac Os oraz Windows (na Windows można je doinstalować w MinGW, który jest też domyślnie instalowany z gitem lub poprzez Cygwin).

W rzeczywistości bardzo rzadko zachodzi potrzeba ręcznego edytowania tak dużych plików. Zwykle obrabia się je za pomocą skryptów lub różnego rodzaju programów. Ja w tym artykule pokażę kilka prostych przykładów jak robić to w Javie.

Jak utworzyć duży plik na potrzeby testów ?

Zaczniemy od utworzenia pliku testowego o odpowiednim rozmiarze czyli ok. 1GB. Użyję do tego FileWriter i BufferedWriter tak, żeby zapis pliku był jak najszybszy:

  1. Inicjujemy tablica String'ów, które będziemy wstawiali do pliku.
  2. Tworzymy BufferedWriter.
  3. Tworzymy pętlę, która wykona 30 mln obrotów, gdzie każdy zapisze 3 linijki.
  4. Zapisujemy linie pobrane z tablicy, oddzielając je znakiem końca wiersza.

W ten sposób przygotowana metoda, pozwoli nam zapisać plik o wielkości ok 1,3GB (na moim komputerze trwa to ok. 13s). Możesz, w zależności od potrzeb, wygenerować większy lub mniejszy plik, manipulując wartością w pętli.

Jak odczytać duży plik w Javie ?

Mamy kilka możliwości, aby odczytać plik w Javie. Każda z tych metod jest trochę inna i każda jest bardziej lub mniej wydajna. Wybór metody odczytu powinien zależeć od tego, jakie masz potrzeby w danej chwili. Ja tylko testuje te metody, więc skupię się na szybkości ich działania.

FileReader i odczyt znak po znaku

Najprostszy odczyt, który tu prezentuję, oparty tylko o FileReader. W przykładzie tym korzystam z najprostszej metody read(), która odczytuje plik bajt po bajcie. FileReader zawiera także inne metody read, które mogą być nawet bardziej wydajne jeśli wykorzystamy bufor. Warto też się z nimi zapoznać.

  1. Tworzymy FileReader.
  2. Odczytujemy bajty za pomoc metody read.
  3. Zliczamy bajty.
  4. Drukujemy ilość zliczonych bajtów.

Wynik:

Scanner i odczyt linia po linii

Do odczytu pliku możemy także użyć klasy Scanner, która ma całkiem zaawansowane możliwości odnoście dzielenia tekstu na tokeny. Jednak odczytywanie przez nią pliku może być znacznie wolniejsze.

  1. Tworzymy obiekt scannera.
  2. Przetwarzamy w pętli dopóki są nowe linie.
  3. Pobieramy nową linię (Uwaga: bez tej linii pętla będzie kręciła się w nieskończoność).
  4. Zliczamy ilość linii.
  5. Drukujemy ilość zliczonych linii.

Wynik:

Ta metoda jest prawie dwa razy wolniejsza od odczytu pojedynczych bajtów.

BufferedReader i odczyt linia po linii

Kolejna metoda to użycie FileReader i opakowanie go w BufferedReader. Buforowanie daje nam o wiele większą wydajność, więc jest to metoda dużo szybsza od odczytywania znak po znaku i także od Scannera (pamiętaj że Scanner ma dodatkowe funkcje, które mogą ci się przydać przy przetwarzaniu).

  1. Tworzymy BufferedReader.
  2. Odczytujemy linie za pomocą metody readLine().
  3. Zliczamy linie.
  4. Drukujemy ilość zliczonych linii.

Wynik:

Prawie 10 razy szybciej od Scanera i 5 razy szybciej od odczytu pojedynczych bajtów.

RandomAccessFile, memory maped file i odczyt znak po znaku

Klasa RandomAccessFile służy do losowego zapisu i odczytu pliku. Znaczy to, że plik może być zapisywany i odczytywany w dowolnym jego miejscu. Wraz z mapowaniem tego pliku w pamięci, daje to bardzo wydajny sposób odczytu pliku.

  1. Tworzymy instancję klasy RandomAccessFile (r – tylko do odczytu).
  2. Tworzymy MappedByteBuffer.
  3. Odczytujemy w pętli dane z bufora.
  4. Pobieramy znak z bufora.
  5. Drukujemy ilość zliczonych znaków.

Wynik:

Tylko 94 ms! Odczyt całego pliku, który ma 1,3 GB zajął nam tylko 94 ms. Dużo, dużo szybciej niż pozostałe metody odczytu. Metoda mapowania pliku do pamięci jest bardzo wydajna.

Porównanie wyników wydajnościowych

Wyniki wydają się jednoznaczne. Najszybszy odczyt plik następuje przez memory mapped file, ale sposób ten może powodować zwiększoną liczbę page faults (warto się zapoznać z tematem zanim się zacznie używać tego sposobu). Jeśli nie potrzebujemy super prędkości do przetwarzania naprawdę wielkich plików, to wystarczy użycie BufferedReader.

Podsumowanie

Jak widać z dużymi plikami w Javie można sobie poradzić na kilka sposobów. A co jeśli masz dziesiątki, a może setki dużych plików i jeden komputer nie jest w stanie sobie z tym poradzić ? Oczywiście do tego też powstały już zaawansowane narzędzia takie jak np. Hadoop, Apache Spark, czy Apache Flink, ale to już temat na kolejny artykuł.

 

PS: Jeśli masz jakieś sugestie dotyczące tego, czy innych artykułów napisz w komentarzu, będę Ci bardzo wdzięczny 😉

Żródła:

https://docs.oracle.com/javase/8/docs/api/java/io/FileReader.html
https://docs.oracle.com/javase/8/docs/api/java/util/Scanner.html
https://docs.oracle.com/javase/8/docs/api/java/io/BufferedReader.html
https://docs.oracle.com/javase/8/docs/api/java/util/RandomAccess.html
https://docs.oracle.com/javase/8/docs/api/java/nio/MappedByteBuffer.html

Zapisz się na newsletter

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat i zachęcam Cię do lektury mojego bloga

2 thoughts to “Jak radzić sobie z dużymi plikami w Javie ?”

  1. Dlaczego raz w wynikach jest w counter:
    1320000000 // counter
    ….
    a innym razem:
    90000000 // counter
    ….

    ?

    1. Raz zliczane są znaki, raz linie, dlatego te liczby się różnią.
      Sam licznik jest tylko po to, żeby za symulować jakiekolwiek działanie w pętli, inaczej trudno by było ocenić czy ta pętla w ogóle działa. A nawet, teoretycznie mogłaby zostać usunięta przez JIT jako „dead code”.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *