Rust文本處理快速入門
編程過程中有許多類型的數據要處理,其中文本處理必不可少,本文主要是記錄在使用Rust開發(fā)的過程中處理文本相關數據的一些代碼,而文本可以分為結構化和非結構化的文本,比如JSON和小說文本(沒有固定格式的文本)。
這里以兩種格式文本為例
- Nginx的訪問日志
- Caddy的訪問日志
為了不使文章過于冗長,大家可以根據自己需要將下面的數據復制成多行,然后自行測試, 或者問ChatGPT之類的AI給你生成一些樣本數據, 比如問AI問題:"給我十條NGINX的訪問日志樣本數據"。
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" "-"
上面的日志對應的日志格式如下:
'$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依賴如下:
因為Rust的標準庫非常精簡(簡陋), 所以很多操作都需要借助第三方庫,比如這里處理JSON的庫serde.
[dependencies] encoding_rs = "0.8.33" regex = "1.10.2" serde_json = "1.0.108"
快速入門
假設我們的任務是統(tǒng)計日志中每個URL的訪問次數。
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
}
// 這樣的代碼太形式化了,應該有類似于GJSON之類的庫, 不夠我沒有用過
// 所以這里就這樣吧, 后文用展開宏節(jié)省一下代碼。
// 其實這里也可以用Options的and_then方法,但是還需要寫一個匿名函數,不是很喜歡。
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這樣的純文本格式,必須得預先知道文本的格式,這可以通過肉眼觀察或者查看輸出端的配置來了解格式,不然的話沒辦法精確的處理,至少是不能將每個字段的值剝離出來。
根據觀察或者說查看Nginx的配置文件,我們知道我們要取的數據在第一個用雙引號""包裹起來的字符串內, 比如"GET / HTTP/1.1"。
解析文本有很多辦法,大致分為兩種,使用正則表達式或者不使用正則表達式,這里選擇的方法是不使用正則表達式,因為正則表達式的維護難度有點大。
// 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(())
}兩個的代碼結果應該都是如下:
url_counter: {"/": 1, "/hello": 2}文件讀取
一般來說文本都是以文件的形式存在的,這里討論的也主要是以文件形式存在的文本,至于網絡數據的文本需要根據對應的協(xié)議來處理了。
獲取文件句柄(打開文件)
在讀取文本之前自然是需要先打開文件或者說獲得文件句柄的。
如果只關心打不打得開,那么可以直接通過問號?操作符將錯誤直接往外拋。
use std::io::Result;
use std::fs::File;
fn main() -> Result<()>{
let filepath = "caddy.log";
let file = File::open(filepath)?;
Ok(())
}如果我們關心錯誤,那么可以用模式匹配判斷一下, **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!("文件不存在");
}
}編碼
當獲取了文件句柄就可以讀取文件內容了,但是我們總要時刻注意文件的編碼是什么,默認情況下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的數據不一定會失敗, 比如全是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
正則表達式
正則表達式很多時候還是很好用的,特別是匹配文本和獲取特定的模式字段,這里還是匹配Nginx的訪問日志記錄,數據樣本如下。
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命令添加。
假設我們想獲取/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!("該日志匹配正則表達式")
} else {
panic!("無法匹配正則表達式")
}
// 獲取匹配的部分
if let Some(caps) = pattern.captures(log) {
println!("{caps:?}");
let uri = caps.get(1).unwrap().as_str();
println!("uri: {uri}");
} else {
panic!("無法捕獲表達式里的內容")
}
}輸出結果如下:
該日志匹配正則表達式
Captures({0: 0..61/"172.17.0.1 - - [20/Dec/2023:01:37:27 +0000] \"GET /hello HTTP/", 1: 49..55/"/hello"})
uri: /hello
如果你看不懂我寫的那串正則表達式,我覺得也沒關系,因為這東西需要額外的學習。因為正則表達式的性能不好預測(針對長文本的時候),所以盡可能的還是用比較好理解的各種字符串方法來獲取所需要的字段吧,如果可以的話。
用展開宏處理嵌套結構
前面在獲取Caddy的uri字段的時候,因為不在最外層,所以需要先判斷request字段在不在,然后再判斷request的值里面有沒有uri字段,這還只是在第二層,如果是更加深的層次,那么需要寫很多的無聊代碼,這實在是無趣的事情,所以我們可以將這種有著相同模式的代碼用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
}
}
}
};
// 使用聲明宏處理遞歸調用的關鍵在于$($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"]不存在"#);
}
}代碼的輸出結果如下:
不存在鍵xx
object["key11"]["key12"] = "key13"
object["key21"]["key21"]["key23"] = "key24"
object["key21"]["key21"]["key23"]["key33"]不存在
除了使用聲明宏也可以使用遞歸函數,這就看大家的喜好了。如果大家看得不是太懂,可以搜索關鍵字rust TT muncher或者rust 標記樹撕咬機 。
這個例子寫完,我才發(fā)現serde_json可以直接使用["key21"]["key21"]["key23"]這樣的語法直接判斷!!!, 不過serde_json的返回結果都是null, 如果鍵值對不存在的話。
總結
說實話,就處理文本數據這塊,我感覺rust的體驗遠遠比不上動態(tài)類型的編程語言,比如Python, 但是為了開發(fā)的一致性,我還是會很多情況使用Rust,在本文稍微提及了一下rust的宏編程,下一篇文章是關于聲明函的教程, 有興趣的可以關注一下。
參考鏈接:
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/
到此這篇關于Rust文本處理快速入門 的文章就介紹到這了,更多相關Rust文本處理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Rust Atomics and Locks并發(fā)基礎理解
這篇文章主要為大家介紹了Rust Atomics and Locks并發(fā)基礎理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
vscode搭建rust開發(fā)環(huán)境的圖文教程
本文主要介紹了vscode搭建rust開發(fā)環(huán)境的圖文教程,文中通過圖文介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-08-08
Rust調用Windows API 如何獲取正在運行的全部進程信息
本文介紹了如何使用Rust調用WindowsAPI獲取正在運行的全部進程信息,通過引入winapi依賴并添加相應的features,可以實現對不同API集的調用,本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友跟隨小編一起看看吧2024-11-11

