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文本处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 从迷你todo 命令行入门Rust示例详解

    从迷你todo 命令行入门Rust示例详解

    这篇文章主要为大家介绍了从一个迷你todo命令行入门Rust的示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • Rust生命周期常见误区(中英对照)全面指南

    Rust生命周期常见误区(中英对照)全面指南

    这篇文章主要WEIDJAI 介绍了Rust生命周期常见误区(中英对照)的全面指南,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • 详解Rust中的所有权机制

    详解Rust中的所有权机制

    Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码,这篇文章主要介绍了Rust中的所有权机制,需要的朋友可以参考下
    2022-10-10
  • 如何使用Rust直接编译单个的Solidity合约

    如何使用Rust直接编译单个的Solidity合约

    本文介绍了如何使用Rust语言直接编译Solidity智能合约,特别适用于没有外部依赖或flatten后的合约,一般情况下,Solidity开发者使用Hardhat或Foundry框架,本文给大家介绍如何使用Rust直接编译单个的Solidity合约,感兴趣的朋友一起看看吧
    2024-09-09
  • 利用rust实现一个命令行工具

    利用rust实现一个命令行工具

    这篇文章主要为大家详细介绍了如何使用 Rust 和 clap 4.4.0 创建一个命令行工具 my_dev_tool,文中的示例代码讲解详细,需要的小伙伴可以参考下
    2023-12-12
  • Rust循环控制结构用法详解

    Rust循环控制结构用法详解

    Rust提供了多种形式的循环结构,每种都适用于不同的场景,在Rust中,循环有三种主要的形式:loop、while和for,本文将介绍Rust中的这三种循环,并通过实例展示它们的用法和灵活性,感兴趣的朋友一起看看吧
    2024-02-02
  • Rust自定义安装路径的详细图文教程

    Rust自定义安装路径的详细图文教程

    工欲善其事必先利其器,无论是对小白还是大神来说,想要学习 Rust 第一步那必须是 Rust 的环境配置,下面这篇文章主要给大家介绍了关于Rust自定义安装路径的详细图文教程,需要的朋友可以参考下
    2023-03-03
  • RUST异步流处理方法详细讲解

    RUST异步流处理方法详细讲解

    这篇文章主要介绍了RUST异步流处理方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-12-12
  • Rust处理错误的实现方法

    Rust处理错误的实现方法

    程序在运行的过程中,总是会不可避免地产生错误,而如何优雅地解决错误,也是语言的设计哲学之一。本文就来和大家来了Rust是如何处理错误的,感兴趣的可以了解一下
    2023-03-03
  • rust如何解析json数据举例详解

    rust如何解析json数据举例详解

    这篇文章主要给大家介绍了关于rust如何解析json数据的相关资料,SON 格式非常轻量级,因此它非常适合在网络中传输大量数据,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-11-11

最新评论