Rust 的 regex crate 提供了完整的正規表示式功能,讓開發者能有效率地處理字串匹配、搜尋和替換等工作。使用 Regex::new 建立正規表示式物件後,可以透過 is_match 進行簡易的匹配判斷,或是使用 captures 擷取匹配的群組資訊。針對需要多次匹配的場景,find_iter 迭代器提供了更有效率的處理方式。此外,lazy_static 宏能幫助延遲初始化正規表示式物件,避免不必要的資源消耗。Unicode 字串的處理中,正規化至關重要。不同的 Unicode 編碼方式可能表示相同的字元,導致字串比較結果不一致。unicode-normalization 套件提供多種正規化形式,例如 NFC、NFD、NFKC 和 NFKD,確保字串在不同表示方式下仍能正確比較。Rust 的輸入輸出系統根據 ReadBufReadWrite 等特性,提供高度抽象化的介面。BufReader 則透過緩衝機制提升讀取效能,減少系統呼叫次數。

正規表示式在Rust中的應用與實踐

正規表示式(Regular Expression)是處理字串的強大工具,尤其在文字匹配、提取和替換方面具有廣泛的應用。在Rust中,regex crate提供了對正規表示式的支援,使得開發者能夠方便地在程式中使用正規表示式。

基本正規表示式使用

Regex值代表了一個已經解析好的正規表示式,準備好被使用。Regex::new建構函式嘗試將一個&str解析為正規表示式,並傳回一個Result

use regex::Regex;

let semver = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?").unwrap();
let haystack = r#"regex = "0.2.5""#;
assert!(semver.is_match(haystack));

內容解密:

  • Regex::new用於建立一個新的正規表示式例項。
  • r"..."是Rust中的原始字串語法,避免了反斜線的轉義問題。
  • semver.is_match(haystack)檢查haystack字串是否匹配semver正規表示式。

捕捉群組

Regex::captures方法搜尋字串中的第一個匹配,並傳回一個regex::Captures值,該值包含了表示式中每個群組的匹配資訊。

let captures = semver.captures(haystack).unwrap();
assert_eq!(&captures[0], "0.2.5");
assert_eq!(&captures[1], "0");
assert_eq!(&captures[2], "2");
assert_eq!(&captures[3], "5");

內容解密:

  • captures[0]傳回整個匹配的字串。
  • captures[1]captures[2]等傳回相應捕捉群組的匹配內容。
  • 如果請求的群組沒有匹配,索引Captures值將會panic。

迭代所有匹配

你可以使用find_iter迭代器遍歷字串中的所有匹配。

let haystack = "In the beginning, there was 1.0.0. \
                For a while, we used 1.0.1-beta, \
                but in the end, we settled on 1.2.4.";
let matches: Vec<&str> = semver.find_iter(haystack)
                               .map(|match_| match_.as_str())
                               .collect();
assert_eq!(matches, vec!["1.0.0", "1.0.1-beta", "1.2.4"]);

內容解密:

  • find_iter產生一個迭代器,遍歷字串中的所有非重疊匹配。
  • map用於將每個Match值轉換為其對應的字串切片。

延遲建立Regex值

使用lazy_static crate可以延遲建立靜態的Regex值,直到它第一次被使用。

#[macro_use]
extern crate lazy_static;

lazy_static! {
    static ref SEMVER: Regex = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?").unwrap();
}

內容解密:

  • lazy_static!宏用於宣告靜態變數,其初始化程式碼在第一次存取時執行。
  • 這種方式避免了在程式啟動時不必要的計算,並使得複雜的靜態變數初始化成為可能。

字串正規化

Unicode中,有些字元有多種表示方式,例如帶重音符號的字母可以被表示為一個單一字元或是一個基本字母加上一個重音符號。這兩種表示方式在Unicode中被視為等價,但在Rust的字串比較中卻被視為不同。

assert!("th\u{e9}" != "the\u{301}");

內容解密:

  • Unicode字元可以有多種表示方式,這使得直接比較字串變得複雜。
  • 在進行字串比較或將字串用作雜湊表鍵值時,需要將字串正規化為某種標準形式。

總之,正規表示式是處理字串的有力工具,而Rust的regex crate和lazy_static crate則提供了方便的使用介面和最佳化手段。同時,瞭解Unicode字元的表示方式對於正確處理字串至關重要。

Unicode 正規化與 Rust 中的輸入輸出處理

Unicode 為全球文字編碼標準,但不同的編碼方式可能導致看似相同的字串在位元層級上不同。幸好,Unicode 定義了正規化形式(Normalized Forms),確保語義等價的字串具有相同的正規化表示,從而使比較、正規化儲存等操作變得簡單。

正規化形式(Normalization Forms)

Unicode 定義了四種正規化形式,主要區別在於字元的組合(Composition)與分解(Decomposition)方式,以及是否考慮相容性等價(Compatibility Equivalence)。

  1. 組合與分解

    • 組合形式(Composed Form):盡可能將多個字元組合成單一字元。例如,越南語中的 “Phở” 可以表示為三個字元 “Ph\u{1edf}",其中 ‘\u{1edf}’ 是一個組合字元,代表帶有兩個變音符號的 “o”。
    • 分解形式(Decomposed Form):將組合字元分解為基本字元和變音符號。例如,“Phở” 被分解為 “Pho\u{31b}\u{309}",其中 ‘o’ 是基本字元,’\u{31b}’ 和 ‘\u{309}’ 分別是兩個變音符號。
  2. 相容性等價(Compatibility Equivalence)

    • Unicode 中有些字元雖然外觀不同,但被視為相容性等價,例如普通數字 ‘5’、上標數字 ‘⁵’ 和帶圓圈數字 ‘⑤’。相容性等價的字元在某些情況下(如搜尋)應被視為相同。
    • NFKC 和 NFKD 正規化形式會將相容性等價的字元統一為某種標準形式,而 NFC 和 NFD 則不會。

Rust 中的 Unicode 正規化

Rust 的 unicode-normalization 套件提供了對 Unicode 字串進行正規化的功能。

使用方法

首先,在 Cargo.toml 中加入以下依賴:

[dependencies]
unicode-normalization = "0.1.5"

然後在程式碼中引入該套件:

extern crate unicode_normalization;
use unicode_normalization::UnicodeNormalization;

fn main() {
    assert_eq!("Phở".nfd().collect::<String>(), "Pho\u{31b}\u{309}");
    assert_eq!("Phở".nfc().collect::<String>(), "Ph\u{1edf}");
    assert_eq!("① Di\u{fb03}culty".nfkc().collect::<String>(), "1 Difficulty");
}

輸入輸出(Input and Output)

Rust 的標準函式庫中,輸入輸出功能圍繞著三個特性(trait):ReadBufReadWrite

  • Read trait:提供位元導向的輸入方法,被稱為讀取器(Readers)。
  • BufRead trait:繼承自 Read,並提供緩衝區功能,可用於讀取行等操作。
  • Write trait:支援位元和 UTF-8 文字輸出,被稱為寫入器(Writers)。

圖示:輸入輸出特性與實作範例

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Rust正規表示式應用與Unicode正規化

package "Rust 記憶體管理" {
    package "所有權系統" {
        component [Owner] as owner
        component [Borrower &T] as borrow
        component [Mutable &mut T] as mutborrow
    }

    package "生命週期" {
        component [Lifetime 'a] as lifetime
        component [Static 'static] as static_lt
    }

    package "智慧指標" {
        component [Box<T>] as box
        component [Rc<T>] as rc
        component [Arc<T>] as arc
        component [RefCell<T>] as refcell
    }
}

package "記憶體區域" {
    component [Stack] as stack
    component [Heap] as heap
}

owner --> borrow : 不可變借用
owner --> mutborrow : 可變借用
owner --> lifetime : 生命週期標註
box --> heap : 堆積分配
rc --> heap : 引用計數
arc --> heap : 原子引用計數
stack --> owner : 棧上分配

note right of owner
  每個值只有一個所有者
  所有者離開作用域時值被釋放
end note

@enduml

此圖示顯示了 Rust 中主要的輸入輸出特性及其常見實作範例。

詳細解說

  • Read 是最基本的輸入特性,所有實作 Read 的型別都支援位元級別的讀取操作。
  • BufReadRead 的基礎上增加了緩衝區的功能,使得按行讀取等操作更加高效。
  • Write 特性則用於輸出,支援位元和 UTF-8 文字的寫入。

輸入輸出範例

use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() {
    let file = File::open("example.txt").expect("無法開啟檔案");
    let reader = BufReader::new(file);

    for line in reader.lines() {
        match line {
            Ok(line) => println!("{}", line),
            Err(err) => println!("讀取錯誤:{}", err),
        }
    }
}

內容解密:

  1. 我們使用 std::fs::File 模組來開啟一個檔案,並使用 BufReader 對其進行緩衝讀取。
  2. BufReader::new(file) 建立了一個帶有緩衝區的讀取器,這使得按行讀取更加高效。
  3. reader.lines() 傳回一個迭代器,每次迭代傳回一行文字。
  4. 我們使用 match 對可能的錯誤進行處理,如果讀取成功,則印出該行文字;如果發生錯誤,則印出錯誤訊息。

讀取器與寫入器

在 Rust 程式語言中,讀取器(Readers)與寫入器(Writers)是處理輸入輸出的基本元件。讀取器允許程式從不同的資料來源讀取位元組,而寫入器則允許程式將位元組寫入不同的資料目的地。

讀取器

讀取器是程式可以從中讀取位元組的值。常見的讀取器例子包括:

  • 使用 std::fs::File::open(filename) 開啟的檔案
  • std::net::TcpStream,用於接收網路資料
  • std::io::stdin(),用於從程式的標準輸入串流讀取資料
  • std::io::Cursor<&[u8]> 值,這是一種可以從記憶體中的位元組陣列讀取的讀取器

寫入器

寫入器是程式可以寫入位元組的值。常見的寫入器例子包括:

  • 使用 std::fs::File::create(filename) 開啟的檔案
  • std::net::TcpStream,用於傳送網路資料
  • std::io::stdout()std::io::stderr(),用於寫入終端機
  • std::io::Cursor<&mut [u8]> 值,這允許將任何可變的位元組片段視為檔案進行寫入
  • Vec<u8>,一種寫入器,其寫入方法會附加到向量

通用程式碼

由於 Rust 提供了標準的 ReadWrite 特性,因此可以撰寫跨多種輸入輸出通道的通用程式碼。例如,以下是一個將任何讀取器的所有位元組複製到任何寫入器的函式:

use std::io::{self, Read, Write, ErrorKind};

const DEFAULT_BUF_SIZE: usize = 8 * 1024;

pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W) -> io::Result<u64>
where
    R: Read,
    W: Write,
{
    let mut buf = [0; DEFAULT_BUF_SIZE];
    let mut written = 0;
    loop {
        let len = match reader.read(&mut buf) {
            Ok(0) => return Ok(written),
            Ok(len) => len,
            Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
            Err(e) => return Err(e),
        };
        writer.write_all(&buf[..len])?;
        written += len as u64;
    }
}

程式碼解密:

  1. 函式簽名copy 函式接受兩個引數:readerwriter,分別代表讀取器和寫入器。它們必須實作 ReadWrite 特性。
  2. 緩衝區設定:使用一個大小為 DEFAULT_BUF_SIZE 的緩衝區來暫存從讀取器讀取的資料。
  3. 迴圈讀取和寫入:在迴圈中,不斷從讀取器讀取資料到緩衝區,然後將緩衝區中的資料寫入寫入器,直到沒有更多資料可讀。
  4. 錯誤處理:對讀取過程中可能發生的錯誤進行處理,包括被中斷的情況,並在必要時重試。
  5. 結果傳回:成功時傳回總共寫入的位元組數;失敗時傳回錯誤。

讀取器的主要方法

std::io::Read 特性提供了多個用於讀取資料的方法,包括:

  • read(&mut buffer):從資料來源讀取一些位元組並儲存在給定的緩衝區中。
  • read_to_end(&mut byte_vec):讀取此讀取器的所有剩餘輸入,並將其附加到 byte_vec 中。
  • read_to_string(&mut string):與上述類別似,但將資料附加到給定的 String 中,如果串流不是有效的 UTF-8,則傳回錯誤。
  • read_exact(&mut buf):準確讀取足夠的資料以填充給定的緩衝區。

這些方法都有預設實作,並且能夠處理 ErrorKind::Interrupted 錯誤,使用起來更加方便。

Rust 中的讀取器與緩衝讀取器

在 Rust 中,輸入/輸出操作是透過 ReadWrite 特徵(trait)來實作的。其中,Read 特徵用於從源讀取資料,而 Write 特徵則用於向目標寫入資料。在本章節中,我們將重點介紹 Read 特徵及其相關型別,尤其是緩衝讀取器(Buffered Reader)的使用。

讀取器(Readers)

Read 特徵是 Rust 中用於從源讀取資料的基本介面。它定義了一系列方法,允許程式從不同的源(如檔案、標準輸入、網路連線等)讀取資料。主要的 Read 方法包括:

  • read(&mut self, buf: &mut [u8]) -> Result<usize>:從源讀取資料到提供的緩衝區 buf 中,並傳回讀取的位元組數。
  • read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize>:持續從源讀取資料,直到到達源的末尾,並將所有讀取的資料追加到 buf 中。

內容解密:

  • read 方法是 Read 特徵的核心,它允許程式從源讀取資料。這個方法是阻塞的,意味著它會等待直到有資料可讀或發生錯誤。
  • read_to_end 方法提供了一種便捷的方式來讀取源中的所有資料,直到源被完全耗盡。

緩衝讀取器(Buffered Readers)

為了提高效率,Rust 提供了緩衝讀取器。緩衝讀取器內部維護了一個緩衝區,用於暫存從源讀取的資料。這樣可以減少對源的存取次數,因為每次存取源通常涉及到系統呼叫,而系統呼叫是相對昂貴的操作。

使用緩衝讀取器

要使用緩衝讀取器,你需要使用 BufReader 型別。BufReader 包裝了一個實作 Read 特徵的源,並提供了緩衝功能。下面是一個例子,展示瞭如何使用 BufReader 來讀取檔案的內容:

use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() -> std::io::Result<()> {
    let file = File::open("example.txt")?;
    let reader = BufReader::new(file);

    for line in reader.lines() {
        match line {
            Ok(line) => println!("{}", line),
            Err(err) => println!("Error reading line: {}", err),
        }
    }

    Ok(())
}

內容解密:

  • 首先,我們開啟一個檔案並建立一個 File 物件。
  • 然後,我們使用 BufReader::new(file) 建立了一個 BufReader 例項,將 File 物件包裝起來。
  • 透過呼叫 reader.lines(),我們獲得了一個迭代器,該迭代器逐行讀取檔案的內容。
  • 在迴圈中,我們處理每一行,如果讀取成功,則列印預出來;如果發生錯誤,則列印錯誤資訊。

grep 程式範例

下面是一個使用 BufReader 實作的簡單 grep 程式,用於在輸入中搜尋指定的字串:

use std::io::{self, BufReader, BufRead};

fn grep(target: &str) -> io::Result<()> {
    let stdin = io::stdin();
    let reader = stdin.lock();
    for line_result in reader.lines() {
        let line = line_result?;
        if line.contains(target) {
            println!("{}", line);
        }
    }
    Ok(())
}

fn main() -> io::Result<()> {
    grep("search_string")?;
    Ok(())
}

內容解密:

  • grep 函式接受一個目標字串,並從標準輸入中逐行讀取內容。
  • 使用 io::stdin().lock() 獲得對標準輸入的鎖定,以確保執行緒安全。
  • 對每一行,如果包含目標字串,則列印該行。

泛型 grep 函式

透過使 grep 函式泛型化,我們可以使其適用於任何實作了 BufRead 特徵的型別,而不僅僅是標準輸入:

fn grep<R: BufRead>(target: &str, reader: R) -> io::Result<()> {
    for line_result in reader.lines() {
        let line = line_result?;
        if line.contains(target) {
            println!("{}", line);
        }
    }
    Ok(())
}

內容解密:

  • grep 函式修改為泛型,使其接受任何實作 BufRead 的型別 R
  • 這樣,我們既可以將其用於標準輸入,也可以將其用於檔案的緩衝讀取器。

本章介紹了 Rust 中輸入/輸出操作的基本構件,包括 Read 特徵和緩衝讀取器的使用。透過使用這些工具,你可以高效地處理輸入/輸出操作,並編寫出既安全又高效的程式。