rust学习 - 构建mini 命令行工具

news2024/11/6 17:31:10

rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为构建命令行程序的最佳选择。

实现一个命令行搜索工具grep,可以在指定文件中搜索指定的字符串。想实现这个功能呢,可以按照以下逻辑流程处理:

  1. 获取输入文件路径、需要搜索的字符串
  2. 读取文件;
  3. 在文件内容中查找字符串所在的行
  4. 打印包含字符串所在的行信息

创建项目ifun-grep

$> cargo new ifun-grep

项目在运行时,可以获取到传递的参数。比如cargo run -- hboot hello.txt,在文件hello.txt查找字符串hboot

读取参数

首先要先获取到传入的参数。通过标准库std::env::args获取

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    dbg!(args);
}

collect()方法可以将传入的参数转换为一个集合。对于变量args必须注明集合类型。

args-print.jpg

参数的第一个值是二进制文件的名称。可以用于程序调试或者打印出文件路径,取出另外两个参数,保存进对应的变量。方便后续传参数使用。

let search = &args[1];
let file_path = &args[2];

println!("will search {} in {}", search, file_path)

读取文件

首先创建测试文件hello.txt,并写入一段文字。

独立寒秋,湘江北去,橘子洲头。

看万山红遍,层林尽染;漫江碧透,百舸争流。

鹰击长空,鱼翔浅底,万类霜天竞自由。

怅寥廓,问苍茫大地,谁主沉浮?

携来百侣曾游,忆往昔峥嵘岁月稠。

恰同学少年,风华正茂;书生意气,挥斥方遒。

指点江山,激扬文字,粪土当年万户侯。

曾记否,到中流击水,浪遏飞舟

读取文件,并打印出文件中的内容。

let content = fs::read_to_string(file_path).expect("you should permission to read the file");

println!("read the content:\n{content}")

通过fs模块的read_to_string方法读取文件内容。expect则用于处理读取文件时发生的错误的提示信息,这在下面的错误处理会有说明。

模块拆分与错误处理

现在所有的处理业务都放在src/main.rs中。取参和读取文件是两个不同功能的逻辑处理,当功能越来越复杂的时候,就应该关注分离。这在我们设计时可提前考虑好

main.rs只被用来处理程序的执行。其他需要处理的逻辑则可以放在srr/lib.rs中。

定义一个解析取参的函数parse_args,现在仍然定义在src/main.rs中。

fn parse_args(args: &Vec<String>) -> (&str, &str) {
    let search = &args[1];
    let file_path = &args[2];

    (search, file_path)
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let (search, file_path) = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

这样main函数不再处理哪个参数对应哪个变量。

我们可以将这一组相关的变量通过结构体定义相互关联起来。这样函数返回将不再使用元组,并且可以通过结构体实例可以访问到每一个属性。

struct Config {
    search: String,
    file_path: String,
}

fn parse_args(args: &Vec<String>) -> Config {
    let search = args[1].clone();
    let file_path = args[2].clone();

    Config { search, file_path }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = parse_args(&args);

    println!("will search {} in {}", search, file_path);
}

在结构体中,实例化赋值需要拥有这些变量值的所有权。而变量args是所有权的拥有者,通过clone()方法拷贝一份数据。

可以看到parse_args返回来一个结构体 Config 的实例,可以通过定义结构体的内部方法来创建实例。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        let search = args[1].clone();
        let file_path = args[2].clone();

        Config { search, file_path }
    }
}

fn main(){
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("will search {} in {}", search, file_path);
}

这样,就不需要parse_args函数了,通过结构体的内部方法实例化实例。

错误处理

如果我们执行cargo run时,不传递任何参数,则程序会报错。这样的提示对于用户并不友好。

首先可以通过判断参数需要的参数信息,说明错误信息。

impl Config {
    fn new(args: &Vec<String>) -> Self {
        if args.len() < 3 {
            panic!("至少传入2个参数")
        }
        // ...
    }
}

提示用户必须传入 2 个从参数,因为有一个默认的路径参数。所以判断不能少于3

除了直接提示错误信息并中断程序,也可以使用Result传递错误,让主函数做决定如何去处理。

impl Config {
    fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

现在提供了一个方法build来处理这个逻辑,之前的new不用了(这里是语义话定义,new常常表示不会产生错误),当有错误时,不是直接终止程序,而是返回一个Err值。

src/main.rs中调用并处理结果。对于错误信息给用户输出有好的提示信息,并以非零错误process::exit(1)退出命令行。

use std::{env, fs, process};

fn main(){
    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("error occurred parseing args:{err}");
        process::exit(1);
    });

    // ...
}

unwrap_or_else可以进行自定义错误处理。这是一个闭包,它调用内部的匿名函数,并通过|err|传递的参数供内部使用。当返回Ok时,则返回内部的值。

提取读取文件的逻辑

参数的取参逻辑经由结构体内部方法处理。现在吧文件读取的逻辑提取出来,并采用传递错误的方式Result返回错误信息。

use std::error::Error;

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    println!("read the content:\n{content}");

    Ok(())
}

使用了 trait 对象 Box<dyn Error>返回实现Errortrait 的类型,不用指定具体的错误类型。灵活性更高dyn表示动态的

接着可以在主函数中调用run()函数,并处理可能出现的错误。

fn main (){
    // ...

    if let Err(e) = run(config) {
        println!("something error:{e}");
        process::exit(1);
    }
}

拆分代码到库

以上定义了结构体,处理取参函数;拆离了读取文件逻辑。但是这些都是在src/main.rs中,有复杂逻辑时,这会让文件行数很多,看起来很让人头疼。

将这一部分拆离的放到其他文件中去。新建src/lib.rs,将这些定义移动到该文件中。

use std::error::Error;
use std::fs;

pub struct Config {
    pub search: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { search, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    println!("read the content:\n{content}");

    Ok(())
}

可以看到通过pub将这些结构体、函数都公有化。包括结构里的字段,这就是一个可以测试的公有 API 的 crate 库。

然后再src/main.rs需要导入

use ifun_grep::{run, Config};

通过use引入作用域。ifun-grep是项目名称,作为前缀。

增加测试

通过测试驱动开发的模式来逐渐增加逻辑。期望从给定的内容中查找出字符串,并打印出所在行。

src/lib.rs增加测试示例

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn on_result() {
        let search = "hboot";
        let content = "\
nice. rust
I'm hboot.
hello world.
";

        assert_eq!(vec!["I'm hboot."], find(search, content));
    }
}

搜索字符串hboot,它在文本的第二行。所以期待搜索输出结果为I'm hboot.

提供一个find函数,用于处理搜索逻辑,先不写搜索逻辑,返回一个空的结果值。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    vec![]
}

利用显示生命周期'a来表明参数content参数与返回值的生命周期相关联。它们存在的时间一样久

执行测试cargo test,理所应当的输出失败,结果返回了一个空的vec![],和预期不匹配。

增加搜索逻辑,按行执行过滤,包含指定的字符串,则存储在结果中。

pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    for line in content.lines() {
        if line.contains(search) {
            // 符合,包含了指定字符串
            result.push(line);
        }
    }

    result
}

通过迭代器遍历给定文本内容lines().字符串判断是否包含contains()方法。将结果值放进result中,并返回。

测试用例测试没有问题,完善一下run函数,搜索出符合的内容并打印出来

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    for line in find(&config.search, &content) {
        println!("{line}");
    }

    Ok(())
}

执行脚本cargo run -- 山 hello.txt,可以看到打印输出两行

run-success.png

增加环境变量

功能已经到到预期,可以搜索出想要包含字符串的文本段落。增加一个额外的功能大小写敏感处理环境变量,当然也可以通过再多传一个参数处理。

更改文本内容为应为

Let life be beautiful like summer flowers.

The world has kissed my soul with its pain.

Eyes are raining for her.

you also miss the stars.

先测试当前程序是否大小写敏感,文本中首个英文单词是大写的,按照小写搜索

$> cargo run -- let hello.txt

没有任何的打印输出,说明当前的搜索逻辑是大小写敏感的,通过传递变量来控制逻辑,修改测试用例,增加两个测试示例:大小写敏感和不敏感测试。

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn case_sensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust"], find(search, content));
    }

    #[test]
    fn case_insensitive() {
        let search = "rust";
        let content = "\
nice. rust
I'm hboot.
hello world.
Rust
";

        assert_eq!(vec!["nice. rust", "Rust"], find_insensitive(search, content));
    }
}

原来的函数find大小写敏感,逻辑不变。增加一个大小写不敏感的函数find_insensitive,在处理搜索时,查询的字符和被搜索的文本行都转小写后,然后在执行查找。

pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
    let mut result = vec![];
    // 搜索 字符串转小写
    let search = search.to_lowercase();

    for line in content.lines() {
        // 文本行内容转小写
        if line.to_lowercase().contains(&search) {
            // 符合,包含了指定字符串
            result.push(line);
        }
    }

    result
}

多了一个操作to_lowercase()将文本内容转成小写。to_lowercase()会新创建一个 String,contains()方法参数需要的是一个引用。

再次执行测试cargo teset.用例全部通过。逻辑写好了,需要通过增加一个配置来处理是否大小写敏感。

修改结构体定义ingore_case表示来忽略大小写。

pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

通过ingore_case字段判断是否调用哪个函数,修改run函数

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    let mut result = vec![];
    if config.ignore_case {
        result = find_insensitive(&config.search, &content)
    } else {
        result = find(&config.search, &content)
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

处理接受变量IGNORE_CASE,通过库std::env处理环境变量。

impl Config {
    pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("至少传入2个参数");
        }
        let search = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            search,
            file_path,
            ignore_case,
        })
    }
}

env::var()返回值为 Result 类型,通过它自己的方法is_ok()判断什么状态,如果设置值则返回 true;未设置则返回 false。

进行测试,不设置变量时,查询小写的let是查询不到的,因为首写的因为单词字母是大些的。

$> cargo run -- let hello.txt

test-case-sensitive.png

通过设置环境变量,执行程序

$> IGNORE_CASE=1 cargo run -- let hello.txt

可以查到目标文本内容。

teset-case-insensitive.png

错误信息处理

我们所预先知道的错误信息都通过程序执行println!打印在控制台,这是一种标准输出.

对于出现错误信息,希望它即时打印输出,而对于程序执行的结果记录下来,保存到文件中,方便查看。

现在使用println!标准输出流重定向到文件中,它会将错误信息也保存到起来,且不会打印。

$> cargo run >output.txt

屏幕上没有任何输出,以为程序执行正常,其实文件中的内容是error occurred parseing args:至少传入2个参数

这就造成了一个问题,不管成功、失败,只有打开文件才能看到。错误输出使用标准错误展示用于错误信息,将错误打印的println!改为eprintln!

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        // println!("error occurred parseing args:{err}");
        eprintln!("error occurred parseing args:{err}");
        process::exit(1);
    });
    println!("will search {} in {}", config.search, config.file_path);

    if let Err(e) = run(config) {
        // println!("something error:{e}");
        eprintln!("something error:{e}");
        process::exit(1);
    }
}

重新执行cargo run >output.txt,错误打印到控制台,而文件output.txt没有输出。

再执行,可以查到数据的命令cargo run -- Let hello.txt > output.txt,查看output.txt,可以看到预期的查找到的内容在文件中。

发布 crate 到Crate.io

crates.io 库,可以这里找找想要的功能库,也可以将自己的 crate 发布到这里。

Rust 的发布配置都有一套默认的、可定制的配置。

  • cargo build 采用的是 dev 配置构建程序
  • cargo build --release 是 release 配置,有更好的发布构建的配置

可以在文件Cargo.toml中通过[profile.*]修改设置默认值。

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

dev 构建和发布构建定义不同的优化等级。opt-level定义何种程度优化,0-3可配置值。dev 默认为 0,release 默认为 3.

如果想 dev 模式下需要一些优化,则可以更改为

[profile.dev]
opt-level = 1

增加文档注释

一个好的模块包,是有很好的文档说明,以方便其他人轻易上手。通过文档注释///已支持 markdown 格式化文本。

给每一个函数增加注释说明,这里只展示部分。

/// the struct `Config` defines command line params.
///
/// # Example
///
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// ```
pub struct Config {
    pub search: String,
    pub file_path: String,
    pub ignore_case: bool,
}

/// the fun is used to execute search
///
/// # example
/// ```
/// let search = String::from("let");
/// let config = ifun_grep::Config {
///     search,
///     file_path:String::from("hello.txt"),
///     ignore_case:false,
/// };
///
/// let result = ifun_grep::run(config);
///
/// assert!(result.is_ok());
/// ```
///
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let content = fs::read_to_string(config.file_path)?;

    // println!("read the content:\n{content}");
    let result;
    if config.ignore_case {
        result = find_insensitive(&config.search, &content);
    } else {
        result = find(&config.search, &content);
    }
    for line in result {
        println!("{line}");
    }

    Ok(())
}

在使用 vscode 时,注释文档上方会有一个执行操作 run doctest。可以单独执行当前写的测试示例是否可以通过执行。

也可以通过cargo test来测试所有的测试示例。不仅会执行mod test的测试示例,也会执行doc test的注释测试示例。

通过命令cargo doc --open来生成在线文档。

$> cargo doc --open

crate-doc.png

可以通过//!对当前文件进行注释说明,必须是在第一行。

//! ifun_grep is a string search library
//!
//! Supports case sensitive search.
//!

注册 crate.io 账户并发布

目前只能使用 github 账号进行授权登录。在个人账号信息中,API Tokens生成 token 授权操作。

$> cargo login 你的token

如果登录不成功,看下提示错误,我是加了参数--registry crates-io才成功的。

$> cargo login 你的token --registry crates-io

登录之后就可以发布了,通过Cargo.toml增加一些仓库元信息,比如仓库名、作者、开源协议、描述等等。

$> cargo publish

发布之前需要验证你登录的账号邮箱,不然发布不了。个人的元信息有几项是必填的,包括name\version\description\license

发布时,如果发布不成功,看错误提示,可能还需要加--registry crates-io

撤销某个版本

如果你发布的版本有很大的问题,可以撤销改版本。不能删除仓库,已发布的代码时永久存在的,只能通过撤销来阻止其他项目引用它。

$> cargo yank --vers 0.1.0

使得当前版本不可用。也可以恢复当前版本的使用

$> cargo yank --vers 0.1.0 --undo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/625099.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Alexnet网络实现猫狗数据集分类(Keras框架)

目录 1、作者介绍2、Alexnet网络2.1 网络介绍2.2 AlexNet网络的主要特点 3、基于Alexnet网络实现猫狗数据集分类3.1 猫狗大战数据集3.2 数据集处理3.3 准备工作3.4 训练过程3.4 对比实验3.4.1 HALCON平台下的Alexnet实验3.4.2 HALCON平台下的Resnet-50对比实验3.4.3 HALCON平台…

进程的通信——管道和共享内存

进程间的通信有很多种 管道 匿名管道pipe 命名管道 System V IPC System V 消息队列 System V 共享内存 System V 信号量 POSIX IPC 消息队列 共享内存 信号量 互斥量 条件变量 读写锁 这篇文章主要介绍管道和共享内存 管道 管道内核数据结构&#xff1a;在Linux2.6中 struct …

搭建自动化测试环境

目录 1、安装Python并配置环境变量。2、安装Pycharm开发工具。3、安装Selenium4、安装浏览器&#xff1a;Chrome和Firefox的其中之一。5、浏览器驱动&#xff1a;下载Chrome浏览器驱动或者是Firefox浏览器驱动。6、配置webdriver公众号粉丝福利 自动化测试环境&#xff1a; Pyt…

接口反应慢优化

遇到某个功能&#xff0c;页面转圈好久&#xff0c;需要优化 1.F12 查看接口时间 2.看参数 总共耗时9.6s Waiting for sercer response 时间是2秒 Content Download 7秒 慢在Content Download F12查看接口响应 显示Failed to load response data:Request content was e…

这些10款优秀的交互设计软件,你知道吗?

交互软件可以帮助设计师从“可用性”和“用户体验”的角度优化他们的作品。如果设计师想创建一个令人满意的交互设计作品&#xff0c;一个方便的交互设计软件是必不可少的。 根据设计师的个人喜好和方便&#xff0c;选择易于使用的交互设计软件来完成创建。本文盘点十款易于使…

【P55】JMeter 图形结果(Graph Results)

文章目录 一、图形结果&#xff08;Graph Results&#xff09;参数说明二、准备工作三、测试计划设计 一、图形结果&#xff08;Graph Results&#xff09;参数说明 可以以图形的方式查看和分析相关指标 使用场景&#xff1a;一般在调试测试计划期间用来查看相关指标&#xf…

微信小程序基础使用-请求数据并渲染

小程序基本使用-请求数据并渲染 小程序模板语法-数据绑定 在js中定义数据 Page({data: {isOpen: true,message: hello world!} })小程序的data是一个对象&#xff0c;不同于vue的data是一个函数 在模块中获取使用数据 小程序中使用 {{}} 实现数据与模板的绑定 内容绑定&a…

Qt Quick-QML地图引擎之v4版本(新增3D模型/抗锯齿任意多边形下载)

在上个版本Qt quick-QML地图引擎之v4版本(新增3D模型/高德/谷歌在线/离线预览/多线程离线裁剪下载/区域查询/位置搜索/路径规划)_qt 高德地图离线_诺谦的博客-CSDN博客更新了很多小功能。经过朋友们一致需求建议&#xff0c;所以V4继续优化。 B站视频&#xff1a; Qt Quick-QML…

【办公效率提升】Window10与ubuntu递归列出当前目录及其所有子目录中的文件和文件夹

在Windows操作系统中&#xff0c;没有内置的类似于Ubuntu的"tree"命令的功能。但是&#xff0c;你可以使用以下两种方法来实现相似的功能&#xff1a; 方法一&#xff1a;使用dir命令和递归 你可以使用Windows的内置命令"dir"以及递归参数"/s"来…

前后端交互二、form表单与模板引擎

零、文章目录 前后端交互二、form表单与模板引擎 1、form表单的基本使用 HTML相关知识请参考HTML入门 &#xff08;1&#xff09;表单是什么 表单在网页中主要负责数据采集功能。HTML中的<form>标签&#xff0c;就是用于采集用户输入的信息的&#xff0c;并通过<…

linux PerfCollect收集日志及perfview分析

Perfview&#xff1a;https://github.com/Microsoft/perfview/releases PerfCollect&#xff1a;https://github.com/dotnet/coreclr/blob/master/Documentation/project-docs/linux-performance-tracing.md Linux 环境中运行的 ASP.NET Core应用中收集跟踪 PerfCollect&#…

5.6.1 Ext JS之标签页的关闭和批零关闭

Tab Panel 是包含多个标签页的面板, 这是一种很常用的组件, 类似于浏览器的标签页。关于 Ext JS的Tab Panel的基本使用可以参考: [Ext JS3.9] 标签面板(TabPanel )介绍与开发, 本篇介绍如何关闭单个标签页和批量关闭标签页。 Tab 标签页的可关闭 默认状况下,标签页是无…

Bitmiracle Docotic.Pdf 9.015 Crack

Docotic.Pdf 库是正确的法语和强大的编程和界面&#xff0c;可以让用户和开发人员创建专业和高质量的 PDF 文件&#xff0c;甚至可以阅读和修改那些已经存在的。它具有干净而强大的编程接口&#xff0c;能够帮助用户创建质量非常好的 PDF 文档。在这个库的帮助下&#xff0c;用…

linux log

linux log 一:printk日志级别二:printk打印消息控制printkprintk消息等级查看与修改/etc/rsyslog.conf 一:printk日志级别 数字越小级别越高 二:printk打印消息控制 console_loglevel&#xff1a;只有当printk打印消息的log优先级高于console_loglevel时&#xff0c;才能输出…

C++编译一些常见的错误集锦

目录 1、段错误&#xff08;Segmentation Fault&#xff09; 2、强异常保证&#xff08;strong exception guarantee&#xff09; 3、有效但未定义的状态&#xff08;valid but unspecified state&#xff09; 1、段错误&#xff08;Segmentation Fault&#xff09; &#…

Material—— VAT(Houdini To UE)

目录 一&#xff0c;介绍 二&#xff0c;柔体 二&#xff0c;刚体 一&#xff0c;介绍 VAT是将动画数据存储在纹理中&#xff0c;通过GPU运算来实现动画的技术&#xff1b;VAT纹理包含每个顶点在不同帧的位置信息&#xff0c;而每个像素代表一个顶点在某个时间点的位置&…

Linux项目流程 + 用git将本地代码上传到gitee

目录 前言 一个"进度条"项目 git上传代码 首次上传代码 安装git 克隆仓库连接 将文件传入路径目录 文件上传三板斧 后续提交更新 git实用用法补充 前言 本文将介绍如何使用makefile编写项目并将其上传到git远程仓库。Makefile是一种用于自动化构建、编译…

Python爬虫:从后端分析为什么你爬虫爬取不到数据

仅仅是小编总结的三点而已&#xff0c;可能不是很全面&#xff0c;如果之后小编了解到新的知识点&#xff0c;可能还会增加的哈&#xff01; 1. 最简单的爬虫代码 也就是各位最常使用的&#xff0c;直接利用requests模块访问当前网站链接&#xff0c;利用相关解析模块从而获取…

第三届陕西省大学生网络安全技能大赛wp

文章目录 第三届陕西省大学生网络安全技能大赛wpwebezpoptestezrceunserializeEsc4pe_T0_Mong0 misc管道可是雪啊飘进双眼 第三届陕西省大学生网络安全技能大赛wp web ezpop 在源码找到base64 解码&#xff1a; /pop3ZTgMw.php&#xff0c;访问获得源码&#xff1a; <?…

通过帮助中心提高客户满意度,帮助中心的最佳实践方式

随着技术的不断发展和产品的不断更新&#xff0c;消费者对产品的需求也越来越高。在这个竞争激烈的市场中&#xff0c;企业必须建立一个完善的帮助中心&#xff0c;来提供及时、准确的技术支持和解决方案&#xff0c;以满足客户的各种需求。这样能够有效地提高客户满意度&#…