#csv #parser #rfc #compliant #memory #heap #fields

rcsv

一个非分配的RFC 4180兼容CSV解析器

1个不稳定版本

0.1.0 2023年1月5日

#312内存管理

自定义许可证

23KB
288

Rust中的非分配CSV解析器

本库的关键特性。

  • 遵循 RFC 4180
  • 不在堆上分配任何内存。
  • 不会抛出异常。

快速示例

fn test_record() {
    let str =
"aa,bb,cc,dd\r\n\
ee,ff,gg,hh\r\n";
    
    let mut parser = rcsv::Parser::new();

    parser.parse::<10>(str.as_bytes(), |index, fields| {
        assert!(index < 2);
        
        if index == 0 {
            assert!(fields[0] == "aa".as_bytes());
            assert!(fields[3] == "dd".as_bytes());
        } else {
            assert!(fields[0] == "ee".as_bytes());
            assert!(fields[3] == "hh".as_bytes());
        }
    });
}

Parser::parse::<10>() 调用为每行分配足够的空间以容纳10个字段。任何额外的字段将被丢弃,不会产生任何错误。

用户指南

解析字符串

Parser::parse() 方法解析以无符号字节数组形式提供的CSV数据。

您需要估计每行期望的字段数量。这是在编译时静态分配空间所需的。您始终可以采取谨慎的态度。例如,如果您期望每行有4个字段,您可以将解析器配置为10个字段。

fn test_record() {
    let str =
"aa,bb,cc,dd\r\n\
ee,ff,gg,hh\r\n";
    
    let mut parser = rcsv::Parser::new();

    parser.parse::<10>(str.as_bytes(), |index, fields| {
        println!("Record no: {}", index);
        println!("Field count: {}", fields.len());
    });
}

应该打印

Record no: 0
Field count: 4
Record no: 1
Field count: 4

parse() 方法接收两个参数。

  1. 要解析的数据。
  2. 一个闭包,它对每个记录(CSV中的行)进行调用。

闭包接收两个参数

  1. 记录的索引。第一行的索引为0。
  2. 字段数组。每个字段都是一个无符号字节数组 &[u8]

如果记录的字段数多于解析器配置的字段数,则多余的字段将被丢弃,并且不会报告给lambda。

fn test_uneven() {
    let str =
"aa,bb,cc,dd\r\n\
ee,ff,gg\r\n\
hh,ii\r\n";
    
    let mut parser = rcsv::Parser::new();

    parser.parse::<3>(str.as_bytes(), |index, fields| {
            assert!(index < 3);
        
            if index == 0 {
                assert!(fields.len() == 3);
                
                assert!(fields[0] == "aa".as_bytes());
                assert!(fields[2] == "cc".as_bytes());
            } else if index == 1 {
                assert!(fields.len() == 3);
                
                assert!(fields[0] == "ee".as_bytes());
                assert!(fields[1] == "ff".as_bytes());
            } else {
                assert!(fields.len() == 2);
                
                assert!(fields[0] == "hh".as_bytes());
                assert!(fields[1] == "ii".as_bytes());
            }
        });
}

解析CSV文件

内存映射用于从CSV文件中读取。

假设您有一个名为 test.csv 的文件,如下所示。

aa,bb,cc
dd,ee,ff
gg,hh,ii

我们可以这样读取文件。

fn test_memory_map_reader() {
    let mapper = match rcsv::mmap::FileMapper::new("test.csv") {
        Ok(r) => r,
        Err(e) => {
            panic!("{}", e);
        }
    };

    let data = mapper.get_bytes();
    let mut parser = rcsv::Parser::new();

    parser.parse::<3>(data, |index, fields| {
        assert!(index < 3);
            
        if index == 0 {
            assert!(fields.len() == 3);
            
            assert!(fields[0] == "aa".as_bytes());
            assert!(fields[2] == "cc".as_bytes());
        } else if index == 1 {
            assert!(fields.len() == 3);
            
            assert!(fields[0] == "dd".as_bytes());
            assert!(fields[1] == "ee".as_bytes());
        } else {
            assert!(fields.len() == 3);
            
            assert!(fields[0] == "gg".as_bytes());
            assert!(fields[1] == "hh".as_bytes());
        }
    });
}

标准一致性

该库符合RFC 4180。它稍微放宽了标准以使其更灵活。这些偏差将在下面讨论。

UNIX换行符

RFC 4180要求每行以CRLF(\r\n)结束。在Linux和macOS中,文件通常以仅LF结尾。库可以容忍这样的文件。

空格在转义字段周围

RFC明确指出空格是字段的一部分。它们不应被忽略。然而,不清楚转义字段的引号前后是否有空格。ABNF语法似乎表明不应该有空格。解析器会丢弃转义字段引号前后空格。

在下面的示例中,未转义的字段 aacc 周围有空格。这些空格被保留。然而,对于像 "bb" 这样的转义字段,引号之外的空间将被忽略。

 aa, "bb",  cc ,
   " dd "  , " ee "
fn test_space() {
    let str = r#" aa, "bb",  cc ,
  " dd ", " ee "
"#;
    let mut parser = rcsv::Parser::new();

    parser.parse::<10>(str.as_bytes(), |index, fields| {
        assert!(index < 2);
        
        if index == 0 {
            assert!(fields[0] == " aa".as_bytes());
            assert!(fields[1] == "bb".as_bytes());
            assert!(fields[2] == "  cc ".as_bytes());
        } else {
            assert!(fields[0] == " dd ".as_bytes());
            assert!(fields[1] == " ee ".as_bytes());
        }
    });
}

取消转义双引号

解析器不会取消转义转义的双引号。我没有找到不分配就能简单做到这一点的方法。在下面的示例中,字段 "b""b" 是未经取消转义双引号报告给lambda的。

aa,"b""b",cc,"d,d"
ee,ff,"g
g",hh
fn test_basic_escape() {
    let str = r#"aa,"b""b",cc,"d,d"
"ee",ff,"g
g",hh
"#;
    let mut parser = rcsv::Parser::new();

    parser.parse::<10>(str.as_bytes(), |index, fields| {
        assert!(index < 2);
        
        if index == 0 {
            assert!(fields[1] == "b\"\"b".as_bytes());
            assert!(fields[3] == "d,d".as_bytes());
        } else {
            assert!(fields[0] == "ee".as_bytes());
            assert!(fields[2] == "g\ng".as_bytes());
            assert!(fields[3] == "hh".as_bytes());
        }
    });
}

内存安全

在Rust中,数组索引运算符 [index] 进行边界检查。切片运算符 [start..stop] 也进行相同的检查。在该方面,库应该是内存安全的。

依赖关系

~215KB