如何读取一个非UTF8编码的csv文件?

8

使用csv库和最新的Rust版本1.31.0,我希望能够像读取UTF-8编码文件一样轻松地读取ANSI(Windows 1252)编码的CSV文件。

在将整个文件读入Vec<u8>后,我尝试过以下方法(但都没有成功):

事实上,在我们公司中,有许多ANSI编码的CSV文件。

如果可能的话,我也希望不要将整个文件加载到Vec<u8>中,而是逐行读取(以CRLF结尾),因为许多文件都很大(50 MB或更多...)。

在文件Cargo.toml中,我有这个依赖项:

[dependencies]
csv = "1"

"

test.csv包含以下内容,保存为Windows-1252编码:

"
Café;au;lait
Café;au;lait

main.rs文件中的代码:

extern crate csv;

use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::process;

fn example() -> Result<(), Box<Error>> {
    let file_name = r"test.csv";
    let file_handle = File::open(Path::new(file_name))?;
    let reader = BufReader::new(file_handle);

    let mut rdr = csv::ReaderBuilder::new()
        .delimiter(b';')
        .from_reader(reader);

    // println!("ANSI");
    // for result in rdr.byte_records() {
    //    let record = result?;
    //    println!("{:?}", record);
    // }

    println!("UTF-8");
    for result in rdr.records() {
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

fn main() {
    if let Err(err) = example() {
        println!("error running example: {}", err);
        process::exit(1);
    }
}

输出为:
UTF-8
error running example: CSV parse error: record 0 (line 1, field: 0, byte: 0): invalid utf-8: invalid UTF-8 in field 0 near byte index 3
error: process didn't exit successfully: `target\debug\test-csv.exe` (exit code: 1)

当使用rdr.byte_records()(取消代码的相关部分注释)时,输出如下:
ANSI
ByteRecord(["Caf\\xe9", "au", "lait"])

@LinkerStorm,您能否解释一下为什么您上一个代码片段不足以解决您的问题?字节记录以某种编码无关的方式为您提供对字节的原始访问。(唯一的要求是该编码与ASCII兼容,我相信Windows-1252是兼容的。) - BurntSushi5
1个回答

26
我怀疑这个问题未被充分说明。特别是,不清楚为什么您使用 ByteRecord API 不足够。在 csv crate 中,字节记录专门用于像这样的情况,其中您的 CSV 数据不严格为 UTF-8,而是以另一种编码(如 Windows-1252)作为 ASCII 兼容编码存在。(ASCII 兼容编码是指 ASCII 为其子集的编码。Windows-1252 和 UTF-8 都是 ASCII 兼容的。UTF-16 不是)。您上面的代码示例显示您正在使用字节记录,但没有解释为什么这是不够的。
话虽如此,如果您的目标是将数据放入 Rust 的字符串数据类型 (String/&str),那么您的唯一选择是将 CSV 数据的内容从 Windows-1252 转换为 UTF-8。这是必要的,因为 Rust 的字符串数据类型使用 UTF-8 作为其内存表示。您不能拥有一个 Rust String/&str,它的编码是 Windows-1252,因为 Windows-1252 不是 UTF-8 的子集。
其他评论已经推荐使用encoding crate。然而,如果您的用例与Encoding Standard解决的相同用例相符,则我建议使用encoding_rs,该标准专门针对网络。幸运的是,我相信存在这样的对齐。
为了满足您以流方式读取CSV数据而无需首先将整个内容加载到内存中的标准,您需要使用一个包装器来实现围绕encoding_rs crate的流解码。encoding_rs_io crate为您提供了这个功能。(它在 ripgrep 中用于在搜索UTF-8之前进行快速流解码。)
以下是一个将所有上述内容结合在一起的示例程序,使用Rust 2018:
use std::fs::File;
use std::process;

use encoding_rs::WINDOWS_1252;
use encoding_rs_io::DecodeReaderBytesBuilder;

fn main() {
    if let Err(err) = try_main() {
        eprintln!("{}", err);
        process::exit(1);
    }
}

fn try_main() -> csv::Result<()> {
    let file = File::open("test.csv")?;
    let transcoded = DecodeReaderBytesBuilder::new()
        .encoding(Some(WINDOWS_1252))
        .build(file);
    let mut rdr = csv::ReaderBuilder::new()
        .delimiter(b';')
        .from_reader(transcoded);
    for result in rdr.records() {
        let r = result?;
        println!("{:?}", r);
    }
    Ok(())
}

使用 Cargo.toml 文件:

[package]
name = "so53826986"
version = "0.1.0"
edition = "2018"

[dependencies]
csv = "1"
encoding_rs = "0.8.13"
encoding_rs_io = "0.1.3"

输出结果:

$ cargo run --release
   Compiling so53826986 v0.1.0 (/tmp/so53826986)
    Finished release [optimized] target(s) in 0.63s
     Running `target/release/so53826986`
StringRecord(["Café", "au", "lait"])

特别是,如果你将rdr.records()替换为rdr.byte_records(),那么我们就可以更清楚地看到发生了什么:

$ cargo run --release
   Compiling so53826986 v0.1.0 (/tmp/so53826986)
    Finished release [optimized] target(s) in 0.61s
     Running `target/release/so53826986`
ByteRecord(["Caf\\xc3\\xa9", "au", "lait"])

换句话说,您的输入包含 Caf\xE9,但字节记录现在包含 Caf\xC3\xA9。这是将 Windows-1252 代码点值 233(编码为其文字字节\xE9)转换为 U+00E9 LATIN SMALL LETTER E WITH ACUTE 的结果,它以 UTF-8 编码为 \xC3\xA9


1
感谢@BurnSushi5,你说得对:我的目标是将数据获取为Rust字符串数据类型。 你已经回答了我的问题,非常感谢! - Linker Storm
2
真是令人惊叹,有些人做出了如此巨大的贡献。这已经是BurntSushi第N次帮我解决Rust编程难题了。感谢你,Andrew! :) - max

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接