Dateien einlesen mit Rust

  • In diesem Beitrag möchte ich euch zeigen, wie man mit Rust Dateien einlesen kann. Aus gegebenen Anlass möchte ich das am Beispiel der Eingabendateien für die Junioraufgabe 2 des aktuellen BwInf zeigen, der Aufgabe "Baywatch".

    Wenn ihr das Programm in diesem Blogeintrag selber schreiben wollt, müsst ihr zuerst Rust installiert haben. Windows-Nutzer können dabei den vorherigen Blogeintrag zum Installieren von Rust unter Windows lesen, oder sich das "Windows Subsystem for Linux" installieren und dann der Anweisung für Linux unter rustup.rs folgen. (Also `curl https://sh.rustup.rs -sSf | sh` ausführen.)

    Wenn ihr das Programm, dass wir hier schreiben, ausführen wollt könnt ihr entweder direkt den Rust-Compiler (`rustc`) nutzen, oder ihr legt euch mit Cargo ein Rust-Projekt an und führt euer Programm mit `cargo run` aus. Auch dazu findet ihr mehr Informationen in dem vorherigen Blogeintrag zum Installieren von Rust unter Windows.

    Wer ganz ungeduldig ist, finden ganz unten den kompletten Quelltext des hier geschriebenen Programms.

    Ein Rust-Programm beginnt immer in der `main`-Funktion. Das einfachste Rust-Programm (das Programm, das nichts tut) sieht so aus:

    fn main() {
      // hier kommt der Rest des Programms hin
    }

    Ein besonderer Augenmerk bei Rust liegt in der Sicherheit (z. B. vor falschen Speicherzugriffen) und der expliziten Fehlerbehandlung. Weil beim Öffnen und Lesen von Dateien aber Fehler auftreten können – z. B. dass die Datei nicht gefunden wurde – müssen wir uns entscheiden, wir wir mit diesen Fehlern umgehen wollen. Ich habe mich hier dafür entschieden, dass bei Fehlern das Programm beendet werden soll und eine Fehlermeldung ausgegeben werden soll. Dies sage ich Rust, indem ich angebe, dass die Funktion `main` auch mit einem Fehler beendet werden kann:

    fn main() -> std::io::Result<()> {
      // hier kommt der Rest des Programms hin Ok(())
    }

    Damit Rust weiß, dass es keinen Fehler gegeben hat, muss ich am Ende `Ok(())` zurückgeben.

    Jetzt möchte ich zwei Listen (in Rust "Vektoren") erstellen, in denen später meine Baywatch-Daten gespeichert werden sollen. Weil das zwei Listen von Wörtern oder Fragezeichen sind, werde ich sie als Vektor von Strings (also Listen von Zeichenketten) anlegen:

      let mut luecken_liste = Vec::::new();
      let mut vollst_liste = Vec::::new();

    Das Schlüsselwort `mut` sagt, dass die Listen "mutable" sind, also "veränderbar".

    Um gleich die Datei öffnen zu können, müssen wir zunächst noch drei Module in unsere Datei importieren, die uns helfen mit Dateien zu arbeiten. Dazu fügen wir ganz oben in unserer Datei noch ein:

    use std::fs::File;
    use std::io::BufRead;
    use std::io::BufReader;

    Jetzt muss ich zunächst meine Datei öffnen, damit ich dann den Inhalt der Datei einlesen kann:

    let file = File::open("datei.txt")?;
    let file_reader = BufReader::new(&file);

    Das Fragezeichen `?` hinter `open` ist hier etwas besonders wichtiges und darf nicht weggelassen werden. Die Funktion `open` gibt nämlich nicht unbedingt eine Datei zurück, sondern kann auch eine Fehlermeldung zurückgeben. Mit dem Fragezeichen-Operator sagen wir Rust, dass wenn es einen Fehler beim Öffnen der Datei gegeben haben sollte, wir das Programm mit diesem Fehler beenden wollen. Damit können wir jetzt aber sicher sein, dass `file` wirklich eine geöffnete Datei enthält.

    Dann müssen wir aus dem `File`-Objekt noch ein `BufReader` machen, um den Inhalt der Datei Zeile-für-Zeile lesen zu können.

    Und genau das machen wir jetzt, indem wir mit einer `for` Schleife über alle Zeilen iterieren. Außerdem verwenden wir die Funktion `enumerate`, um zusätzlich zu den Zeilen auch die Zeilennummern zu erhalten:

      for (i, l) in file_reader.lines().enumerate() {
        // Inhalt der for-Schleife
      }

    In unserer Schleife sind jetzt `i` die Zeilennummer und `l` eine Zeichenkette, die unsere ganze Zeile enthält.

    Jetzt können wir die Zeile in einzelne Worte auftrennen indem wir die Zeile an jedem Leerzeichen mit `split` aufsplitten:

        let zeile = l?;
        let mut v = zeile.split(" ").map(|x| x.to_string());

    Weil es beim Einlesen jeder Zeile auch wieder zu Fehlern kommen kann, müssen wir auch hier wieder mit dem Fragezeichen-Operator `?` prüfen, ob ein Fehler aufgetreten ist. Falls nicht, wird der Inhalt der Zeile in der Variable `zeile` gespeichert. Mit `split(" ")` trennen wir die Zeile bei den Leerzeichen in einzelne Wörter auf. Allerdings erhalten wird dabei noch keine Strings, weshalb wir die Wörter alle noch mit `to_string()` in Strings konvertieren müssen. Am einfachsten geht das mit der `map` Methode, die die Konvertierung für alle Wörter durchführt.

    Jetzt können wir unsere Wörter in die beiden Listen `luecken_liste` und `vollst_liste` füllen. Damit wir aber die richtigen Wörter in die richtige Liste schreiben, müssen wir prüfen, in welcher Zeile wir uns befinden. Sind wir in der ersten Zeile, soll `luecken_liste` befüllt werden, in der zweiten Zeile soll `vollst_liste` befüllt werden. Sollten wir in die dritte Zeile kommen, brauchen wir nicht mehr weitermachen und können die Schleife mit `break` beenden. Da Rust beim Aufzählen mit 0 beginnt, müssen wir die Zeilenzahlen entsprechend als 0, 1 und 2 angeben. Die Prüfung können wir mit `match` machen, die Funktion `collect` sammelt die Wörter zu einem Vektor zusammen:

        match i {
          0 => { luecken_liste = v.collect(); }
          1 => { vollst_liste = v.collect(); }
          _ => break
        }

    Der Unterstrich `_` steht hier für jede andere Zahl, die nicht 0 oder 1 ist.

    Jetzt sind wir fertig. Die Listen sind mit den entsprechenden Worten befüllt. Um das zu testen, können wir uns den Inhalt der Listen ausgeben lassen:

      println!("Lückenliste:");
      for bucht in luecken_liste {
        println!("{}", bucht);
      }
      println!("Vollständige Liste:");
      for bucht in vollst_liste {
        println!("{}", bucht);
      }

    Jetzt ist das Programm zum Einlesen der Baywatch-Dateien fertig. Für den Rest der Aufgabe seid ihr selbst gefragt! :)

    Hier ist der komplette Quelltext des oben geschriebenen Programms nochmal zusammengefasst:

    use std::fs::File;
    use std::io::BufRead;
    use std::io::BufReader;

    fn main() -> std::io::Result<()> {
      let mut luecken_liste = Vec::::new();
      let mut vollst_liste = Vec::::new();
      let file = File::open("datei.txt")?;
      let file_reader = BufReader::new(&file);
      
      for (i, l) in file_reader.lines().enumerate() {
        let zeile = l?;
        let mut v = zeile.split(" ").map(|x| x.to_string());
        match i {
          0 => { luecken_liste = v.collect(); }
          1 => { vollst_liste = v.collect(); }
          _ => break
        }
      }
      
      println!("Lückenliste:");
      for bucht in luecken_liste {
        println!("{}", bucht);
      }
      println!("Vollständige Liste:");
      for bucht in vollst_liste {
        println!("{}", bucht);
      }
      Ok(())
    }

    Dieses Programm könnt ihr in eurem Rust-Projekt als "main.rs" speichern und dann mit `cargo run` ausführen. Alternativ könnt ihr den Quelltext auch mit `rustc ` direkt kompilieren und das dabei erzeugte Programm dann ausführen.

    Solltet ihr noch Fragen haben, könnt ihr sie gerne im Forum oder hier als Kommentar stellen!

0 Kommentare