我将向您展示一个完整的解决方案并向您解释。但让我们先来看看它:
#include <iostream>
#include <vector>
#include <fstream>
#include <regex>
#include <string>
#include <algorithm>
// I omit in the example here the manual input of the filenames. This exercise can be done by somebody else
// Use fixed filenames in this example.
const std::string inputFileName("r:\\input.txt");
const std::string outputFileName("r:\\output.txt");
// The delimiter for the source csv file
std::regex re{ R"(\|)" };
std::string addQuotes(const std::string& s) {
// if there are single quotes in the string, then replace them with double quotes
std::string result = std::regex_replace(s, std::regex(R"(")"), R"("")");
// If there is any quote (") or comma in the file, then quote the complete string
if (std::any_of(result.begin(), result.end(), [](const char c) { return ((c == '\"') || (c == ',')); })) {
result = "\"" + result + "\"";
}
return result;
}
// Some output function
void printData(std::vector<std::vector<std::string>>& v, std::ostream& os) {
// Go throug all rows
std::for_each(v.begin(), v.end(), [&os](const std::vector<std::string>& vs) {
// Define delimiter
std::string delimiter{ "" };
// Show the delimited strings
for (const std::string& s : vs) {
os << delimiter << s;
delimiter = ",";
}
os << "\n";
});
}
int main() {
// We first open the ouput file, becuse, if this cannot be opened, then no meaning to do the rest of the exercise
// Open output file and check, if it could be opened
if (std::ofstream outputFileStream(outputFileName); outputFileStream) {
// Open the input file and check, if it could be opened
if (std::ifstream inputFileStream(inputFileName); inputFileStream) {
// In this variable we will store all lines from the CSV file including the splitted up columns
std::vector<std::vector<std::string>> data{};
// Now read all lines of the CSV file and split it into tokens
for (std::string line{}; std::getline(inputFileStream, line); ) {
// Split line into tokens and add to our resulting data vector
data.emplace_back(std::vector<std::string>(std::sregex_token_iterator(line.begin(), line.end(), re, -1), {}));
}
std::for_each(data.begin(), data.end(), [](std::vector<std::string>& vs) {
std::transform(vs.begin(), vs.end(), vs.begin(), addQuotes);
});
// Output, to file
printData(data, outputFileStream);
// And to the screen
printData(data, std::cout);
}
else {
std::cerr << "\n*** Error: could not open input file '" << inputFileName << "'\n";
}
}
else {
std::cerr << "\n*** Error: could not open output file '" << outputFileName << "'\n";
}
return 0;
}
那么,让我们来看看。我们有功能
-
main,读取csv文件,拆分成token,转换,写入
-
addQuotes。必要时添加报价
-
printData打印他将数据转换成输出流
让我们从main 开始。 main会先打开输入文件和输出文件。
输入文件包含一种结构化数据,也称为 csv(逗号分隔值)。但是这里我们没有逗号,而是一个管道符号作为分隔符。
结果通常会存储在二维向量中。第一个维度是行,另一个维度是列。
那么,接下来我们需要做什么?正如我们所见,我们首先需要从源流中读取所有完整的文本行。这可以通过单行轻松完成:
for (std::string line{}; std::getline(inputFileStream, line); ) {
如您所见here,for 语句有一个声明/初始化部分,然后是一个条件,然后是一个语句,在循环结束时执行。这是众所周知的。
我们首先定义一个std::string 类型的变量“line”,并使用默认初始化器创建一个空字符串。然后我们使用std::getline 从流中读取一个完整的行并将其放入我们的变量中。 std::getline 返回对 sthe 流的引用,并且流具有重载的 bool 运算符,如果出现故障(或文件结尾),它将返回该处。因此,for 循环不需要额外检查文件结尾。而且我们不使用for循环的最后一条语句,因为通过读取一行,文件指针会自动前进。
这给了我们一个非常简单的for循环,逐行读取一个完整的文件。
请注意:在 for 循环中定义变量“line”,会将其范围限定为 for 循环。意思是,它只在 for 循环中可见。这通常是防止外部名称空间污染的良好解决方案。
好的,现在是下一行:
data.emplace_back(std::vector<std::string>(std::sregex_token_iterator(line.begin(), line.end(), digit), {}));
哦哦,那是什么?
好的,让我们一步一步来。首先,我们显然想在我们的二维数据向量中添加一些东西。我们将使用std::vectors 函数emplace_back。我们也可以使用push_back,但这意味着我们需要进行不必要的数据复制。因此,我们选择了emplace_back 来就地构建我们想要添加到二维数据向量中的东西。
我们要添加什么?我们想要添加一个完整的行,因此是一个列向量。在我们的例子中是std::vector<std::string>。而且,因为我们想在原地构造这个向量,所以我们用向量范围构造函数来调用它。请看这里:Constructor number 5。范围构造函数接受两个迭代器,一个开始迭代器和一个结束迭代器作为参数,并将迭代器指向的所有值复制到向量中。
所以,我们期望一个开始和结束迭代器。我们在这里看到了什么:
- 开始迭代器是:
std::sregex_token_iterator(line.begin(), line.end(), digit)
- 而结束迭代器就是
{}
但这是什么东西,sregex_token_iterator?
这是一个迭代一行中的模式的迭代器。模式由regex 给出。您可以阅读 here 关于 C++ 正则表达式库的信息。由于它非常强大,不幸的是,您需要了解它的时间更长一些。我不能在这里覆盖它。但是让我们为我们的目的描述它的基本功能:你可以用某种元语言描述一个模式,并且
std::sregex_token_iterator 将查找该模式,如果找到匹配项,则返回相关数据。在我们的例子中,模式非常简单:数字。这可以用“\d+”来描述,意思是,尝试匹配一个或多个数字。
现在将{} 作为结束迭代器。您可能已经读到{} 将执行默认构造/初始化。如果您阅读here, number 1,那么您会看到“默认构造函数”构造了一个序列结束迭代器。所以,正是我们需要的。
读取所有数据后,我们会将单个字符串转换为所需的输出。这将通过std::transform 和函数addQuotes 完成。
这里的策略是先用双引号替换单引号。
然后,接下来,我们看看,如果字符串中有任何逗号或引号,那么我们将整个字符串附加在引号中。
最后但同样重要的是,我们有一个简单的输出函数,可以将转换后的数据打印到文件中并显示在屏幕上。