Rust文本处理快速入门

 更新时间:2024年03月31日 11:35:41   作者:又耳笔记  
编程过程中有许多类型的数据要处理,其中文本处理必不可少,本文主要介绍了Rust文本处理快速入门 ,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧

编程过程中有许多类型的数据要处理,其中文本处理必不可少,本文主要是记录在使用Rust开发的过程中处理文本相关数据的一些代码,而文本可以分为结构化和非结构化的文本,比如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"

快速入门

假设我们的任务是统计日志中每个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之类的库, 不够我没有用过
                // 所以这里就这样吧, 后文用展开宏节省一下代码。
                // 其实这里也可以用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}

文件读取

一般来说文本都是以文件的形式存在的,这里讨论的也主要是以文件形式存在的文本,至于网络数据的文本需要根据对应的协议来处理了。

获取文件句柄(打开文件)

在读取文本之前自然是需要先打开文件或者说获得文件句柄的。
如果只关心打不打得开,那么可以直接通过问号?操作符将错误直接往外抛。

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 标记树撕咬机 。
这个例子写完,我才发现serde_json可以直接使用["key21"]["key21"]["key23"]这样的语法直接判断!!!, 不过serde_json的返回结果都是null, 如果键值对不存在的话。

总结

说实话,就处理文本数据这块,我感觉rust的体验远远比不上动态类型的编程语言,比如Python, 但是为了开发的一致性,我还是会很多情况使用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文本处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Rust语言之trait中的个方法可以重写吗

    Rust语言之trait中的个方法可以重写吗

    在Rust中,trait定义了一组方法,这些方法可以被一个或多个类型实现,当你为某个类型实现一个trait时,你可以为该trait中的每个方法提供自己的具体实现,本文将给大家介绍一下trait中的个方法是否可以重写,需要的朋友可以参考下
    2023-10-10
  • Rust中的derive属性示例详解

    Rust中的derive属性示例详解

    derive属性的出现解决了手动实现一些特性时需要编写大量重复代码的问题,它可以让编译器自动生成这些特性的基本实现,从而减少了程序员需要编写的代码量,这篇文章主要介绍了Rust中的derive属性详解,需要的朋友可以参考下
    2023-04-04
  • Rust 文档注释功能示例代码

    Rust 文档注释功能示例代码

    Rust的文档注释使用特定的格式,以便通过 rustdoc工具生成 API 文档,本文给大家介绍Rust 文档注释功能,感兴趣的朋友跟随小编一起看看吧
    2024-04-04
  • Rust包和Crate超详细讲解

    Rust包和Crate超详细讲解

    这篇文章主要介绍了Rust包管理和Crate,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • 详解Rust语言中anyhow的使用

    详解Rust语言中anyhow的使用

    anyhow是一个Rust库,用于简化错误处理和提供更好的错误报告,这个库适合用于应用程序,而不是用于创建库,因为它提供了一个非结构化的,方便使用的错误类型,本文就给大家讲讲Rust语言中anyhow的使用,需要的朋友可以参考下
    2023-08-08
  • C++的替代:微软如何使用rust?

    C++的替代:微软如何使用rust?

    这篇文章主要介绍了微软如何使用rust的,帮助大家了解c++和rust这两门编程语言的联系与区别,感兴趣的朋友可以了解下
    2020-09-09
  • Rust 智能指针实现方法

    Rust 智能指针实现方法

    这篇文章主要介绍了Rust 智能指针的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-01-01
  • 详解在Rust语言中如何声明可变的static类型变量

    详解在Rust语言中如何声明可变的static类型变量

    在Rust中,可以使用lazy_static宏来声明可变的静态变量,lazy_static是一个用于声明延迟求值静态变量的宏,本文将通过一个简单的例子,演示如何使用 lazy_static 宏来声明一个可变的静态变量,需要的朋友可以参考下
    2023-08-08
  • 一文学会Rust语言如何操作JSON

    一文学会Rust语言如何操作JSON

    JSON在Web开发中被广泛应用于数据交换,本文主要介绍了Rust语言操作JSON,包括序列化、反序列化、JSON创建等多个方面,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Rust 语言中的dyn 关键字及用途解析

    Rust 语言中的dyn 关键字及用途解析

    在Rust中,"dyn"关键字用于表示动态分发(dynamic dispatch),它通常与trait对象一起使用,以实现运行时多态, 在Rust中,多态是通过trait和impl来实现的,这篇文章主要介绍了Rust 语言中的 dyn 关键字,需要的朋友可以参考下
    2024-03-03

最新评论