rust學習 - 構建mini 命令行工具

来源:https://www.cnblogs.com/dreamHot/archive/2023/06/08/17467837.html
-Advertisement-
Play Games

rust 的運行速度、安全性、單二進位文件輸出和跨平臺支持使其成為構建命令行程式的最佳選擇。 實現一個命令行搜索工具`grep`,可以在指定文件中搜索指定的字元串。想實現這個功能呢,可以按照以下邏輯流程處理: 1. 獲取輸入文件路徑、需要搜索的字元串 2. 讀取文件; 3. 在文件內容中查找字元串所 ...


rust 的運行速度、安全性、單二進位文件輸出和跨平臺支持使其成為構建命令行程式的最佳選擇。

實現一個命令行搜索工具grep,可以在指定文件中搜索指定的字元串。想實現這個功能呢,可以按照以下邏輯流程處理:

  1. 獲取輸入文件路徑、需要搜索的字元串
  2. 讀取文件;
  3. 在文件內容中查找字元串所在的行
  4. 列印包含字元串所在的行信息

創建項目ifun-grep

$> cargo new ifun-grep

項目在運行時,可以獲取到傳遞的參數。比如cargo run -- hboot hello.txt,在文件hello.txt查找字元串hboot

讀取參數

首先要先獲取到傳入的參數。通過標準庫std::env::args獲取

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    dbg!(args);
}

collect()方法可以將傳入的參數轉換為一個集合。對於變數args必須註明集合類型。

args-print.jpg

參數的第一個值是二進位文件的名稱。可以用於程式調試或者列印出文件路徑,取出另外兩個參數,保存進對應的變數。方便後續傳參數使用。

let search = &args[1];
let file_path = &args[2];

println!("will search {} in {}", search, file_path)

讀取文件

首先創建測試文件hello.txt,並寫入一段文字。

獨立寒秋,湘江北去,橘子洲頭。

看萬山紅遍,層林盡染;漫江碧透,百舸爭流。

鷹擊長空,魚翔淺底,萬類霜天競自由。

悵寥廓,問蒼茫大地,誰主沉浮?

攜來百侶曾游,憶往昔崢嶸歲月稠。

恰同學少年,風華正茂;書生意氣,揮斥方遒。

指點江山,激揚文字,糞土當年萬戶侯。

曾記否,到中流擊水,浪遏飛舟

讀取文件,並列印出文件中的內容。

let content = fs::read_to_string(file_path).expect("you should permission to read the file");

println!("read the content:\n{content}")

通過fs模塊的read_to_string方法讀取文件內容。expect則用於處理讀取文件時發生的錯誤的提示信息,這在下麵的錯誤處理會有說明。

模塊拆分與錯誤處理

現在所有的處理業務都放在src/main.rs中。取參和讀取文件是兩個不同功能的邏輯處理,當功能越來越複雜的時候,就應該關註分離。這在我們設計時可提前考慮好

main.rs只被用來處理程式的執行。其他需要處理的邏輯則可以放在srr/lib.rs中。

定義一個解析取參的函數parse_args,現在仍然定義在src/main.rs中。

fn parse_args(args: &Vec<String>) -> (&str, &str) {
    let search = &args[1];
    let file_path = &args[2];

    (search, file_path)
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let (search, file_path) = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

這樣main函數不再處理哪個參數對應哪個變數。

我們可以將這一組相關的變數通過結構體定義相互關聯起來。這樣函數返回將不再使用元組,並且可以通過結構體實例可以訪問到每一個屬性。

struct Config {
    search: String,
    file_path: String,
}

fn parse_args(args: &Vec<String>) -> Config {
    let search = args[1].clone();
    let file_path = args[2].clone();

    Config { search, file_path }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

在結構體中,實例化賦值需要擁有這些變數值的所有權。而變數args是所有權的擁有者,通過clone()方法拷貝一份數據。

可以看到parse_args返回來一個結構體 Config 的實例,可以通過定義結構體的內部方法來創建實例。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        let search = args[1].clone();
        let file_path = args[2].clone();

        Config { search, file_path }
    }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("will search {} in {}", search, file_path);
}

這樣,就不需要parse_args函數了,通過結構體的內部方法實例化實例。

錯誤處理

如果我們執行cargo run時,不傳遞任何參數,則程式會報錯。這樣的提示對於用戶並不友好。

首先可以通過判斷參數需要的參數信息,說明錯誤信息。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        if args.len() < 3 {
            panic!("至少傳入2個參數")
        }
        // ...
    }
}

提示用戶必須傳入 2 個從參數,因為有一個預設的路徑參數。所以判斷不能少於3

除了直接提示錯誤信息並中斷程式,也可以使用Result傳遞錯誤,讓主函數做決定如何去處理。

impl Config {
    fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少傳入2個參數");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

現在提供了一個方法build來處理這個邏輯,之前的new不用了(這裡是語義話定義,new常常表示不會產生錯誤),當有錯誤時,不是直接終止程式,而是返回一個Err值。

src/main.rs中調用並處理結果。對於錯誤信息給用戶輸出有好的提示信息,並以非零錯誤process::exit(1)退出命令行。

use std::{env, fs, process};

fn main(){
    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("error occurred parseing args:{err}");
        process::exit(1);
    });

    // ...
}

unwrap_or_else可以進行自定義錯誤處理。這是一個閉包,它調用內部的匿名函數,並通過|err|傳遞的參數供內部使用。當返回Ok時,則返回內部的值。

提取讀取文件的邏輯

參數的取參邏輯經由結構體內部方法處理。現在吧文件讀取的邏輯提取出來,並採用傳遞錯誤的方式Result返回錯誤信息。

use std::error::Error;

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    println!("read the content:\n{content}");

    Ok(())
}

使用了 trait 對象 Box<dyn Error>返回實現Errortrait 的類型,不用指定具體的錯誤類型。靈活性更高dyn表示動態的

接著可以在主函數中調用run()函數,並處理可能出現的錯誤。

fn main (){
    // ...

    if let Err(e) = run(config) {
        println!("something error:{e}");
        process::exit(1);
    }
}

拆分代碼到庫

以上定義了結構體,處理取參函數;拆離了讀取文件邏輯。但是這些都是在src/main.rs中,有複雜邏輯時,這會讓文件行數很多,看起來很讓人頭疼。

將這一部分拆離的放到其他文件中去。新建src/lib.rs,將這些定義移動到該文件中。

use std::error::Error;
use std::fs;

pub struct Config {
    pub search: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少傳入2個參數");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    println!("read the content:\n{content}");

    Ok(())
}

可以看到通過pub將這些結構體、函數都公有化。包括結構里的欄位,這就是一個可以測試的公有 API 的 crate 庫。

然後再src/main.rs需要導入

use ifun_grep::{run, Config};

通過use引入作用域。ifun-grep是項目名稱,作為首碼。

增加測試

通過測試驅動開發的模式來逐漸增加邏輯。期望從給定的內容中查找出字元串,並列印出所在行。

src/lib.rs增加測試示例

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn on_result() {
        let search = "hboot";
        let content = "\
nice. rust
I'm hboot.
hello world.
";

        assert_eq!(vec!["I'm hboot."], find(search, content));
    }
}

搜索字元串hboot,它在文本的第二行。所以期待搜索輸出結果為I'm hboot.

提供一個find函數,用於處理搜索邏輯,先不寫搜索邏輯,返回一個空的結果值。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    vec![]
}

利用顯示生命周期'a來表明參數content參數與返回值的生命周期相關聯。它們存在的時間一樣久

執行測試cargo test,理所應當的輸出失敗,結果返回了一個空的vec![],和預期不匹配。

增加搜索邏輯,按行執行過濾,包含指定的字元串,則存儲在結果中。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    for line in content.lines() {
        if line.contains(search) {
            // 符合,包含了指定字元串
            result.push(line);
        }
    }

    result
}

通過迭代器遍歷給定文本內容lines().字元串判斷是否包含contains()方法。將結果值放進result中,並返回。

測試用例測試沒有問題,完善一下run函數,搜索出符合的內容並列印出來

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    for line in find(&config.search, &content) {
        println!("{line}");
    }

    Ok(())
}

執行腳本cargo run -- 山 hello.txt,可以看到列印輸出兩行

run-success.png

增加環境變數

功能已經到到預期,可以搜索出想要包含字元串的文本段落。增加一個額外的功能大小寫敏感處理環境變數,當然也可以通過再多傳一個參數處理。

更改文本內容為應為

Let life be beautiful like summer flowers.

The world has kissed my soul with its pain.

Eyes are raining for her.

you also miss the stars.

先測試當前程式是否大小寫敏感,文本中首個英文單詞是大寫的,按照小寫搜索

$> cargo run -- let hello.txt

沒有任何的列印輸出,說明當前的搜索邏輯是大小寫敏感的,通過傳遞變數來控制邏輯,修改測試用例,增加兩個測試示例:大小寫敏感和不敏感測試。

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn case_sensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust"], find(search, content));
    }

    #[test]
    fn case_insensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust", "Rust"], find_insensitive(search, content));
    }
}

原來的函數find大小寫敏感,邏輯不變。增加一個大小寫不敏感的函數find_insensitive,在處理搜索時,查詢的字元和被搜索的文本行都轉小寫後,然後在執行查找。

pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    // 搜索 字元串轉小寫
    let search = search.to_lowercase();

    for line in content.lines() {
        // 文本行內容轉小寫
        if line.to_lowercase().contains(&search) {
            // 符合,包含了指定字元串
            result.push(line);
        }
    }

    result
}

多了一個操作to_lowercase()將文本內容轉成小寫。to_lowercase()會新創建一個 String,contains()方法參數需要的是一個引用。

再次執行測試cargo teset.用例全部通過。邏輯寫好了,需要通過增加一個配置來處理是否大小寫敏感。

修改結構體定義ingore_case表示來忽略大小寫。

pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

通過ingore_case欄位判斷是否調用哪個函數,修改run函數

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    let mut result = vec![];
    if config.ignore_case {
        result = find_insensitive(&config.search, &content)
    } else {
        result = find(&config.search, &content)
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

處理接受變數IGNORE_CASE,通過庫std::env處理環境變數。

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少傳入2個參數");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            search,
            file_path,
            ignore_case,
        })
    }
}

env::var()返回值為 Result 類型,通過它自己的方法is_ok()判斷什麼狀態,如果設置值則返回 true;未設置則返回 false。

進行測試,不設置變數時,查詢小寫的let是查詢不到的,因為首寫的因為單詞字母是大些的。

$> cargo run -- let hello.txt

test-case-sensitive.png

通過設置環境變數,執行程式

$> IGNORE_CASE=1 cargo run -- let hello.txt

可以查到目標文本內容。

teset-case-insensitive.png

錯誤信息處理

我們所預先知道的錯誤信息都通過程式執行println!列印在控制台,這是一種標準輸出.

對於出現錯誤信息,希望它即時列印輸出,而對於程式執行的結果記錄下來,保存到文件中,方便查看。

現在使用println!標準輸出流重定向到文件中,它會將錯誤信息也保存到起來,且不會列印。

$> cargo run >output.txt

屏幕上沒有任何輸出,以為程式執行正常,其實文件中的內容是error occurred parseing args:至少傳入2個參數

這就造成了一個問題,不管成功、失敗,只有打開文件才能看到。錯誤輸出使用標準錯誤展示用於錯誤信息,將錯誤列印的println!改為eprintln!

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        // println!("error occurred parseing args:{err}");
        eprintln!("error occurred parseing args:{err}");
        process::exit(1);
    });
    println!("will search {} in {}", config.search, config.file_path);

    if let Err(e) = run(config) {
        // println!("something error:{e}");
        eprintln!("something error:{e}");
        process::exit(1);
    }
}

重新執行cargo run >output.txt,錯誤列印到控制台,而文件output.txt沒有輸出。

再執行,可以查到數據的命令cargo run -- Let hello.txt > output.txt,查看output.txt,可以看到預期的查找到的內容在文件中。

發佈 crate 到Crate.io

crates.io 庫,可以這裡找找想要的功能庫,也可以將自己的 crate 發佈到這裡。

Rust 的發佈配置都有一套預設的、可定製的配置。

  • cargo build 採用的是 dev 配置構建程式
  • cargo build --release 是 release 配置,有更好的發佈構建的配置

可以在文件Cargo.toml中通過[profile.*]修改設置預設值。

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

dev 構建和發佈構建定義不同的優化等級。opt-level定義何種程度優化,0-3可配置值。dev 預設為 0,release 預設為 3.

如果想 dev 模式下需要一些優化,則可以更改為

[profile.dev]
opt-level = 1

增加文檔註釋

一個好的模塊包,是有很好的文檔說明,以方便其他人輕易上手。通過文檔註釋///已支持 markdown 格式化文本。

給每一個函數增加註釋說明,這裡只展示部分。

/// the struct `Config` defines command line params.
///
/// # Example
///
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// ```
pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

/// the fun is used to execute search
///
/// # example
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// let result = ifun_grep::run(config);
///
/// assert!(result.is_ok());
/// ```
///
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    let result;
    if config.ignore_case {
        result = find_insensitive(&config.search, &content);
    } else {
        result = find(&config.search, &content);
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

在使用 vscode 時,註釋文檔上方會有一個執行操作 run doctest。可以單獨執行當前寫的測試示例是否可以通過執行。

也可以通過cargo test來測試所有的測試示例。不僅會執行mod test的測試示例,也會執行doc test的註釋測試示例。

通過命令cargo doc --open來生成線上文檔。

$> cargo doc --open

crate-doc.png

可以通過//!對當前文件進行註釋說明,必須是在第一行。

//! ifun_grep is a string search library
//!
//! Supports case sensitive search.
//!

註冊 crate.io 賬戶併發布

目前只能使用 github 賬號進行授權登錄。在個人賬號信息中,API Tokens生成 token 授權操作。

$> cargo login 你的token

如果登錄不成功,看下提示錯誤,我是加了參數--registry crates-io才成功的。

$> cargo login 你的token --registry crates-io

登錄之後就可以發佈了,通過Cargo.toml增加一些倉庫元信息,比如倉庫名、作者、開源協議、描述等等。

$> cargo publish

發佈之前需要驗證你登錄的賬號郵箱,不然發佈不了。個人的元信息有幾項是必填的,包括name\version\description\license

發佈時,如果發佈不成功,看錯誤提示,可能還需要加--registry crates-io

撤銷某個版本

如果你發佈的版本有很大的問題,可以撤銷改版本。不能刪除倉庫,已發佈的代碼時永久存在的,只能通過撤銷來阻止其他項目引用它。

$> cargo yank --vers 0.1.0

使得當前版本不可用。也可以恢復當前版本的使用

$> cargo yank --vers 0.1.0 --undo
追逐的不應該是夢想,隨心所欲,隨遇而安!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:本文將探索內核中解析PE文件的相關內容。 本文分享自華為雲社區《驅動開發:內核PE結構VA與FOA轉換》,作者: LyShark 。 本章將探索內核中解析PE文件的相關內容,PE文件中FOA與VA、RVA之間的轉換也是很重要的,所謂的FOA是文件中的地址,VA則是記憶體裝入後的虛擬地址,RVA是 ...
  • WFP框架是微軟推出來替代TDIHOOK傳輸層驅動介面網路通信的方案,其預設被設計為分層結構,該框架分別提供了用戶態與內核態相同的AIP函數,在兩種模式下均可以開發防火牆產品,以下代碼我實現了一個簡單的驅動過濾防火牆。WFP 框架分為兩大層次模塊,用戶態基礎過濾引擎`BFE (BaseFilteri... ...
  • *以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接[ 微信公眾號「ENG八戒」](https://mp.weixin.qq.com/s/Xd_FwT8E8Yx9Vnb64h6C8w) > 帶給現代 C++ 性能飛躍的特性很多,今天一邊聊技術,一邊送福利! ![](https://img2023. ...
  • 環境:CentOS 7.6_x64 Python版本 :3.9.12 pjsip版本:2.13 一、背景描述 pjsip地址:https://www.pjsip.org/ GitHub地址:https://github.com/pjsip/pjproject pjsip文檔地址:https://do ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第9面: > 面試官:C++中,設計一個類要註意哪些東西? > > 二師兄:設計一個類主要考慮以下幾個方面:1.面向對象的封裝、繼承及多態。2.`big three`或者`big five`。3.運算符和函數重載、靜態成員、友元、異常處理等相關問題。 ...
  • 哈嘍大家好,我是鹹魚 好久沒更新 python 爬蟲相關的文章了,今天我們使用 selenium 模塊來簡單寫個爬蟲程式——爬取某東網商品信息 網址鏈接:https://www.jd.com/ 完整源碼在文章最後 ## 元素定位 我們需要找到網頁上元素的位置信息(xpth 路徑) ![image]( ...
  • ## 前言 在C語言中,枚舉是一種方便組織和表示一組相關常量的工具。枚舉類型有助於提高代碼的可讀性和可維護性。本文將介紹C語言枚舉的基本概念、語法和用法,以及一些高級技巧。 ## 一、人物簡介 - 第一位閃亮登場,有請今後會一直教我們C語言的老師 —— 自在。 ![](https://img2023 ...
  • | static基本知識 | header | | | | | | | 類名.靜態成員變數(推薦) 同一個類中靜態成員變數的訪問可以省略類名。 1.靜態成員變數(有static修飾,屬於類、載入一次,可以被共用訪問),訪問格式 類名.靜態成員變數(推薦) 對象.靜態成員變數(不推薦)。 2.實例成員 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...