使用任意分隔符从 FileStream 读取
Reading from FileStream with arbitrary delimiter
我在使用 C++ 从文件中读取消息时遇到了问题。通常人们所做的是创建一个文件流,然后使用 getline()
函数来获取消息。 getline()
函数可以接受一个额外的参数作为分隔符,这样它 return 每个 "line" 由新的分隔符而不是默认的 '\n' 分隔。但是,此分隔符必须是字符。在我的用例中,消息中的分隔符可能是其他类似“|--|”的东西,所以我尝试获得一个解决方案,使其接受字符串作为分隔符而不是字符。
我稍微搜索了一下 Whosebug,发现了一些有趣的帖子。
Parse (split) a string in C++ using string delimiter (standard C++)
这个给出了一个使用 string::find()
和 string::substr()
来解析任意定界符的解决方案。然而,那里的所有解决方案都假设输入是一个字符串而不是一个流,在我的例子中,文件流数据太 big/waste 不能一次放入内存所以它应该逐个读入 msg (或大量的消息一次)。
实际上,通读了 std::getline()
函数的 gcc 实现,似乎更容易处理大小写分隔符是单个字符的情况。因为每次加载一大块字符时,您总是可以搜索分隔符并将它们分开。如果您的定界符超过一个字符,情况会有所不同,但定界符本身可能会跨越两个不同的块,并导致许多其他极端情况。
不确定以前是否有人遇到过这种要求以及你们如何优雅地处理它。好像有一个像istream& getNext (istream&& is, string& str, string delim)
这样的标准函数就好了?这对我来说似乎是一个通用的用例。为什么这个不在标准库中,这样人们就不用再单独实现自己的版本了?
非常感谢
如果您可以逐字节读取,您可以构建一个状态转换table有限状态机的实现来识别您的停止条件
std::string delimeter="someString";
//initialize table with a row per target string character, a column per possible char and all zeros
std::vector<vector<int> > table(delimeter.size(),std::vector<int>(256,0));
int endState=delimeter.size();
//set the entry for the state looking for the next letter and finding that character to the next state
for(unsigned int i=0;i<delimeter.size();i++){
table[i][(int)delimeter[i]]=i+1;
}
现在你可以像这样使用它了
int currentState=0;
int read=0;
bool done=false;
while(!done&&(read=<istream>.read())>=0){
if(read>=256){
currentState=0;
}else{
currentState=table[currentState][read];
}
if(currentState==endState){
done=true;
}
//do your streamy stuff
}
当然这仅在定界符为扩展 ASCII 时才有效,但对于像您的示例这样的内容它会很好地工作。
STL 根本不支持您的要求。您将必须编写自己的函数(或找到第 3 方函数)来满足您的需要。
例如,您可以使用 std::getline()
读取分隔符的第一个字符,然后使用 std::istream::get()
读取后续字符并将它们与分隔符的其余部分进行比较。例如:
std::istream& my_getline(std::istream &input, std::string &str, const std::string &delim)
{
if (delim.empty())
throw std::invalid_argument("delim cannot be empty!");
if (delim.size() == 1)
return std::getline(input, str, delim[0]);
str.clear();
std::string temp;
char ch;
bool found = false;
do
{
if (!std::getline(input, temp, delim[0]))
break;
str += temp;
found = true;
for (int i = 1; i < delim.size(); ++i)
{
if (!input.get(ch))
{
if (input.eof())
input.clear(std::ios_base::eofbit);
str.append(delim.c_str(), i);
return input;
}
if (delim[i] != ch)
{
str.append(delim.c_str(), i);
str += ch;
found = false;
break;
}
}
}
while (!found);
return input;
}
看起来,创建类似getline()
的内容最简单:读取到分隔符的last 字符。然后检查字符串对于分隔符是否足够长,如果是,是否以分隔符结尾。如果不是,请继续阅读:
std::string getline(std::istream& in, std::string& value, std::string const& separator) {
std::istreambuf_iterator<char> it(in), end;
if (separator.empty()) { // empty separator -> return the entire stream
return std::string(it, end);
}
std::string rc;
char last(separator.back());
for (; it != end; ++it) {
rc.push_back(*it);
if (rc.back() == last
&& separator.size() <= rc.size()
&& rc.substr(rc.size() - separator.size()) == separator) {
return rc.resize(rc.size() - separator.size());
}
}
return rc; // no separator was found
}
我在使用 C++ 从文件中读取消息时遇到了问题。通常人们所做的是创建一个文件流,然后使用 getline()
函数来获取消息。 getline()
函数可以接受一个额外的参数作为分隔符,这样它 return 每个 "line" 由新的分隔符而不是默认的 '\n' 分隔。但是,此分隔符必须是字符。在我的用例中,消息中的分隔符可能是其他类似“|--|”的东西,所以我尝试获得一个解决方案,使其接受字符串作为分隔符而不是字符。
我稍微搜索了一下 Whosebug,发现了一些有趣的帖子。
Parse (split) a string in C++ using string delimiter (standard C++)
这个给出了一个使用 string::find()
和 string::substr()
来解析任意定界符的解决方案。然而,那里的所有解决方案都假设输入是一个字符串而不是一个流,在我的例子中,文件流数据太 big/waste 不能一次放入内存所以它应该逐个读入 msg (或大量的消息一次)。
实际上,通读了 std::getline()
函数的 gcc 实现,似乎更容易处理大小写分隔符是单个字符的情况。因为每次加载一大块字符时,您总是可以搜索分隔符并将它们分开。如果您的定界符超过一个字符,情况会有所不同,但定界符本身可能会跨越两个不同的块,并导致许多其他极端情况。
不确定以前是否有人遇到过这种要求以及你们如何优雅地处理它。好像有一个像istream& getNext (istream&& is, string& str, string delim)
这样的标准函数就好了?这对我来说似乎是一个通用的用例。为什么这个不在标准库中,这样人们就不用再单独实现自己的版本了?
非常感谢
如果您可以逐字节读取,您可以构建一个状态转换table有限状态机的实现来识别您的停止条件
std::string delimeter="someString";
//initialize table with a row per target string character, a column per possible char and all zeros
std::vector<vector<int> > table(delimeter.size(),std::vector<int>(256,0));
int endState=delimeter.size();
//set the entry for the state looking for the next letter and finding that character to the next state
for(unsigned int i=0;i<delimeter.size();i++){
table[i][(int)delimeter[i]]=i+1;
}
现在你可以像这样使用它了
int currentState=0;
int read=0;
bool done=false;
while(!done&&(read=<istream>.read())>=0){
if(read>=256){
currentState=0;
}else{
currentState=table[currentState][read];
}
if(currentState==endState){
done=true;
}
//do your streamy stuff
}
当然这仅在定界符为扩展 ASCII 时才有效,但对于像您的示例这样的内容它会很好地工作。
STL 根本不支持您的要求。您将必须编写自己的函数(或找到第 3 方函数)来满足您的需要。
例如,您可以使用 std::getline()
读取分隔符的第一个字符,然后使用 std::istream::get()
读取后续字符并将它们与分隔符的其余部分进行比较。例如:
std::istream& my_getline(std::istream &input, std::string &str, const std::string &delim)
{
if (delim.empty())
throw std::invalid_argument("delim cannot be empty!");
if (delim.size() == 1)
return std::getline(input, str, delim[0]);
str.clear();
std::string temp;
char ch;
bool found = false;
do
{
if (!std::getline(input, temp, delim[0]))
break;
str += temp;
found = true;
for (int i = 1; i < delim.size(); ++i)
{
if (!input.get(ch))
{
if (input.eof())
input.clear(std::ios_base::eofbit);
str.append(delim.c_str(), i);
return input;
}
if (delim[i] != ch)
{
str.append(delim.c_str(), i);
str += ch;
found = false;
break;
}
}
}
while (!found);
return input;
}
看起来,创建类似getline()
的内容最简单:读取到分隔符的last 字符。然后检查字符串对于分隔符是否足够长,如果是,是否以分隔符结尾。如果不是,请继续阅读:
std::string getline(std::istream& in, std::string& value, std::string const& separator) {
std::istreambuf_iterator<char> it(in), end;
if (separator.empty()) { // empty separator -> return the entire stream
return std::string(it, end);
}
std::string rc;
char last(separator.back());
for (; it != end; ++it) {
rc.push_back(*it);
if (rc.back() == last
&& separator.size() <= rc.size()
&& rc.substr(rc.size() - separator.size()) == separator) {
return rc.resize(rc.size() - separator.size());
}
}
return rc; // no separator was found
}