rust_1

rust编写csv排序工具知识点

周末两天的时间,学了以下io操作,文件的读写,代码的组织、测试和安全性的操作。

然后简单地写了一个用于将pipeline输出的qc_metrics.tsv转变成qc_metrics.csv并且按照样本顺序排序的工具

这是对这个工具的回顾。回头看一看如何发到github上。


琐碎的知识点:

rust analyzer自动格式化代码shift+alt+f。如果没能自动化,很可能因为代码有语法错误。

Path和PathBuf的区别是前者没有所有权,后者有。

.clone()和.cloned()的区别,.cloned()相当于对迭代器所有元素clone,以下两个用法等价。

1
2
let a: Vec<String> = v.iter().cloned().collect();
let b: Vec<String> = v.iter().map(|x| x.clone()).collect();

传递参数

使用clap工具,基本用法如下:

1
cargo add clap --features derive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Parser, Debug)]
#[command(name="read_and_write",version, about, long_about = None)]
pub struct Args {
#[arg(short, long, help = "read file path")]
pub read_filename: String,
#[arg(short, long, help = "output file path",default_value_t=String::from("output.csv"))]
pub output_filename: String,

#[arg(short = 'm',long, help = "input the order file",default_value_t=String::from("order.txt"))]
pub order_filename:String,
}

fn main(){
let args = Args::parse();
}

使用

1
cargo run -- --help

安全性

rust返回的内容多是Result<T,Box>或者std::io::Result<()>,这两者我暂时还没搞清楚区别;

等学了智能指针之后再回来看。

这里的PathBuf是一种常见的处理路径的格式。

File::open()和read_to_string()没什么好说的,是必须记住的内容,值得注意的是”?”,用来快速返回结果或传递错误,只能在返回Result<>或者Option<>的函数中使用,因为main函数返回的是(),所以不能够用问号“?”。

1
2
3
4
5
6
7
8
9
10
11
use std::error::Error;
use std::fs::File;
use std::io::{Read,Write};
use std::path::PathBuf;
pub fn read(filename: PathBuf) -> Result<String, Box<dyn Error>> {
let mut file = File::open(filename)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)

}

在main函数中,使用返回Result<>的函数时,需要使用match进行模式匹配,示例如下:

1
2
3
4
5
6
7
8
9
use std::process;    
fn main() {
let csv_content = match tsv2csv(tsv_file) {
Ok(content) => content,
Err(_) => {
println!("something wrong with tsv file");
process::exit(1);
},
};}

错误时需要使用process::exit(1);来退出程序并返回错误码。

读和写

读写用std::fs::File或者std::io::{Read,Write},我暂时不清楚这两者的区别。

注意这里file都必须是可变的,因为需要移动光标(好像是)?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::fs::File;
use std::io::{Read,Write};

pub fn read(filename: PathBuf) -> Result<String, Box<dyn Error>> {
let mut file = File::open(filename)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)

}

pub fn write(filename:PathBuf,content:&str) -> Result<(), Box<dyn Error>> {
let mut file = File::create(filename)?;
file.write_all(content.as_bytes())?;
Ok(())
}

文件内容操作

这里写了两个函数,tsv2csv,

列一下主要用到的函数:

.lines()返回Lines迭代器

.next()对迭代器用,返回第一个元素的Option()对象,消耗掉lines第一个元素。通常单独处理header。

push_str(),在string的末尾增加内容,输入&str。

push(),输入char。(和vector的不一样)

尽量在处理各种文本的时候,能用String就用String,能够省去很多麻烦!!!

先把csv文件转成Vec<Vec>的二维矩阵,方便处理。

.collect(),用来将迭代器转变成集合类型,需要显示声明集合类型。

.split(),用来将String分割成Split迭代器,

.join(“,”),用来将Vec[‘a’,’b’]变成”a,b”,集合变字符串。

.replace(from,to)

.map(),迭代器最常用的method!输入闭包,依次修改每个元素;另外.filter()是另外一个迭代器最常用的method。

String::with_capacity(data.capacity()),高级用法,使String有固定空间,优化内存分配。

HashMap的常见用法,.enumerate(), .insert(), .keys(), .values(),.get(k)返回Option()

ok_or()可以用来将Option()转化为result

.iter()和.into_iter(),前者是引用,后者会消耗所有权

contains()很好用

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
pub fn tsv2csv(tsv_file:PathBuf) -> Result<String, Box<dyn Error>> {
let mut result = String::new();
let content = read(tsv_file)?;
let mut lines = content.lines();
let mut header = lines.next().expect("csv is empty").to_string();
header = header.replace("\t",",");
result.push_str(&header);
result.push('\n');

let rows:Vec<Vec<String>> = lines.map(|row| row.split("\t").map(|s| s.to_string()).collect()).collect();
for mut row in rows {
row[0] = row[0].replace("_R","-");
let row_str = row.join(",");
result.push_str(&row_str);
result.push('\n');
}
Ok(result)
}

pub fn sort_by_rowname(data: String, order_file: PathBuf) -> Result<String, Box<dyn Error>> {
let order_content = read(order_file)?;
let order:Vec<&str> = order_content.lines().collect();
let mut result = String::with_capacity(data.capacity());
let mut rank = HashMap::new();

for (i, k) in order.into_iter().enumerate() {
rank.insert(k, i);
}

let mut lines = data.lines();
let header = lines.next().ok_or("csv is empty")?;

let mut rows:Vec<Vec<&str>> = lines.map(|line| line.split(",").collect::<Vec<&str>>()).collect();
let rownames:Vec<String> = rows.iter().map(|row| row[0].to_string()).collect();
for &key in rank.keys() {
if !rownames.contains(&key.to_string()) {
rows.push(vec![key])
}

}

rows.sort_by_key(|row| {
let k = row[0];
rank.get(k).cloned().unwrap_or(usize::MAX)
});
result.push_str(header);
result.push('\n');

for row in rows {
result.push_str(&row.join(","));
result.push('\n')
}


Ok(result)
}



测试与代码组织

1
catgo test -- --nocapture #可以展示println!()打印的内容

基本结构如下:

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
#[cfg(test)]
mod test {
use super::*;

#[test]
fn order_input() {
let order = PathBuf::from("order.txt");
let content = match read(order) {
Ok(content) => content,
Err(_) => "something wrong with order.txt".to_string(),
};
let content:Vec<&str> = content.lines().collect();
//println!("{:?}",content);
}

#[test]
fn tsv2csv() {
let mut result = String::new();
let tsv_file = PathBuf::from("qc_metrics.tsv");
let content = match read(tsv_file) {
Ok(content) => content,
Err(_) => "something wrong with tsv file".to_string(),
};
let mut lines = content.lines();
let mut header = lines.next().expect("csv is empty").to_string();
header = header.replace("\t",",");
result.push_str(&header);
result.push('\n');

let rows:Vec<Vec<String>> = lines.map(|row| row.split("\t").map(|s| s.to_string()).collect()).collect();
for mut row in rows {
row[0] = row[0].replace("_R","-");
let row_str = row.join(",");
result.push_str(&row_str);
result.push('\n');
}
println!("{}",result);
}

}