This is an old revision of the document!
Table of Contents
Laboratorium 6 - CSV Reader
CSV to popularny tekstowy format zapisu danych.
- Poszczególne pola oddzielone są separatorami, np. przecinkami, średnikami lub znakami tabulacji.
- Kolumny powinny zawierać dane tego samego typu (liczby całkowite, double, teksty, daty)
- Często na początku pliku umieszczany jest wiersz nagłówkowy z nazwami (etykietami) kolumn
- W niektórych rekordach może brakować wartości
- Teksty mogą być ujęte w cudzysłowy
Przykład titanic-part.csv
- informacje o pasażerach Titanica.
PassengerId,Survived,Pclass,Name,Sex,Age,SibSp,Parch,Ticket,Fare,Cabin,Embarked 1,0,3,"Braund, Mr. Owen Harris",male,22,1,0,A/5 21171,7.25,,S 2,1,1,"Cumings, Mrs. John Bradley (Florence Briggs Thayer)",female,38,1,0,PC 17599,71.2833,C85,C 3,1,3,"Heikkinen, Miss. Laina",female,26,0,0,STON/O2. 3101282,7.925,,S 4,1,1,"Futrelle, Mrs. Jacques Heath (Lily May Peel)",female,35,1,0,113803,53.1,C123,S 5,0,3,"Allen, Mr. William Henry",male,35,0,0,373450,8.05,,S 6,0,3,"Moran, Mr. James",male,,0,0,330877,8.4583,,Q 7,0,1,"McCarthy, Mr. Timothy J",male,54,0,0,17463,51.8625,E46,S 8,0,3,"Palsson, Master. Gosta Leonard",male,2,3,1,349909,21.075,,S 9,1,3,"Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)",female,27,0,2,347742,11.1333,,S 10,1,2,"Nasser, Mrs. Nicholas (Adele Achem)",female,14,1,0,237736,30.0708,,C 11,1,3,"Sandstrom, Miss. Marguerite Rut",female,4,1,1,PP 9549,16.7,G6,S 12,1,1,"Bonnell, Miss. Elizabeth",female,58,0,0,113783,26.55,C103,S 13,0,3,"Saundercock, Mr. William Henry",male,20,0,0,A/5. 2151,8.05,,S 14,0,3,"Andersson, Mr. Anders Johan",male,39,1,5,347082,31.275,,S 15,0,3,"Vestrom, Miss. Hulda Amanda Adolfina",female,14,0,0,350406,7.8542,,S 16,1,2,"Hewlett, Mrs. (Mary D Kingcome) ",female,55,0,0,248706,16,,S 17,0,3,"Rice, Master. Eugene",male,2,4,1,382652,29.125,,Q 18,1,2,"Williams, Mr. Charles Eugene",male,,0,0,244373,13,,S
Naszym zadaniem jest napisanie klasy konfigurowalnej CSVReader
pozwalającej na odczyt danych z plików CSV.
Przykładowe pliki do wykorzystania
Zadeklaruj klasę CSVReader
public class CSVReader { BufferedReader reader; String delimiter; boolean hasHeader; /** * * @param filename - nazwa pliku * @param delimiter - separator pól * @param hasHeader - czy plik ma wiersz nagłówkowy */ public CSVReader(String filename,String delimiter,boolean hasHeader) { reader = new BufferedReader(new FileReader(filename)); this.delimiter = delimiter; this.hasHeader = hasHeader; if(hasHeader)parseHeader(); } //... }
Reader
to klasa pozwalająca na odczyt bajtów i konwersję ich na znaki unicode. Czyli np. dwa bajty w pliku w formacie UTF-8 rerezentujące polskie znaki zostaną zamienione na ę, ą, ł, itd.FileReader
czyta znaki z plikuBufferedReader
dodaje możliwość buforowanego odczytu, czyli czytania całych linii
Nazwy kolumn
Nazwy kolumn będą przechowywane w dwóch miejscach: na liście oraz w mapie. Mapa ma przyspieszyć wyszukiwanie indeksu w tablicy. Można po prostu szukać tekstu na liście, ale w ten sposób będzie prościej.
// nazwy kolumn w takiej kolejności, jak w pliku List<String> columnLabels = new ArrayList<>(); // odwzorowanie: nazwa kolumny -> numer kolumny Map<String,Integer> columnLabelsToInt = new HashMap<>();
Funkcja parseHeader()
pokazuje typowe przetwarzanie wiersza pliku (w tym przypadku nagłówka)
void parseHeader() { // wczytaj wiersz String line = reader.readLine(); if(line==null){ return; } // podziel na pola String[]header = line.split(delimiter); // przetwarzaj dane w wierszu for(int i=0;i<header.length;i++){ // dodaj nazwy kolumn do columnLabels i numery do columnLabelsToInt }
Odczyt danych
Przyjmijmy następującą strategię:
- aby wczytać kolejny rekord należy wywołać funkcję
next()
. Jeśli nie udało się wczytać wiersza - ma ona zwrócićfalse
- przed rozpoczęciem dostępu do danych nic nie jest wczytane, czyli na początku należy wywołać
next()
- funkcja
next
działa podobnie, jakparseHeader()
rozpakowuje zawartość wiersza i przypisuje docurrent
- klasa zapewnia funkcje odczytu zawartości elementów tablicy
current
(i ewentualnej konwersji).
String[]current; boolean next(){ // czyta następny wiersz, dzieli na elementy i przypisuje do current // return false; }
Czyli standardowy sposób dostępu do danych powinien być następujący:
CSVReader reader = new CSVReader("titanic-part.csv",",",true); while(reader.next()){ int id = reader.getInt("PassengerId"); String name = reader.get("Name"); double fare = reader.getDouble("Fare"); System.out.printf(Locale.Us,"%d %s %d",id, name, fare); }
Ponieważ plik CSV może nie zawierać nagłówka, klasa CSVReader
powininna zapewniać interfejs dostępu do pól poprzez numer kolumny
CSVReader reader = new CSVReader("titanic-part.csv",",",true); while(reader.next()){ int id = reader.getInt(0); String name = reader.get(3); double fare = reader.getDouble(9); System.out.printf(Locale.Us,"%d %s %d",id, name, fare); }
Uwaga, nie zaczynaj od pliku titanic-part.csv
, ponieważ będzie sprawiał problemy. Potraktuj powyższy kod, jako przykład i dostosuj do konkretnego pliku. O tym na samym końcu…
Do zaimplementowania
- Wymienione wcześniej funkcje
- Konstruktory przyjmujące standardowe wartości
CSVReader(String filename,String delimiter)
orazCSVReader(String filename)
- Dodaj jeszcze konstruktor
public CSVReader(Reader reader, String delimiter, boolean hasHeader)
pozwalający na odczyt z dowolnego źródła
- Wywołaj konstruktor najbardziej ogólny z konstruktorów przyjmujących standardowe wartości argumentów wywołania tak, aby nie duplikować kodu. O wywołaniu konstruktora z innego - patrz treść wykładu Wykład 4-5 Klasy, pola, metody, konstruktory, klonowanie
List<String> getColumnLabels()
- zwraca etykiety kolumn
int getRecordLength()
- zwraca długość bieżącego wczytanego rekordu
boolean isMissing(int columnIndex)
– czy wartość istnieje w bieżącym rekordzie
boolean isMissing(String columnLabel)
– analogiczny dostęp przez etykietę kolumny
String get(int columnIndex)
orazString get(String columnLabel)
zwraca wartość jako String, raczej pusty tekst, a nienull
.
int getInt(int columnIndex)
orazint getInt(String columnLabel)
- funkcja konwertuje wartość do int. UżyjInteger.parseInt()
. Funkcja wygeneruje wyjątek, jeśli pole było puste.
long getLong(int columnIndex)
orazlong getLong(String columnLabel)
double getDouble(int columnIndex)
orazdouble getDouble(String columnLabel)
Napisz testy
Nie będą to formalnie testy jednostkowe poszczególnych funkcji ale raczej testy scenariuszy odczytu z pliku
- Napisz testy całych sekwencji odczytu przykładowych plików. W pętli wykonuj
next()
i odczytaj zawartość wszystkich pól jako String oraz wypisz - Przetestuj poprawność funkcji zwracających wartości poszczególnych typów.
- Pokaż, że jesteś w stanie przetworzyć plik z brakującymi wartościami pól. Możesz zastosować dwie strategie - albo przechwycisz wyjątek, albo sprawdzisz, czy nie brakuje wartości.
- Napisz testy odwołań się do nieistniejących kolumn, zarówno podanych jako indeksy, jak i nazwy…
- Przetestuj także odczyt z innych źródeł niż plik, np.
String text = "a,b,c\n123.4,567.8,91011.12"; reader = new CSVReader(new StringReader(text),",",true);
Teksty w cudzysłowach
Zastanów się, co można zrobić z tekstami w cudzysłowach. Przy eksporcie CSV pola umieszcza się w cudzysłowach, jeśli zwierają separatory pól. Tak właśnie jest w pliku titanic-part.csv. Jest to problem, ponieważ przy standardowej metodzie przetwarzania nastąpi wydzielenie pól wewnątrz tekstu w cudzysłowach, czego oczywiście chcemy uniknąć.
Odpowiedź jest tu: https://stackoverflow.com/questions/15738918/splitting-a-csv-file-with-quotes-as-text-delimiter-using-string-split, ale trzeba dostosować kod do zdefiniowanego ogranicznika pól…
Możesz użyć String.format()
do przygotowania wyrażenia regularnego dla funkcji split()
lub przekazać wyrażenie regularne, jako ogranicznik. Wybór nie jest jednoznaczny - oba rozwiązania mają wady i zalety. Prawdopodobnie bardziej elastyczne jest przekazanie wyrażenia regularnego.
Funkcje getTime i getDate
Napisz funkcje CSVReader
zwracające czas i datę. Ich dodatkowym parametrem powinien być format zapisu, czyli np. LocalTime getTime(int columnIndes,String format)
.
Możesz wykorzystać poniższe fragmenty kodu
LocalTime time = LocalTime.parse("12:55",DateTimeFormatter.ofPattern("HH:mm")); System.out.println(time); time = LocalTime.parse("12:55:23",DateTimeFormatter.ofPattern("HH:mm:ss")); System.out.println(time); LocalDate date = LocalDate.parse("2017-11-30", DateTimeFormatter.ofPattern("yyyy-MM-dd")); System.out.println(date); date = LocalDate.parse("23.11.2017", DateTimeFormatter.ofPattern("dd.MM.yyyy")); System.out.println(date);
Możesz także napisać funkcje zwracające LocalDateTime (połaczenie daty i czasu).