====== 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 =====
*{{:po:with-header.csv|}}
*{{:po:no-header.csv|}}
*{{:po:accelerator.csv|}}
*{{:po:missing-values.csv|}}
*{{:po:elec.csv|}}
===== 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 pliku
*''BufferedReader'' 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 columnLabels = new ArrayList<>();
// odwzorowanie: nazwa kolumny -> numer kolumny
Map 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
===== 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, jak ''parseHeader()'' rozpakowuje zawartość wiersza i przypisuje do ''current''
* 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)'' oraz ''CSVReader(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 [[http://pszwed.kis.agh.edu.pl/wyklady_java/w4-5-java-klasy.pdf|Wykład 4-5 Klasy, pola, metody, konstruktory, klonowanie]]
* ''List 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)'' oraz ''String get(String columnLabel)'' zwraca wartość jako String, raczej pusty tekst, a nie ''null''.
* ''int getInt(int columnIndex)'' oraz ''int getInt(String columnLabel)'' - funkcja konwertuje wartość do int. Użyj ''Integer.parseInt()''. Funkcja wygeneruje wyjątek, jeśli pole było puste.
* ''long getLong(int columnIndex)'' oraz ''long getLong(String columnLabel)''
* ''double getDouble(int columnIndex)'' oraz ''double getDouble(String columnLabel)''
=== Uwagi do isMissing() ===
for(String s:"ala ma kota,12,,,,4,,,,".split(",")){
System.out.println("<"+s+">");
}
Wynik
<12>
<>
<>
<>
<4>
Brakuje atrybutu jeśli:
* numer kolumny jest poza zakresem ''current.length''
* tekst jest pusty
===== 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 {{:po: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).
===== Zestaw znaków =====
Poniższy kod pokazuje, jak odczytać/zapisać plik wskazując zestaw znaków
try (BufferedReader input = new BufferedReader(new InputStreamReader( new FileInputStream(inname), Charset.forName("Cp1250")))){
try(PrintWriter output = new PrintWriter( new OutputStreamWriter(new FileOutputStream(outname),"Cp1250"))){
for(;;){
String line = input.readLine();
if(line==null)break;
output.println(line);
}
}