C++: 简约 CSV 流






4.81/5 (74投票s)
几行代码即可读写 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 库。由于分隔符可以即时更改,我使用了这个库,在相对较短的时间内编写了用于 MTL 和 Wavefront 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:现在,您引用的字符串中的引号会被转义。默认转义字符串是
"""
,可以通过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
输出问题,并添加了NChar
(char
包装器)类,用于将数值[-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++)相比,基准测试可能会有所不同,而我现在无法访问它们。
相关文章
- C++: 简约 CSV 流
- C++14: 基于 C 文件 API 的 CSV 流
- C++: 简化二进制流
- C++: 新的文本流