JuliaKurs23/chapters/13_IO.qmd
2024-06-05 13:14:30 +02:00

480 lines
11 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
engine: julia
---
# Ein- und Ausgabe
```{julia}
#| error: false
#| echo: false
#| output: false
# https://github.com/JuliaLang/julia/blob/master/base/show.jl#L516-L520
# https://github.com/JuliaLang/julia/blob/master/base/show.jl#L3073-L3077
using InteractiveUtils
import QuartoNotebookWorker
Base.stdout = QuartoNotebookWorker.with_context(stdout)
myactive_module() = Main.Notebook
Base.active_module() = myactive_module()
```
## Konsole
Das Betriebssystem stellt für ein Programm üblicherweise 3 Kanäle _(streams)_ zur Verfügung:
- Standardeingabekanal `stdin`
- Standardausgabekanal `stdout` und
- Standardfehlerausgabekanal `stderr`.
Wenn das Programm in einem Terminal (oder Konsole bzw. Shell) gestartet wird, kann das Programm über `stdin` die Tastatureingaben
einlesen und Ausgaben über `stdout` sowie `stdout` erscheinen im Terminal.
- Schreiben nach `stdout`: `print()`,`println()`,`printstyled()`
- Schreiben nach `stderr`: `print(strerr,...)`, `println(stderr,...)`, `printstyled(stderr,...)`
- Lesen von `stdin`: `readline()`
### Eingaben
Die Sprache _Python_ stellt eine Funktion `input()` zur Verfügung:
```{.python}
ans = input("Bitte eine positive Zahl eingeben!")
```
Die Funktion gibt den Prompt aus, wartet auf eine Eingabe und liefert die
Eingabe als `string` zurück.
In Julia kann man diese Funktion so implementieren:
```{julia}
function input(prompt = "Eingabe:")
println(prompt)
flush(stdout)
return chomp(readline())
end
```
**Anmerkungen**
- Schreibanweisungen werden von modernen Betriebssystemen gebuffert. Mit `flush(stdout)` wird die Leerung des Buffers und sofortige Schreiboperation erzwungen.
- `readline()` liefert einen String zurück, der mit einer Newline `\n` endet. Die Funktion `chomp()` entfernt einen eventuellen Zeilenumbruch vom Ende eines Strings.
```{julia}
#| eval: false
a = input("Bitte 2 Zahlen eingeben!")
```
```{julia}
#| echo: false
a = "34 56"
```
### Verarbeitung der Eingabe
> `split(str)` zerlegt einen String in "Wörter" und liefert einen _(array of strings)_:
```{julia}
av = split(a)
```
> `parse(T, str)` versucht, `str` in den Typ `T` umzuwandeln:
```{julia}
v = parse.(Int, av)
```
`parse()` erzeugt einen Fehler, wenn der String sich nicht als Wertangabe von Typ `T` parsen lässt. Man kann den Fehler mit
`try/catch` abfangen oder die Funktion `tryparse(T, str)` verwenden, die in so einem Fall `nothing` zurückgibt - worauf man dann
z.B. mit `isnothing()` testen kann.
### Einzelne Tastenanschläge einlesen
- `readline()` u.ä. warten auf den Abschluss der Eingabe durch Drücken der `Enter`-Taste.
- Techniken zum Einlesen einzelner _keystrokes_ findet man hier:
- [https://stackoverflow.com/questions/56888266/how-to-read-keyboard-inputs-at-every-keystroke-in-julia](https://stackoverflow.com/questions/56888266/how-to-read-keyboard-inputs-at-every-keystroke-in-julia)
- [https://stackoverflow.com/questions/60954235/how-can-i-test-whether-stdin-has-input-available-in-julia](https://stackoverflow.com/questions/60954235/how-can-i-test-whether-stdin-has-input-available-in-julia)
## Formatierte Ausgabe mit dem `Printf`-Makro
Oft möchte man Zahlen oder Strings mit einer strikten Formatvorgabe - Gesamtlänge, Nachkommastellen, rechts/linksbündig usw - ausgeben.
Dazu definiert das Paket `Printf` die Makros `@sprintf` und `@printf`, welche sehr ähnlich wie die gleichnamigen C-Funktionen arbeiten.
```{julia}
using Printf
x = 123.7876355638734
@printf("Ausgabe rechtsbündig mit max. 10 Zeichen Platz und 3 Nachkommastellen: x= %10.3f", x)
```
Das erste Argument ist ein String, der Platzhalter (hier: `%10.3`) für auszugebende Variablen enthält; gefolgt von diesen Variablen als weitere Argumente.
Platzhalter haben die Form
```
%[flags][width][.precision]type
```
wobei die Angaben in eckigen Klammern alle optional sind.
**Typangaben im Platzhalter**
| | |
|:--|:------------|
|`%s`| `string`|
|`%i`| `integer`|
|`%o`| `integer octal (base=8)`|
|`%x, %X`| `integer hexadecimal (base=16) with digits 0-9abcdef or 0-9ABCDEF, resp.`|
|`%f`| `floating point number`|
|`%e`| `floating point number, scientific representation`|
|`%g`| `floating point, uses %f or %e depending on value`|
: {.striped .hover}
**Flags**
| | |
|:----|:-----|
|Pluszeichen| rechtsbündig (Standard)|
|Minuszeichen| linksbündig|
|Null| mit führenden Nullen|
: {.striped .hover}
**Width**
```
Anzahl der minimal verwendeten Zeichen (wenn nötig, werden auch mehr genommen)
```
### Beispiele:
```{julia}
using Printf # Paket laden nicht vergessen!
```
```{julia}
@printf("|%s|", "Hallo") # string mit Platzhalter für String
```
Die senkrechten Striche sind nicht Teil des Platzhalters. Sie sollen die Begrenzung des Ausgabefeldes anzeigen.
```{julia}
@printf("|%10s|", "Hallo") # Minimallänge, rechtsbündig
```
```{julia}
@printf("|%-10s|", "Hallo") # linksbündig
```
```{julia}
@printf("|%3s|", "Hallo") # Längenangabe kann überschritten werden
# besser eine 'kaputt formatierte' Tabelle als falsche Werte!
```
```{julia}
j = 123
k = 90019001
l = 3342678
@printf("j= %012i, k= %-12i, l = %12i", j, k, l) # 0-Flag für führende Nullen
```
`@printf` und `@sprintf` können wie alle Makros wie Funktionen aufgerufen werden:
```{julia}
@printf("%i %i", 22, j)
```
-- oder wie Makros, also ohne Funktionsklammern und ohne Komma:
```{julia}
@printf "%i %i" 22 j
```
`@printf` kann als erstes Argument noch einen Stream übergeben bekommen.
Ansonsten besteht die Argumentliste aus
- Formatstring mit Platzhaltern
- Variablen in der Reihenfolge der Platzhalter, in Anzahl und Typ zu den Platzhaltern passend
```{julia}
@printf(stderr, "Erstes Resultat: %i %s\nZweites Resultat %i",
j, "(geschätzt)" ,k)
```
Das Makro `@sprintf` druckt nichts, sondern liefert den ausgefüllten formatierten String zurück:
```{julia}
str = @sprintf("x = %10.6f", π );
```
```{julia}
str
```
### Formatierung der Gleitkommazahlen:
Bedeutung des _Precision_-Wertes:
- `%f` und `%e`-Format: maximale Anzahl der Nachkommastellen
- `%g`-Format: maximale Anzahl von ausgegebenen Ziffern (Vor- + Nachkommastellen)
```{julia}
x = 123456.7890123456
@printf("%20.4f %20.4e", x, x) # 4 Nachkommastellen
```
```{julia}
@printf("%20.7f %20.7e", x, x) # 7 Nachkommastellen
```
```{julia}
@printf("%20.7g %20.4g", x, x) # insgesamt 7 bzw. 4 Stellen
```
## Dateioperationen
Dateien werden
- geöffnet $\Longrightarrow$ dabei ensteht ein neues _stream_-Objekt (zusätzlich zu `stdin, stdout, stderr`)
- dann kann dieser _stream_ gelesen und geschrieben werden
- geschlossen $\Longrightarrow$ _stream_-Objekt wird von Datei getrennt
```{.julia}
stream = open(path, mode)
```
- path: Dateiname/pfad
- mode:
```
"r" read, öffnet am Dateianfang
"w" write, öffnet am Dateianfang (Datei wird neu angelegt oder überschrieben)
"a" append, öffnet zum Weiterschreiben am Dateiende
```
Schreiben wir mal eine Datei:
```{julia}
file = open("datei.txt", "w")
```
```{julia}
@printf(file, "%10i\n", k)
```
```{julia}
println(file, " zweite Zeile")
```
```{julia}
close(file)
```
Schauen wir uns die Datei an:
```{julia}
;cat datei.txt
```
...und jetzt öffnen wir sie wieder zum Einlesen:
```{julia}
stream = open("datei.txt", "r")
```
`readlines(stream)` liefert alle Zeilen einer Textdatei als Vector von Strings.
`eachline(stream)` liefert einen Iterator über die Zeilen der Datei.
```{julia}
n = 0
for line in eachline(stream) # Lese zeilenweise
n += 1
println(n, line) # Drucke mit Zeilennummer
end
close(stream)
```
## Pakete für Dateiformate
Für die Ein- und Ausgabe in den verschiedensten Dateiformaten existieren Julia-Pakete, z.B.
- [PrettyTables.jl](https://ronisbr.github.io/PrettyTables.jl/stable/) Ausgabe von formatierten Tabellen
- [DelimitedFiles.jl](https://docs.julialang.org/en/v1/stdlib/DelimitedFiles/) Ein- und Ausgabe von Matrizen u.ä.
- [CSV.jl](https://csv.juliadata.org/stable/) Ein- und Ausgabe von Dateien mit "comma-separated values" u.ä.
- [XLSX.jl](https://felipenoris.github.io/XLSX.jl/stable/tutorial/) Ein- und Ausgabe von Excel-Dateien
und viele andere mehr...
### DelimitedFiles.jl
Dieses Paket ermöglicht das bequeme Abspeichern/Einlesen von Matrizen. Dazu stellt es die Funktionen `writedlm()` und `readdlm()` zur
Verfügung.
```{julia}
using DelimitedFiles
```
Wir erzeugen eine 200×3-Matrix von Zufallszahlen
```{julia}
A = rand(200,3)
```
und speichern diese
```{julia}
f = open("data2.txt", "w")
writedlm(f, A)
close(f)
```
Die geschriebene Datei fängt so an:
```{julia}
;head data2.txt
```
Das Wiedereinlesen ist noch einfacher:
```{julia}
B = readdlm("data2.txt")
```
Noch ein Punkt: Beim Umgang mit Dateien wird in Julia oft die `do`-Notation verwendet, s. @sec-do.
Dazu nutzt man, dass `open()` auch Methoden hat, bei denen das 1. Argument eine `function(iostream)` ist.
Diese wird dann auf den _stream_ angewendet und dieser abschliessend automatisch geschlossen. Die `do`-Notation erlaubt es,
diese Funktion anonym nach dem `do` zu definieren:
```{julia}
open("data2.txt", "w") do io
writedlm(io, A)
end
```
### CSV und DataFrames
- Das CSV-Format wird oft benutzt, um Tabellen in einer nicht nur mit MS Excel lesbaren Form zur Verfügung zu stellen.
- Ein Beispiel ist die Wetter- und Klimadatenbank _Meteostat_.
- Das Paket [DataFrames.jl](https://dataframes.juliadata.org/stable/) stellt Funktionen zum bequemen Umgang mit tabellarischen Daten
zur Verfügung.
```{julia}
using CSV, DataFrames, Downloads
# Wetterdaten von Westerland, s. https://dev.meteostat.net/bulk/hourly.html
url = "https://bulk.meteostat.net/v2/hourly/10018.csv.gz"
http_response = Downloads.download(url)
file = CSV.File(http_response, header=false);
```
Die Daten sehen so aus:
```{julia}
# https://dev.meteostat.net/bulk/hourly.html#endpoints
#
# Spalte 1 Datum
# 2 Uhrzeit (Stunde)
# 3 Temp
# 5 Luftfeuchtigkeit
# 6 Niederschlag
# 8 Windrichtung
# 9 Windstärke
df = DataFrame(file)
```
```{julia}
#| error: false
#| echo: false
#| output: false
#| eval: false
describe(df)
```
Zum bequemen Plotten und zum Umgang mit den Datums- und Zeitformaten in der Wettertabelle
laden wir noch 2 Helferlein:
```{julia}
using StatsPlots, Dates
```
Wir erzeugen eine neue Spalte, die Datum (aus Spalte 1) und Uhrzeit (aus Spalte 2) kombiniert:
```{julia}
# neue Spalte mit Sp.1 und 2 (date & time) kombiniert
df[!, :datetime] = DateTime.(df.Column1) .+ Hour.(df.Column2);
```
```{julia}
#| error: false
#| echo: false
#| output: false
#| eval: false
@df df plot(:datetime, :Column3)
```
Und nun zum Plot:
```{julia}
@df df plot(:datetime, [:Column9, :Column6, :Column3],
xlims = (DateTime(2023,9,1), DateTime(2024,5,30)),
layout=(3,1), title=["Wind" "Regen" "Temp"],
legend=:none, size=(800,800))
```