使用字符串分隔符(标准C++)在C++中解析(拆分)一个字符串

657

我正在使用以下代码在C++中解析字符串:

using namespace std;

string parsed,input="text to be parsed";
stringstream input_stringstream(input);

if (getline(input_stringstream,parsed,' '))
{
     // do some processing.
}

使用单个字符作为分隔符是可以的。但如果我想用一个字符串作为分隔符怎么办。

例如:我想要分割:

scott>=tiger

使用>=作为分隔符,以便我可以获取scott和tiger。


7
请跳转到链接 https://stackoverflow.blog/2019/10/11/c-creator-bjarne-stroustrup-answers-our-top-five-c-questions 并滚动至第五个问题。 - Wais Kamal
请参考此问题,使用C++20实现读取文件和分割字符串的功能。 - AmirSalar
1
@WaisKamal:你本可以直接链接到 https://dev59.com/k3VC5IYBdhLWcg3wnCj6。 - Thomas Weller
35个回答

965
你可以使用std::string::find()函数查找字符串分隔符的位置,然后使用std::string::substr()来获取一个令牌。
示例:
std::string s = "scott>=tiger";
std::string delimiter = ">=";
std::string token = s.substr(0, s.find(delimiter)); // token is "scott"
  • find(const string& str, size_t pos = 0)函数返回字符串中第一个出现str的位置,如果未找到,则返回npos

  • substr(size_t pos = 0, size_t n = npos)函数返回从pos位置开始长度为n的子字符串。


如果您有多个分隔符,在提取一个标记后,可以将其删除(包括分隔符),以继续进行后续提取(如果要保留原始字符串,请使用s = s.substr(pos + delimiter.length());):

s.erase(0, s.find(delimiter) + delimiter.length());

通过这种方式,您可以轻松循环获取每个标记。

完整示例

std::string s = "scott>=tiger>=mushroom";
std::string delimiter = ">=";

size_t pos = 0;
std::string token;
while ((pos = s.find(delimiter)) != std::string::npos) {
    token = s.substr(0, pos);
    std::cout << token << std::endl;
    s.erase(0, pos + delimiter.length());
}
std::cout << s << std::endl;

输出:

scott
tiger
mushroom

147
对于那些不想修改输入字符串的人,可以执行以下操作:size_t last = 0; size_t next = 0; while ((next = s.find(delimiter, last)) != string::npos) { cout << s.substr(last, next-last) << endl; last = next + 1; } cout << s.substr(last) << endl;这段代码会将字符串分割成若干子串并输出,分割标志由delimiter指定。 - Hayk Martiros
57
注意:mushroom在循环之外输出,即s = mushroom - Don Larynx
4
请注意,由于分隔符的长度为2个字符,因此您需要添加2而不是1: std::string s = "scott>=tiger>=mushroom"; std::string delimiter = ">="; size_t last = 0; size_t next = 0; while ((next = s.find(delimiter, last)) != std::string::npos) { std::cout << s.substr(last, next - last) << std::endl; last = next + 2; } std::cout << s.substr(last) << std::endl; - ervinbosenbacher
24
想知道这615位点赞者中有多少人错过了最后一行并在他们的生产代码中运行了隐藏的错误。从评论来看,我敢打赌至少有几个人会出现这种情况。在我看来,如果这个答案不使用cout而是以函数的形式展示,它将更加适合。 - Qix - MONICA WAS MISTREATED
2
FYI,在 while 循环中,npos 表示没有位置(或字符串结尾)。 - Tan Nguyen
显示剩余5条评论

153

用于字符串定界符

基于一个字符串定界符拆分字符串。例如,以字符串定界符"-+"为基础拆分字符串"adsf-+qwret-+nvfkbdsj-+orthdfjgh-+dfjrleih",输出将会是{"adsf", "qwret", "nvfkbdsj", "orthdfjgh", "dfjrleih"}

#include <iostream>
#include <sstream>
#include <vector>

// for string delimiter
std::vector<std::string> split(std::string s, std::string delimiter) {
    size_t pos_start = 0, pos_end, delim_len = delimiter.length();
    std::string token;
    std::vector<std::string> res;

    while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos) {
        token = s.substr (pos_start, pos_end - pos_start);
        pos_start = pos_end + delim_len;
        res.push_back (token);
    }

    res.push_back (s.substr (pos_start));
    return res;
}

int main() {
    std::string str = "adsf-+qwret-+nvfkbdsj-+orthdfjgh-+dfjrleih";
    std::string delimiter = "-+";
    std::vector<std::string> v = split (str, delimiter);

    for (auto i : v) cout << i << endl;

    return 0;
}
{"adsf", "qwer", "poui", "fdgh"}
#include <iostream>
#include <sstream>
#include <vector>

std::vector<std::string> split (const std::string &s, char delim) {
    std::vector<std::string> result;
    std::stringstream ss (s);
    std::string item;

    while (getline (ss, item, delim)) {
        result.push_back (item);
    }

    return result;
}

int main() {
    std::string str = "adsf+qwer+poui+fdgh";
    std::vector<std::string> v = split (str, '+');

    for (auto i : v) cout << i << endl;

    return 0;
}
adsf
qwer
poui
fdgh

你正在返回 vector<string>,我认为它会调用复制构造函数。 - Mayur
7
所有我看到的参考资料都表明,在这种情况下,对拷贝构造函数的调用会被消除。 - David Given
1
使用“现代”(C++03?)编译器,我相信这是正确的,RVO和/或移动语义将消除复制构造函数。 - Kevin
3
我尝试了单个字符分隔符的方法,如果字符串以分隔符结尾(即在行末有一个空的CSV列),它不会返回空字符串,而是返回少一个字符串的结果。例如:1,2,3,4\nA,B,C, - kounoupis
3
我也尝试了分隔符字符串的方法,如果字符串以分隔符结尾,最后一个分隔符将成为最后一个被提取出来的字符串的一部分。 - kounoupis

129

这种方法使用 std::string::find 而不会改变原始字符串,通过记住先前子字符串令牌的开头和结尾。

#include <iostream>
#include <string>

int main()
{
    std::string s = "scott>=tiger";
    std::string delim = ">=";

    auto start = 0U;
    auto end = s.find(delim);
    while (end != std::string::npos)
    {
        std::cout << s.substr(start, end - start) << std::endl;
        start = end + delim.length();
        end = s.find(delim, start);
    }

    std::cout << s.substr(start, end);
}

我如何在vector<string>上执行此操作,其中向量中的两个字符串具有相同的格式和相同的分隔符。我只想以与单个字符串相同的方式输出解析出的两个字符串。我的“string delim”当然将保持不变。 - Areeb Muzaffar
最后一行不应该是s.substr(start, end - start)吗?我猜这只有在start + end > size()的情况下才能起作用,因此它总是取字符串的剩余部分 - Jonas Wilms
1
由于 end == std::string::npos,这意味着我们要返回最后一个标记。 - moswald
2
最后一行可以进一步简化为s.substr(start),无需指定长度,因为如果省略长度,它将提取整个尾部子字符串。 - Peng
你可以将 end = s.find(delim, start) 移到 while 条件中。 - Steve Ward
如果你使用支持的C++版本,我觉得你可以用std::string_view(s.begin()+start, s.begin() + end - start)来替换substr以提高性能。 - Кое Кто

62
您可以使用下一个函数来拆分字符串:
vector<string> split(const string& str, const string& delim)
{
    vector<string> tokens;
    size_t prev = 0, pos = 0;
    do
    {
        pos = str.find(delim, prev);
        if (pos == string::npos) pos = str.length();
        string token = str.substr(prev, pos-prev);
        if (!token.empty()) tokens.push_back(token);
        prev = pos + delim.length();
    }
    while (pos < str.length() && prev < str.length());
    return tokens;
}

12
IMO它的效果不如预期:split("abc","a")会返回一个向量或单个字符串,即"bc",我认为如果它返回元素向量["", "bc"]会更有意义。在Python中使用str.split()时,如果在开头或结尾找到了定界符delim,对我来说它应该返回一个空字符串,但这只是我的观点。无论如何,我认为这应该被提到。 - kyriakosSt
5
强烈建议移除 if (!token.empty()) 来预防 @kyriakosSt 提到的问题以及与连续分隔符相关的其他问题。 - Steve
1
如果可以的话,我会取消我的赞,但是SO不会让我这样做。@kyriakosSt提出的问题是一个问题,而删除 if (!token.empty()) 似乎并不能完全解决它。 - bhaller
2
@bhaller 这个片段的设计目的是跳过空片段。如果您需要保留空片段,恐怕您需要编写另一个 split 实现。建议您为了社区的利益在这里发布它。 - Sviatoslav

49

C++20的一种实现方式:

#include <iostream>
#include <ranges>
#include <string_view>

int main()
{
    std::string hello = "text to be parsed";
    auto split = hello
        | std::ranges::views::split(' ')
        | std::ranges::views::transform([](auto&& str) { return std::string_view(&*str.begin(), std::ranges::distance(str)); });

    for (auto&& word : split)
    {
        std::cout << word << std::endl;
    }
}

请见:
https://dev59.com/_1YM5IYBdhLWcg3wxSN_#48403210
https://en.cppreference.com/w/cpp/ranges/split_view


9
哦,哇。有点复杂。 - Patrick Bassut
对于gcc,您似乎需要10或更高版本,对于Clang,它甚至无法使用最新版本(15),但是当前的主干版本可以工作。请参见http://godbolt.org/z/a6fEGYo16。可能在clang中存在此问题:https://github.com/llvm/llvm-project/issues/52696。 - Zitrax
简单的事情变得复杂了 :) Common Lisp(使用cl-ppcre库):(defvar *delimiters* (cl-ppcre:create-scanner " ")) (cl-ppcre:split *delimiters* "lets see if it works")。但有些人喜欢复杂化 :) - BitTickler
1
幸运的是,C++23增加了一个构造函数重载,用于std::string_view,使得这变得更简单。 - Matt Eding

44

你也可以使用正则表达式来实现这个功能:

std::vector<std::string> split(const std::string str, const std::string regex_str)
{
    std::regex regexz(regex_str);
    std::vector<std::string> list(std::sregex_token_iterator(str.begin(), str.end(), regexz, -1),
                                  std::sregex_token_iterator());
    return list;
}

等同于:

std::vector<std::string> split(const std::string str, const std::string regex_str)
{
    std::sregex_token_iterator token_iter(str.begin(), str.end(), regexz, -1);
    std::sregex_token_iterator end;
    std::vector<std::string> list;
    while (token_iter != end)
    {
        list.emplace_back(*token_iter++);
    }
    return list;
}

然后像这样使用:

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

std::vector<std::string> split(const std::string str,
                               const std::string regex_str) {
    std::regex regexz(regex_str);
    return {std::sregex_token_iterator(str.begin(), str.end(), regexz, -1),
            std::sregex_token_iterator()};
}

int main()
{
    std::string input_str = "lets split this";
    std::string regex_str = " "; 
    auto tokens = split(input_str, regex_str);
    for (auto& item: tokens)
    {
        std::cout<<item <<std::endl;
    }
}

在线试玩!

你可以像平常一样使用子字符串、字符等,也可以使用实际的正则表达式进行分割。
它还简洁而且是C++11!


2
如果C++11是可用的,那么这应该是正确的答案。如果不可用的话,你应该使用C++>=11,这会改变游戏规则! - DeusXMachina
请问您能否解释一下split()函数中的返回语句吗?我正在尝试弄清楚标记是如何被推入std::vector容器中的。谢谢。 - BFamz
将其编写为return std::vector<std::string>{ std::sregex_token_iterator(str.begin(), str.end(), std::regex(regex_str), -1), std::sregex_token_iterator() };会让你更清楚地了解如何创建和返回临时std::vector吗?我们在这里使用列表初始化。请看这里 - Hossein
8
@DeusXMachina说:“这确实是个不错的解决方案。”唯一的问题是,最后代码段中的“更加简洁的形式!”在 _LIBCPP_STD_VER > 11时将无法编译,因为该方法被标记为“delete”,但早期的代码段不需要隐式使用右值引用 && 可以编译并在 C++2a 下正常运行。 - pob
这对于大型案例来说似乎有些慢。除此之外非常好。 - Tom Sirgedas
我建议使用 std::string regex_str= "\\s+" 来避免在连续多个空格时出现空字符串。 - PLG

22

这段代码会将文本中的每一行分割出来,并将它们添加到一个向量中。

vector<string> split(char *phrase, string delimiter){
    vector<string> list;
    string s = string(phrase);
    size_t pos = 0;
    string token;
    while ((pos = s.find(delimiter)) != string::npos) {
        token = s.substr(0, pos);
        list.push_back(token);
        s.erase(0, pos + delimiter.length());
    }
    list.push_back(s);
    return list;
}

调用者:

vector<string> listFilesMax = split(buffer, "\n");

它运行得很好!我添加了list.push_back(s);因为它缺失了。 - Stoica Mircea
1
它错过了字符串的最后一部分。while循环结束后,我们需要将s的剩余部分作为新令牌添加进去。 - whihathac
我已经对代码示例进行了编辑,以修复缺失的push_back。 - fret
1
它会更好看一些 vector<string> split(char *phrase, const string delimiter="\n") - Mayur
我知道有点晚了,但是如果在push之前添加这个if语句if (token != "") list.push_back(token);来防止附加空字符串,它会更有效。 - Oliver Tworkowski
1
很多时候,被视为“正确”行为的做法是将空字符串保留下来。当然,在你的使用情况中可能不希望这样做,那么你的建议完全有效。 - squ1dd13

21
答案已经存在,但是选定的答案使用了非常耗费资源的擦除函数,想象一下一个非常大的字符串(以MB为单位)。因此我使用以下函数。
vector<string> split(const string& str, const string& delim)
{
    vector<string> result;
    size_t start = 0;

    for (size_t found = str.find(delim); found != string::npos; found = str.find(delim, start))
    {
        result.emplace_back(str.begin() + start, str.begin() + found);
        start = found + delim.size();
    }
    if (start != str.size())
        result.emplace_back(str.begin() + start, str.end());
    return result;      
}

2
我测试过了,它可以正常工作。谢谢!在我看来,这是最好的答案,因为原始回答者所述,此解决方案减少了内存开销,并且结果方便地存储在向量中。(复制了Python的string.split()方法。) - Robbie Capps
一个不错的改进是使用emplace_back()而不是push_back(string(...)) - jezza
@jezza 感到荣幸。 - Shubham Agrawal
你也可以删除对字符串构造函数的显式调用。emplace_back()将其参数转发给构造函数,因此您只需编写result.emplace_back(i_str.begin()+startIndex, i_str.begin()+found);即可。 - jezza
@jezza 完成了.... - Shubham Agrawal
你可以将 found = i_str.find(i_delim, startIndex) 移动到 while 条件中,以避免在两个位置调用 find - Steve Ward

19

strtok允许您将多个字符作为分隔符传入。我敢打赌,如果您传入">=",您的示例字符串将被正确分割(即使">"和"="被视为单独的分隔符)。

如果您不想使用c_str()将字符串转换为char*,则可以使用substrfind_first_of进行标记化。

string token, mystring("scott>=tiger");
while(token != mystring){
  token = mystring.substr(0,mystring.find_first_of(">="));
  mystring = mystring.substr(mystring.find_first_of(">=") + 1);
  printf("%s ",token.c_str());
}

3
谢谢。但是我想仅使用C ++,而不使用任何C函数,例如strtok(),因为这将要求我使用字符数组而不是字符串。 - TheCrazyProgrammer
2
@TheCrazyProgrammer 所以呢?如果一个 C 函数能够满足你的需求,那就直接使用它。这并不是一个在 C++ 中没有 C 函数的世界(事实上,在 C++ 中必须有这些函数)。.c_str() 也很简单便宜。 - Qix - MONICA WAS MISTREATED
1
如果您的字符串中有重复元素,则if(token != mystring)的检查会给出错误的结果。我使用了您的代码来创建一个没有这个问题的版本。它有很多改变,从根本上改变了答案,所以我写了自己的答案而不是编辑。请查看下面的代码。 - Amber Elferink

5

以防将来有人需要 Vincenzo Pii 的答案带外盒功能。

#include <vector>
#include <string>


std::vector<std::string> SplitString(
    std::string str,
    std::string delimeter)
{
    std::vector<std::string> splittedStrings = {};
    size_t pos = 0;

    while ((pos = str.find(delimeter)) != std::string::npos)
    {
        std::string token = str.substr(0, pos);
        if (token.length() > 0)
            splittedStrings.push_back(token);
        str.erase(0, pos + delimeter.length());
    }

    if (str.length() > 0)
        splittedStrings.push_back(str);
    return splittedStrings;
}

我修复了一些错误,使得该函数不会返回一个空字符串,即使在字符串的开头或结尾有分隔符。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接