65.9K
CodeProject 正在变化。 阅读更多。
Home

C++: 简约 CSV 流

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (74投票s)

2016年4月12日

MIT

4分钟阅读

viewsIcon

182224

downloadIcon

5003

几行代码即可读写 CSV!

引言

MiniCSV 是一个基于 C++ 文件流的小型单头文件库,使用起来相对容易。话不多说,让我们看一些实际代码。

写入

我们看到一个使用 csv::ofstream 类将制表符分隔的值写入文件的示例。现在,在 1.7 版本中调用 set_delimiter 时,您可以指定转义字符串。

#include "minicsv.h"

struct Product
{
    Product() : name(""), qty(0), price(0.0f) {}
    Product(std::string name_, int qty_, float price_) 
        : name(name_), qty(qty_), price(price_) {}
    std::string name;
    int qty;
    float price;
};

int main()
{
    csv::ofstream os("products.txt");
    os.set_delimiter('\t', "##");
    if(os.is_open())
    {
        Product product("Shampoo", 200, 15.0f);
        os << product.name << product.qty << product.price << NEWLINE;
        Product product2("Soap", 300, 6.0f);
        os << product2.name << product2.qty << product2.price << NEWLINE;
    }
    os.flush();
    return 0;
}

NEWLINE 定义为 '\n'。我们不能在这里使用 std::endl,因为 csv::ofstream 不是从 std::ofstream 派生的。

读取

要读取同一文件,将使用 csv::ifstream,而 std::cout 用于在控制台上显示读取的项。

#include "minicsv.h"
#include <iostream>

int main()
{
    csv::ifstream is("products.txt");
    is.set_delimiter('\t', "##");
    if(is.is_open())
    {
        Product temp;
        while(is.read_line())
        {
            is >> temp.name >> temp.qty >> temp.price;
            // display the read items
            std::cout << temp.name << "," << temp.qty << "," << temp.price << std::endl;
        }
    }
    return 0;
}

控制台输出如下

Shampoo,200,15
Soap,300,6

重载流运算符

字符串流已在 v1.6 中引入。我将向您展示一个如何为 Product 类重载字符串流运算符的示例。对于文件流,概念是相同的。

#include "minicsv.h"
#include <iostream>

struct Product
{
    Product() : name(""), qty(0), price(0.0f) {}
    Product(std::string name_, int qty_, float price_) : name(name_), 
                               qty(qty_), price(price_) {}
    std::string name;
    int qty;
    float price;
};

template<>
inline csv::istringstream& operator >> (csv::istringstream& istm, Product& val)
{
    return istm >> val.name >> val.qty >> val.price;
}

template<>
inline csv::ostringstream& operator << (csv::ostringstream& ostm, const Product& val)
{
    return ostm << val.name << val.qty << val.price;
}

int main()
{
    // test string streams using overloaded stream operators for Product
    {
        csv::ostringstream os;
        os.set_delimiter(',', "$$");
        Product product("Shampoo", 200, 15.0f);
        os << product << NEWLINE;
        Product product2("Towel, Soap, Shower Foam", 300, 6.0f);
        os << product2 << NEWLINE;

        csv::istringstream is(os.get_text().c_str());
        is.set_delimiter(',', "$$");
        Product prod;
        while (is.read_line())
        {
            is >> prod;
            // display the read items
            std::cout << prod.name << "|" << prod.qty << "|" << prod.price << std::endl;
        }
    }
    return 0;
}

这就是显示在控制台上的内容。

Shampoo|200|15
Towel, Soap, Shower Foam|300|6

如果类型具有 private 成员怎么办?创建一个接受 stream 对象的成员函数。

class Product
{
public:
    void read(csv::istringstream& istm)
    {
        istm >> this->name >> this->qty >> this->price;
    }
};

template<>
inline csv::istringstream& operator >> (csv::istringstream& istm, Product& prod)
{
    prod.read(istm);
    return istm;
}

结论

MiniCSV 是一个基于 C++ 文件流的小型 CSV 库。由于分隔符可以即时更改,我使用了这个库,在相对较短的时间内编写了用于 MTLWavefront OBJ 格式的文件解析器,与手写且没有任何库帮助的代码相比。MiniCSV 现在托管在 Github。感谢您的阅读!

历史

  • 2014-03-09:初始发布
  • 2014-08-20:删除智能指针的使用
  • 2015-03-23:通过移除每次写入一行的刷新,性能提高 75%,修复了多次重定义的 lnk2005 错误。read_line 替换 ifstream 上的 eof
  • 2015-09-22:v1.7:转义/反转义,以及对文本进行包围/修剪引号
  • 2015-09-24:添加了重载的 stringstream 运算符示例。
  • 2015-09-27:v1.7.2 中 const char* 的流运算符重载。
  • 2015-10-04:修复了 v1.7.3 中 G++ 和 Clang++ 的编译错误。
  • 2015-10-20:在启用了 enable_trim_quote_on_str 时,读取时忽略引号内的分隔符。例如:10.0,"Bottle,Cup,Teaspoon",123.0 将被读取为 3 个标记:<10.0><Bottle,Cup,Teaspoon><123.0>
  • 2016-05-05:现在,您引用的字符串中的引号会被转义。默认转义字符串是 "&quot;",可以通过 os.enable_surround_quote_on_str()is.enable_trim_quote_on_str() 进行更改
  • 2016-07-10:版本 1.7.9:读取 UTF-8 BOM
  • 2016-08-02:版本 1.7.10:用于流的分隔符类,因此如果分隔符不断变化,则无需重复调用 set_delimiter。请参阅下面的代码示例
    // demo sep class usage
    csv::istringstream is("vt:33,44,66");
    is.set_delimiter(',', "$$");
    csv::sep colon(':', "<colon>");
    csv::sep comma(',', "<comma>");
    while (is.read_line())
    {
        std::string type;
        int r = 0, b = 0, g = 0;
        is >> colon >> type >> comma >> r >> b >> g;
        // display the read items
        std::cout << type << "|" << r << "|" << b << "|" << g << std::endl;
    }
  • 2016-08-23:版本 1.7.11:修复了 num_of_delimiter 函数:不计算引号内的分隔符
  • 2016-08-26:版本 1.8.0:为数据转换添加了更好的错误消息。在此之前,使用 std::istringstream 进行的数据转换错误未被检测到。

    更改前
    template<typename T>
    csv::ifstream& operator >> (csv::ifstream& istm, T& val)
    {
        std::string str = istm.get_delimited_str();
        
    #ifdef USE_BOOST_LEXICAL_CAST
        val = boost::lexical_cast<T>(str);
    #else
        std::istringstream is(str);
        is >> val;
    #endif
    
        return istm;
    }

    更改后

    template<typename T>
    csv::ifstream& operator >> (csv::ifstream& istm, T& val)
    {
        std::string str = istm.get_delimited_str();
    
    #ifdef USE_BOOST_LEXICAL_CAST
        try 
        {
            val = boost::lexical_cast<T>(str);
        }
        catch (boost::bad_lexical_cast& e)
        {
            throw std::runtime_error(istm.error_line(str).c_str());
        }
    #else
        std::istringstream is(str);
        is >> val;
        if (!(bool)is)
        {
            throw std::runtime_error(istm.error_line(str).c_str());
        }
    #endif
    
        return istm;
    }

    重大更改:这意味着旧代码捕获 boost::bad_lexical_cast 必须更改为捕获 std::runtime_error。对于 csv::istringstream 也是如此。请注意,std::istringstream 在捕获错误方面不如 boost::lexical_cast。例如,"4a" 会在没有错误的情况下转换为整数 4

    csv::ifstream 错误日志示例如下

    csv::ifstream conversion error at line no.:2, 
    filename:products.txt, token position:3, token:aa

    对于 csv::istringstream 也是类似的,只是没有文件名。

    csv::istringstream conversion error at line no.:2, token position:3, token:aa
  • 2017-01-08:版本 1.8.2 改进了输入流性能。运行基准测试以查看(注意:请先更新驱动器/文件夹位置)。

    与 1.8.0 版本相比的基准测试结果:

           mini_180::csv::ofstream:  348ms
           mini_180::csv::ifstream:  339ms <<< v1.8.0
               mini::csv::ofstream:  347ms
               mini::csv::ifstream:  308ms <<< v1.8.2
      mini_180::csv::ostringstream:  324ms
      mini_180::csv::istringstream:  332ms <<< v1.8.0
          mini::csv::ostringstream:  325ms
          mini::csv::istringstream:  301ms <<< v1.8.2
  • 2017-01-23:版本 1.8.3 添加了单元测试,并允许 2 个引号转义 1 个引号,以符合 CSV 规范。
  • 2017-02-07:版本 1.8.3b 添加了更多单元测试,并删除了 CPOL 许可证文件。
  • 2017-03-12:版本 1.8.4 修复了一些 char 输出问题,并添加了 NCharchar 包装器)类,用于将数值 [-127..128] 写入 char 变量。
    bool test_nchar(bool enable_quote)
    {
        csv::ostringstream os;
        os.set_delimiter(',', "$$");
        os.enable_surround_quote_on_str(enable_quote, '\"');
    
        os << "Wallet" << 56 << NEWLINE;
    
        csv::istringstream is(os.get_text().c_str());
        is.set_delimiter(',', "$$");
        is.enable_trim_quote_on_str(enable_quote, '\"');
    
        while (is.read_line())
        {
            try
            {
                std::string dest_name = "";
                char dest_char = 0;
    
                is >> dest_name >> csv::NChar(dest_char);
    
                std::cout << dest_name << ", " 
                    << (int)dest_char << std::endl;
            }
            catch (std::runtime_error& e)
            {
                std::cerr << __FUNCTION__ << e.what() << std::endl;
            }
        }
        return true;
    }

    显示输出

    Wallet, 56
  • 2017-09-18: 版本 1.8.5

    如果 set_delimiter() 中的转义参数为空,则包含分隔符的文本将自动用引号括起来(以符合 Microsoft Excel 和一般的 CSV 实践)。

    "Hello,World",600

    Microsoft Excel 和 MiniCSV 将其读取为 "Hello,World" 和 600

  • 2021-02-21:版本 1.8.5d:修复了 quote_unescape 中的无限循环。
  • 2021-05-06:MiniCSV 通过换行符检测行的结尾。字符串输入中的换行符不可避免地会破坏解析。新版本 1.8.6 通过转义换行符来处理它。
  • 2023-03-11:v1.8.7 为 ostream_base 添加了 set_precision()reset_precision()get_precision(),用于设置输出中的 float/double/long double 精度。

常见问题解答

为什么读取器流在处理未用引号括起来的 CSV 文本时会遇到错误?

答案:要解决此问题,请记住将 enable_trim_quote_on_str 设置为 false

使用 MiniCSV 的产品

关注点

最近,我遇到了一个有趣的 基准测试结果,其中 Vincent La 的 string_view CSV 解析器读取了一个 5MB 文件。您可以看到短字符串缓冲区 (SSO) 的效果。

每个列的长度为 12 个字符的基准测试

长度在 SSO 限制(24 字节)之内,以避免堆分配。

csv_parser timing:113ms
MiniCSV timing:71ms
CSV Stream timing:187ms

每个列的长度为 30 个字符的基准测试

长度超出了 SSO 限制,内存必须在堆上分配!现在 string_view csv_parser 获胜。

csv_parser timing:147ms
MiniCSV timing:175ms
CSV Stream timing:434ms

注意:虽然我不确定为什么 CSV 流在 VC++ 15.9 更新中如此缓慢。

注意:与其他 C++ 编译器(如 G++ 和 Clang++)相比,基准测试可能会有所不同,而我现在无法访问它们。

相关文章

© . All rights reserved.