Rust文本處理快速入門
編程過程中有許多類型的數(shù)據(jù)要處理,其中文本處理必不可少,本文主要是記錄在使用Rust開發(fā)的過程中處理文本相關(guān)數(shù)據(jù)的一些代碼,而文本可以分為結(jié)構(gòu)化和非結(jié)構(gòu)化的文本,比如JSON和小說文本(沒有固定格式的文本)。
這里以兩種格式文本為例
- Nginx的訪問日志
- Caddy的訪問日志
為了不使文章過于冗長,大家可以根據(jù)自己需要將下面的數(shù)據(jù)復(fù)制成多行,然后自行測試, 或者問ChatGPT之類的AI給你生成一些樣本數(shù)據(jù), 比如問AI問題:"給我十條NGINX的訪問日志樣本數(shù)據(jù)"。
nginx的訪問日志測試樣本如下:
172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] "GET /hello HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] "GET /hello HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
上面的日志對應(yīng)的日志格式如下:
'$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"';
caddy的訪問日志測試樣本如下:
{"level":"info","ts":1683783840.9822006,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"::1","remote_port":"56352","proto":"HTTP/1.1","method":"GET","host":"localhost:20023","uri":"/","headers":{"Accept":["*/*"],"User-Agent":["curl/7.29.0"]}},"user_id":"","duration":0.000221154,"size":17060,"status":200,"resp_headers":{"Server":["Caddy"],"Etag":["\"rudac9d5w\""],"Content-Type":["text/html; charset=utf-8"],"Last-Modified":["Tue, 09 May 2023 01:19:21 GMT"],"Accept-Ranges":["bytes"],"Content-Length":["17060"]}}
{"level":"info","ts":1683783841.9822006,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"::1","remote_port":"56352","proto":"HTTP/1.1","method":"GET","host":"localhost:20023","uri":"/hello","headers":{"Accept":["*/*"],"User-Agent":["curl/7.29.0"]}},"user_id":"","duration":0.000221154,"size":17060,"status":200,"resp_headers":{"Server":["Caddy"],"Etag":["\"rudac9d5w\""],"Content-Type":["text/html; charset=utf-8"],"Last-Modified":["Tue, 09 May 2023 01:19:21 GMT"],"Accept-Ranges":["bytes"],"Content-Length":["17060"]}}
{"level":"info","ts":1683783841.9822006,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"::1","remote_port":"56352","proto":"HTTP/1.1","method":"GET","host":"localhost:20023","uri":"/hello","headers":{"Accept":["*/*"],"User-Agent":["curl/7.29.0"]}},"user_id":"","duration":0.000221154,"size":17060,"status":200,"resp_headers":{"Server":["Caddy"],"Etag":["\"rudac9d5w\""],"Content-Type":["text/html; charset=utf-8"],"Last-Modified":["Tue, 09 May 2023 01:19:21 GMT"],"Accept-Ranges":["bytes"],"Content-Length":["17060"]}}
Caddy的訪問日志是JSON格式,就不需要什么額外的說明了。
本文代碼的所有Rust依賴如下:
因?yàn)镽ust的標(biāo)準(zhǔn)庫非常精簡(簡陋), 所以很多操作都需要借助第三方庫,比如這里處理JSON的庫serde.
[dependencies] encoding_rs = "0.8.33" regex = "1.10.2" serde_json = "1.0.108"
快速入門
假設(shè)我們的任務(wù)是統(tǒng)計日志中每個URL的訪問次數(shù)。
Caddy日志解析
Caddy的日志格式是每行都是一個合法的JSON格式的文本,所以直接使用serde_json處理即可。
// https://youerning.top/post/rust-text-processing-tutorial/ use std::collections::HashMap; use std::io::BufRead; use std::io::BufReader; use std::io::Result; use std::fs::File; use serde_json::Value; fn main() -> Result<()>{ let filepath = "caddy.log"; let file = File::open(filepath)?; let reader = BufReader::new(file); let mut url_counter = HashMap::new(); for line in reader.lines() { match line { Ok(line) => { // println!("line: {line}"); if let Err(_) = serde_json::from_str::<Value>(&line) { continue } let data: Value = serde_json::from_str(&line).unwrap(); if let None = data.get("request") { continue } // 這樣的代碼太形式化了,應(yīng)該有類似于GJSON之類的庫, 不夠我沒有用過 // 所以這里就這樣吧, 后文用展開宏節(jié)省一下代碼。 // 其實(shí)這里也可以用Options的and_then方法,但是還需要寫一個匿名函數(shù),不是很喜歡。 if let None = data.get("request").unwrap().get("uri") { continue } let uri = data.get("request").unwrap().get("uri").unwrap(); if let None = uri.as_str() { continue } let uri = uri.as_str().unwrap(); // *url_counter.entry(uri.to_owned()).or_insert(0) += 1; let v = url_counter.entry(uri.to_owned()).or_insert(0); *v += 1; }, Err(err) => { return Err(err) } } } println!("url_counter: {url_counter:?}"); Ok(()) }
Nginx日志解析
類似于Nginx這樣的純文本格式,必須得預(yù)先知道文本的格式,這可以通過肉眼觀察或者查看輸出端的配置來了解格式,不然的話沒辦法精確的處理,至少是不能將每個字段的值剝離出來。
根據(jù)觀察或者說查看Nginx的配置文件,我們知道我們要取的數(shù)據(jù)在第一個用雙引號""包裹起來的字符串內(nèi), 比如"GET / HTTP/1.1"。
解析文本有很多辦法,大致分為兩種,使用正則表達(dá)式或者不使用正則表達(dá)式,這里選擇的方法是不使用正則表達(dá)式,因?yàn)檎齽t表達(dá)式的維護(hù)難度有點(diǎn)大。
// https://youerning.top/post/rust-text-processing-tutorial/ use std::collections::HashMap; use std::io::BufRead; use std::io::BufReader; use std::io::Result; use std::fs::File; fn main() -> Result<()>{ let filepath = "nginx.log"; let file = File::open(filepath)?; let reader = BufReader::new(file); let mut url_counter = HashMap::new(); for line in reader.lines() { match line { Ok(line) => { // println!("line: {line}"); let spilts:Vec<&str> = line.split_whitespace().collect(); if spilts.len() < 13 { continue } // 注意: 這里不會考慮包含代理的日志記錄 // 如果是代理的日志記錄可能是 http://xxxx:xxx/abc這種格式 if !spilts.get(6).unwrap().starts_with("/") { continue } let uri = *spilts.get(6).unwrap(); // *url_counter.entry(uri.to_owned()).or_insert(0) += 1; let v = url_counter.entry(uri.to_owned()).or_insert(0); *v += 1; }, Err(err) => { return Err(err) } } } println!("url_counter: {url_counter:?}"); Ok(()) }
兩個的代碼結(jié)果應(yīng)該都是如下:
url_counter: {"/": 1, "/hello": 2}
文件讀取
一般來說文本都是以文件的形式存在的,這里討論的也主要是以文件形式存在的文本,至于網(wǎng)絡(luò)數(shù)據(jù)的文本需要根據(jù)對應(yīng)的協(xié)議來處理了。
獲取文件句柄(打開文件)
在讀取文本之前自然是需要先打開文件或者說獲得文件句柄的。
如果只關(guān)心打不打得開,那么可以直接通過問號?操作符將錯誤直接往外拋。
use std::io::Result; use std::fs::File; fn main() -> Result<()>{ let filepath = "caddy.log"; let file = File::open(filepath)?; Ok(()) }
如果我們關(guān)心錯誤,那么可以用模式匹配判斷一下, **io::Error有很多類型的, 這里僅判斷了不存在的類型 **
use std::io::{Result, ErrorKind}; use std::fs::File; fn main() -> Result<()>{ let filepath = "caddy.log"; let file = match File::open(filepath) { Ok(file) => file, Err(err) => { if err.kind() == ErrorKind::NotFound{ println!("文件不存在"); } return Err(err) } }; Ok(()) }
如果只是判斷文件不存在還有一些簡單的方法,比如:
use std::path::Path; fn main() { let path = Path::new("caddy.logx"); if !path.exists() { println!("文件不存在"); } }
編碼
當(dāng)獲取了文件句柄就可以讀取文件內(nèi)容了,但是我們總要時刻注意文件的編碼是什么,默認(rèn)情況下Rust提供的一些方法都是以UTF8格式來讀取文件的,比如
use std::io::{Result, Read}; use std::fs::File; fn main() -> Result<()>{ let filepath = "caddy.log"; let mut file = File::open(filepath)?; let mut content = String::new(); file.read_to_string(&mut content)?; println!("content: {content}"); Ok(()) }
雖然UTF8是主流,但是,但是,但是。。。還有一些例外,比如GBK。
如果我們使用上面的代碼讀取GBK格式的文件,那么會有以下報錯。
Error: Error { kind: InvalidData, message: "stream did not contain valid UTF-8" }
所以,我們需要指定編碼,這需要使用第三方庫encoding_rs, 可以通過cargo add encoding_rs添加依賴,本文使用的是0.8.33
值得注意的是: 非GBK的數(shù)據(jù)不一定會失敗, 比如全是ASCII字符的文本。
use std::io::{Result, Error, ErrorKind}; use std::fs; use encoding_rs::GBK; fn main() -> Result<()>{ let filepath = "gbk.log"; let content = fs::read(&filepath)?; println!("{}", content.len()); let (content, _, had_err) = GBK.decode(&content); if had_err { return Err(Error::new(ErrorKind::Other, "使用GBK解碼失敗")) } println!("{}", content.len()); println!("content: {content:?}"); Ok(()) }
字符串處理
字符串的操作,大家可以直接查閱官方文檔,這里就不一一列舉它有的工作方法了,參考文檔: https://doc.rust-lang.org/std/string/struct.String.html
正則表達(dá)式
正則表達(dá)式很多時候還是很好用的,特別是匹配文本和獲取特定的模式字段,這里還是匹配Nginx的訪問日志記錄,數(shù)據(jù)樣本如下。
172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] "GET /hello HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"
這需要依賴第三方庫regex, 可通過cargo add regex命令添加。
假設(shè)我們想獲取/hello這個字符串。
use regex::Regex; fn main() { let log = r#"172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] "GET /hello HTTP/1.1" 200 612 "-" "curl/7.29.0" "-""#; let pattern = Regex::new(r#".+?"GET\s+(.+)\s+HTTP.+?"#).unwrap(); // 判斷是否匹配 if pattern.is_match(log) { println!("該日志匹配正則表達(dá)式") } else { panic!("無法匹配正則表達(dá)式") } // 獲取匹配的部分 if let Some(caps) = pattern.captures(log) { println!("{caps:?}"); let uri = caps.get(1).unwrap().as_str(); println!("uri: {uri}"); } else { panic!("無法捕獲表達(dá)式里的內(nèi)容") } }
輸出結(jié)果如下:
該日志匹配正則表達(dá)式
Captures({0: 0..61/"172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] \"GET /hello HTTP/", 1: 49..55/"/hello"})
uri: /hello
如果你看不懂我寫的那串正則表達(dá)式,我覺得也沒關(guān)系,因?yàn)檫@東西需要額外的學(xué)習(xí)。因?yàn)檎齽t表達(dá)式的性能不好預(yù)測(針對長文本的時候),所以盡可能的還是用比較好理解的各種字符串方法來獲取所需要的字段吧,如果可以的話。
用展開宏處理嵌套結(jié)構(gòu)
前面在獲取Caddy的uri字段的時候,因?yàn)椴辉谧钔鈱樱孕枰扰袛鄏equest字段在不在,然后再判斷request的值里面有沒有uri字段,這還只是在第二層,如果是更加深的層次,那么需要寫很多的無聊代碼,這實(shí)在是無趣的事情,所以我們可以將這種有著相同模式的代碼用rust聲明宏來完成。
use serde_json::json; macro_rules! serde_get { ($value: ident, $first: expr) => { { match ($value).get($first) { Some(val) => Some(val), None => { None } } } }; ($value: ident, $first: expr, $($others:expr)+) => { { match ($value).get($first) { Some(val) => { serde_get!(val, $($others)+) }, None => { None } } } }; // 使用聲明宏處理遞歸調(diào)用的關(guān)鍵在于$($others:tt)* ($value: ident, $first: expr, $($others:tt)* ) => { { match ($value).get($first) { Some(val) => { serde_get!(val, $($others)+) } None => None } } }; } fn main() { let object = json!({ "key11": {"key12": "key13"}, "key21": {"key22": {"key23": "key24"}} }); if let None = serde_get!(object, "xx") { println!("不存在鍵xx"); } if let Some(val) = serde_get!(object, "key11", "key12") { println!(r#"object["key11"]["key12"] = {val:}"#); } if let Some(val) = serde_get!(object, "key21", "key22", "key23") { println!(r#"object["key21"]["key21"]["key23"] = {val:}"#); } if let Some(val) = serde_get!(object, "key21", "key22", "key23", "key24") { println!(r#"object["key21"]["key21"]["key23"]["key33"] = {val:}"#); } else { println!(r#"object["key21"]["key21"]["key23"]["key33"]不存在"#); } }
代碼的輸出結(jié)果如下:
不存在鍵xx
object["key11"]["key12"] = "key13"
object["key21"]["key21"]["key23"] = "key24"
object["key21"]["key21"]["key23"]["key33"]不存在
除了使用聲明宏也可以使用遞歸函數(shù),這就看大家的喜好了。如果大家看得不是太懂,可以搜索關(guān)鍵字rust TT muncher或者rust 標(biāo)記樹撕咬機(jī) 。
這個例子寫完,我才發(fā)現(xiàn)serde_json可以直接使用["key21"]["key21"]["key23"]這樣的語法直接判斷!!!, 不過serde_json的返回結(jié)果都是null, 如果鍵值對不存在的話。
總結(jié)
說實(shí)話,就處理文本數(shù)據(jù)這塊,我感覺rust的體驗(yàn)遠(yuǎn)遠(yuǎn)比不上動態(tài)類型的編程語言,比如Python, 但是為了開發(fā)的一致性,我還是會很多情況使用Rust,在本文稍微提及了一下rust的宏編程,下一篇文章是關(guān)于聲明函的教程, 有興趣的可以關(guān)注一下。
參考鏈接:
https://github.com/serde-rs/json
https://docs.rs/encoding_rs/latest/encoding_rs/
https://docs.rs/regex/latest/regex/
https://earthly.dev/blog/rust-macros/
https://youerning.top/post/rust/rust-text-processing-tutorial/
到此這篇關(guān)于Rust文本處理快速入門 的文章就介紹到這了,更多相關(guān)Rust文本處理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Rust Atomics and Locks并發(fā)基礎(chǔ)理解
這篇文章主要為大家介紹了Rust Atomics and Locks并發(fā)基礎(chǔ)理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02vscode搭建rust開發(fā)環(huán)境的圖文教程
本文主要介紹了vscode搭建rust開發(fā)環(huán)境的圖文教程,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08Rust調(diào)用Windows API 如何獲取正在運(yùn)行的全部進(jìn)程信息
本文介紹了如何使用Rust調(diào)用WindowsAPI獲取正在運(yùn)行的全部進(jìn)程信息,通過引入winapi依賴并添加相應(yīng)的features,可以實(shí)現(xiàn)對不同API集的調(diào)用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-11-11