string的split

C++ 的 string 为什么不提供 split 函数?

参考文档: https://www.zhihu.com/question/36642771

C++11以前有很多原因不能提供一个通用的split,比如说需要考虑split以后的结果存储在什么类型的容器中,可以是vector、list等等包括自定义容器,很难提供一个通用的;再比如说需要split的源字符串很大的时候运算的时间可能会很长,所以这个split最好是lazy的,每次只返回一条结果。

C++11之前只能自己写,我目前发现的史上最优雅的一个实现是这样的:

void split(const string &s, vector<string> &tokens, const string &delimiters = " ")
{
  string::size_type lastPos = s.find_first_not_of(delimiters, 0);
  string::size_type pos = s.find_first_of(delimiters, lastPos);
  while (string::npos != pos || string::npos != lastPos)
  {
    tokens.push_back(s.substr(lastPos, pos - lastPos));
    //use emplace_back after C++11
    lastPos = s.find_first_not_of(delimiters, pos);
    pos = s.find_first_of(delimiters, lastPos);
  }
}

从C++11开始,标准库中提供了regex,regex用来做split就是小儿科了,比如:

std::string text = "Quick brown fox.";
std::regex ws_re("\\s+"); // whitespace
std::vector<std::string> v(std::sregex_token_iterator(text.begin(), text.end(), ws_re, -1), std::sregex_token_iterator());
for (auto &&s : v)
  std::cout << s << "\n";

C++17提供的string_view可以加速上面提到的第一个split实现,减少拷贝,性能有不小提升,参看此文:Speeding Up string_view String Split Implementation。

从C++20开始,标准库中提供了ranges,有专门的split view,只要写str | split(’ ‘)就可以切分字符串,如果要将结果搜集到vector中,可以这样用(随手写的,可能不是最简):

string str("hello world test split");
auto sv = str 
  | ranges::views::split(' ') 
  | ranges::views::transform([](auto &&i){ 
     return i | ranges::to<string>(); }) 
  |ranges::to<vector>();

for (auto &&s : sv)
{
  cout << s << "\n";
}

其实C语言里面也有一个函数strtok用于char*的split,例如:

#include <iostream> 
#include <string> 

using namespace std; 

int main() { 
  string str = "one two three four five"; 
  char *token = strtok(str.data(), " "); // non-const data() needs c++17
  while (token != NULL)
  {
    std::cout << token << '\n';
    token = strtok(NULL, " ");
  }
}

这里要注意的是strtok的第一个参数类型是char*而不是const char*,实际上strtok的确会改变输入的字符串。

换行符拆分

参考文档: https://www.techiedelight.com/zh/split-a-string-on-newlines-in-cpp/

使用 std::getline

在换行符上拆分字符串的一个简单解决方案是使用 std::getline 功能。它可用于从由换行符分隔的输入流中提取标记,如下所示:

#include <iostream>
#include <string>
#include <vector>
#include <regex>
 
std::vector<std::string> splitString(const std::string& str)
{
    std::vector<std::string> tokens;
 
    std::stringstream ss(str);
    std::string token;
    while (std::getline(ss, token, '\n')) {
        tokens.push_back(token);
    }
 
    return tokens;
}
 
int main()
{
    std::string str = "C\nC++\nJava";
 
    std::vector<std::string> tokens = splitString(str);
 
    for (auto const &token: tokens) {
        std::cout << token << std::endl;    
    }
 
    return 0;
}

使用 string::find

std::string::find 成员函数从指定位置开始在字符串中搜索指定字符。它返回指定字符的第一次出现和 string::npos 如果没有找到。它可以按如下方式在换行符上拆分字符串:

#include <iostream>
#include <string>
#include <vector>
 
std::vector<std::string> splitString(const std::string& str)
{
    std::vector<std::string> tokens;
 
    std::string::size_type pos = 0;
    std::string::size_type prev = 0;
    while ((pos = str.find('\n', prev)) != std::string::npos) {
        tokens.push_back(str.substr(prev, pos - prev));
        prev = pos + 1;
    }
    tokens.push_back(str.substr(prev));
 
    return tokens;
}
 
int main()
{
    std::string str = "C\nC++\nJava";
 
    std::vector<std::string> tokens = splitString(str);
 
    for (auto const &token: tokens) {
        std::cout << token << std::endl;    
    }
 
    return 0;
}

使用升压

最后,我们可以使用 boost::algorithm::split_regex boost 库提供的算法,用于将输入序列拆分为由分隔符分隔的标记。此函数等效于 C strtok,在头文件中可用 <boost/algorithm/string/regex.hpp>.

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <boost/regex.hpp>
#include <boost/algorithm/string/regex.hpp>
 
std::vector<std::string> splitString(const std::string& str)
{
    std::vector<std::string> tokens;
    split_regex(tokens, str, boost::regex("(\r\n)+"));
    return tokens;
}
 
int main()
{
    std::string str = "C\nC++\nJava";
 
    std::vector<std::string> tokens = splitString(str);
 
    for (auto const &token: tokens) {
        std::cout << token << std::endl;    
    }
 
    return 0;
}

使用 std::sregex_token_iterator

#include <iostream>
#include <string>
#include <vector>
#include <regex>

int main()
{
  std::string str = "adas\nsensor\ngnss\ninspvax\nInspvax.ap";
  std::regex line_re("\n");

  std::vector<std::string> lines(
      std::sregex_token_iterator(str.begin(), str.end(), line_re, -1),
      std::sregex_token_iterator());

  for (auto s : lines)
  {
    std::cout << "line:---" << s << std::endl;
  }

  return 0;
}

文件中带\n的字符串

文件内容: cat data

abc\n123\ngnss\ninspvax\nInspvax

上面代码:

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <fstream>

int main()
{
  std::ifstream infile; 
  infile.open("./data");

  std::ostringstream tmp;
  tmp << infile.rdbuf();
  std::string str = tmp.str();
  infile.close();

  std::regex line_re("\n");

  std::vector<std::string> lines(
      std::sregex_token_iterator(str.begin(), str.end(), line_re, -1),
      std::sregex_token_iterator());

  for (auto s : lines)
  {
    std::cout << "line:---" << s << std::endl;
  }

  return 0;
}

输出:

line:---abc\n123\ngnss\ninspvax\nInspvax

正确分割方式

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <fstream>

int main()
{
  std::ifstream infile; 
  infile.open("./data");

  std::ostringstream tmp;
  tmp << infile.rdbuf();
  std::string str = tmp.str();
  infile.close();

  std::regex line_re("\\\\n");

  std::vector<std::string> lines(
      std::sregex_token_iterator(str.begin(), str.end(), line_re, -1),
      std::sregex_token_iterator());

  for (auto s : lines)
  {
    std::cout << "line:---" << s << std::endl;
  }

  return 0;
}

输出:

line:---abc
line:---123
line:---gnss
line:---inspvax
line:---Inspvax

原因: 文件中换行\n并不是以字符方式展现,如果文件中字符串存在\n它就是一个字符串。