广告位联系
返回顶部
分享到

Rust文本处理快速入门

Rust语言 来源:互联网 作者:佚名 发布时间:2024-03-31 22:03:46 人浏览
摘要

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

编程过程中有许多类型的数据要处理,其中文本处理必不可少,本文主要是记录在使用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" "-"

上面的日志对应的日志格式如下:

1

2

3

'$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.

1

2

3

4

[dependencies]

encoding_rs = "0.8.33"

regex = "1.10.2"

serde_json = "1.0.108"

快速入门

假设我们的任务是统计日志中每个URL的访问次数。

Caddy日志解析

Caddy的日志格式是每行都是一个合法的JSON格式的文本,所以直接使用serde_json处理即可。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

// 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"。

解析文本有很多办法,大致分为两种,使用正则表达式或者不使用正则表达式,这里选择的方法是不使用正则表达式,因为正则表达式的维护难度有点大。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

// 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(())

}

两个的代码结果应该都是如下:

1

url_counter: {"/": 1, "/hello": 2}

文件读取

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

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

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

1

2

3

4

5

6

7

8

9

use std::io::Result;

use std::fs::File;

 

 

fn main() -> Result<()>{

    let filepath = "caddy.log";

    let file = File::open(filepath)?;

    Ok(())

}

如果我们关心错误,那么可以用模式匹配判断一下, **io::Error有很多类型的, 这里仅判断了不存在的类型 **

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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(())

}

如果只是判断文件不存在还有一些简单的方法,比如:

1

2

3

4

5

6

7

8

9

use std::path::Path;

 

 

fn main() {

    let path = Path::new("caddy.logx");

    if !path.exists() {

        println!("文件不存在");

    }

}

编码

当获取了文件句柄就可以读取文件内容了,但是我们总要时刻注意文件的编码是什么,默认情况下Rust提供的一些方法都是以UTF8格式来读取文件的,比如

1

2

3

4

5

6

7

8

9

10

11

12

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字符的文本。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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这个字符串。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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声明宏来完成。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

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/


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 一文弄懂rust声明宏
    Rust支持两种宏,一种是声明宏,一种是过程宏,前者相较于后者还是比较简单的。本文主要是讲解Rust元编程里的声明宏,通过声明宏可以减
  • Rust文本处理快速入门
    编程过程中有许多类型的数据要处理,其中文本处理必不可少,本文主要是记录在使用Rust开发的过程中处理文本相关数据的一些代码,而文
  • Rust Aya 框架编写 eBPF 程序
    Linux 内核 6.1 版本中有一个非常引人注意的变化:引入了对 Rust 编程语言的支持。Rust 是一种系统编程语言,Rust 通过提供非常强大的编译时
  • Rust中Cargo的使用介绍
    1、cargo简介 Cargo 是 Rust 的构建系统和包管理器。?多数 Rustacean 们使? Cargo 来管理他们的 Rust 项?,因为它可以为你处理很多任务,?如构建代码
  • 深入了解Rust结构体的使用

    深入了解Rust结构体的使用
    结构体是一种自定义的数据类型,它允许我们将多个不同的类型组合成一个整体。下面我们就来学习如何定义和使用结构体,并对比元组与
  • 深入了解Rust中引用与借用的用法

    深入了解Rust中引用与借用的用法
    好久没更新 Rust 了,上一篇文章中我们介绍了 Rust 的所有权,并且最后定义了一个 get_length 函数,但调用时会导致 String 移动到函数体内部,
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计