如何在C++中对字符串进行标记化?

476

Java有一个方便的split方法:

String str = "The quick brown fox";
String[] results = str.split(" ");

C++中是否有简便的方法来做到这一点?


237
我无法相信这个常规任务在C++中会如此令人头疼。 - wfbarksdale
6
在 C++ 中并不难实现“分割字符串”这个功能,有多种方法可以实现。相比之下,程序员对 C# 的掌握度要高于 C++,这与市场营销和投资有关。如果需要了解在 C++ 中实现字符串分割的不同方法,可以参考以下链接:http://www.cplusplus.com/faq/sequences/strings/split/。 - hB0
11
@hB0浏览了许多问题和答案,但仍然无法决定,这是一个头痛的问题。其中一个需要那个库,另一个只是为了空间,另一个处理不了空间。 - Paschalis
8
为什么C++中的一切都必须是一场挣扎? - Wael Assaf
显示剩余7条评论
37个回答

194
Boost分词器类可以使这种事情变得非常简单:

Boost tokenizer 类。

#include <iostream>
#include <string>
#include <boost/foreach.hpp>
#include <boost/tokenizer.hpp>

using namespace std;
using namespace boost;

int main(int, char**)
{
    string text = "token, test   string";

    char_separator<char> sep(", ");
    tokenizer< char_separator<char> > tokens(text, sep);
    BOOST_FOREACH (const string& t, tokens) {
        cout << t << "." << endl;
    }
}

更新至 C++11:

#include <iostream>
#include <string>
#include <boost/tokenizer.hpp>

using namespace std;
using namespace boost;

int main(int, char**)
{
    string text = "token, test   string";

    char_separator<char> sep(", ");
    tokenizer<char_separator<char>> tokens(text, sep);
    for (const auto& t : tokens) {
        cout << t << "." << endl;
    }
}

1
很好的东西,我最近用了这个。我的Visual Studio编译器有一个奇怪的抱怨,直到我使用一个空格来分隔tokens(text, sep)之前的两个“>”字符:(错误C2947:期望“>”终止模板参数列表,找到“>>”) - AndyUK
@AndyUK 是的,没有空格编译器会将其解析为提取运算符而不是两个闭合模板。 - EnabrenTane
3
注意char_separator构造函数的第三个参数(drop_empty_tokens是默认选项,另一个选项是keep_empty_tokens)。 - Benoit
双重大于符号>>在C++11中不应该是问题,如果有问题,那么VS2010编译器+存在一个错误,您应该在connect上报告它。 - paulm
5
@puk - 这是C++头文件常用的后缀,类似于C语言头文件的“.h”。 - Ferruccio
显示剩余2条评论

181
这是一个非常简单的例子:
#include <vector>
#include <string>

vector<string> split(const char *str, char c = ' ')
{
    std::vector<std::string> result;

    do
    {
        const char *begin = str;

        while(*str != c && *str)
            str++;

        result.push_back(std::string(begin, str));
    } while (0 != *str++);

    return result;
}

我需要在.h文件中添加这个方法的原型吗? - Suhrob Samiev
12
由于仍使用字符串文字,这并不是“最佳”答案。字符串文字是C中的常规字符数组。我认为提问者想知道是否可以对C++中类型为“string”的字符串进行标记化处理。 - Vijay Kumar Kanta
这需要一个新的答案,因为我强烈怀疑在C++11中包含正则表达式已经改变了最佳答案。 - Omnifarious
1
这个答案存在字符串问题,即第一个/最后一个字符与分隔符相等。例如,字符串“a”会导致结果为[" ", "a"]。 - y30
@y30: 我认为它的结果是 ["","a"] - undefined

175
C++标准库算法普遍基于迭代器而不是具体容器。不幸的是,这使得在C++标准库中提供类似Java的split函数变得困难,尽管没有人会认为这很方便。但它的返回类型会是什么?std::vector<std::basic_string<…>>吗?也许是,但那样我们就被迫执行(可能是冗余和昂贵的)分配。
相反,C++提供了大量的方式来基于任意复杂的分隔符分割字符串,但没有一个像其他语言那样封装得好。这些方法有很多填充整个博客帖子
最简单的方法是使用std::string::find进行迭代,直到遇到std::string::npos,然后使用std::string::substr提取内容。
更流畅(和惯用的,但基本的)在空格上分割的版本将使用std::istringstream
auto iss = std::istringstream{"The quick brown fox"};
auto str = std::string{};

while (iss >> str) {
    process(str);
}

使用std::istream_iterators,可以利用其迭代器范围构造函数将字符串流的内容复制到向量中。
多个库(例如Boost.Tokenizer)提供特定的分词器。
更高级的拆分需要正则表达式。C++特别提供了std::regex_token_iterator以实现此目的:
auto const str = "The quick brown fox"s;
auto const re = std::regex{R"(\s+)"};
auto const vec = std::vector<std::string>(
    std::sregex_token_iterator{begin(str), end(str), re, -1},
    std::sregex_token_iterator{}
);

64
遗憾的是,boost并不总是适用于所有项目。我将不得不寻找一个非boost的解决方案。 - FuzzyBunnySlippers
45
并非所有项目都适用于“开源”模式。我工作的行业受到严格监管。这并不是一个问题,而是生活的事实。Boost并不普及到每个地方。 - FuzzyBunnySlippers
5
@NonlinearIdeas另一个问题/答案根本不是关于开源项目的。对于任何项目都是如此。话虽如此,我当然明白像MISRA C这样的限制性标准,但那时你已经理解了你需要从头开始构建一切(除非你碰巧找到符合标准的库-这很少见)。无论如何,重点并不是“Boost不可用”——而是您有特殊要求,几乎任何通用答案都不适用。 - Konrad Rudolph
1
@NonlinearIdeas 举个例子,其他非Boost的答案也不符合MISRA标准。 - Konrad Rudolph
4
“STL barf”是什么?整个社区都非常赞成替换C预处理器——事实上,已经有提议这样做了。但您建议使用PHP或其他语言代替将是一个巨大的倒退。 - Konrad Rudolph
显示剩余18条评论

147
另一种快速的方法是使用getline。类似这样:
std::istringstream iss(str);
std::string s;

while (std::getline(iss, s, ' ')) {
  std::cout << s << std::endl;
}

如果你愿意的话,可以编写一个简单的split()方法,返回一个std::vector<string>,这非常有用。

2
我在使用这种技术时遇到了问题,字符串中的0x0A字符使得while循环过早退出。否则,这是一个不错的简单快速的解决方案。 - Ryan H.
4
这很好,但必须记住,这样做不考虑默认分隔符'\n'。这个例子可以工作,但如果你使用像这样的东西:while(getline(inFile,word,' ')),其中inFile是包含多行的ifstream对象,你将会得到有趣的结果。 - hackrock
很遗憾getline返回流而不是字符串,这使得它在初始化列表中无法使用,除非使用临时存储。 - fuzzyTew
1
很酷!没有使用boost和C++11,是那些遗留项目的好解决方案! - Deqing
1
那就是答案,函数的名称有点别扭。 - Nils
如果字符串以分隔符结尾,您将不会得到最后一个空条目。这是因为根据定义,行以行末字符结束。 - Calmarius

119

使用strtok进行分词。在我看来,除非strtok无法满足您的需求,否则没有必要围绕分词构建一个类。可能不行,但是在我15多年的C和C++解析代码编写经验中,我始终使用strtok。这里是一个例子。

char myString[] = "The quick brown fox";
char *p = strtok(myString, " ");
while (p) {
    printf ("Token: %s\n", p);
    p = strtok(NULL, " ");
}

以下是一些注意事项(可能不适合您的需求)。在此过程中,字符串将被“破坏”,这意味着 EOS 字符将放置在定界符位置。正确使用可能需要您制作一个非 const 版本的字符串。您还可以在解析过程中更改定界符列表。

在我自己的看法中,上面的代码比编写一个单独的类要简单得多,更易于使用。对我来说,这是语言提供的那些功能之一,并且它执行得很好,很干净。它只是一个“基于 C”的解决方案。它是合适的、容易的,而且您无需编写大量额外的代码:-)


47
虽然我不讨厌C语言,但是strtok函数不是线程安全的,而且你需要确保发送给它的字符串包含一个空字符,以避免可能的缓冲区溢出。 - tloach
11
有strtok_r函数,但这是一个C++问题。 - Prof. Falken
3
在MS C++编译器中,strtok是线程安全的,因为内部静态变量是在TLS(线程本地存储)上创建的(实际上这取决于编译器)。 - Ahmed
5
“线程安全”不仅指函数能够在不同线程中运行。在这种情况下,如果在strtok运行时修改了线程,那么在整个strtok运行期间,字符串可能仍然有效,但是由于字符串已经发生更改,strtok仍然会出错。现在它已经超过了空字符,并且它将继续读取内存,直到它遇到安全违规或找到一个空字符。这是原始的C字符串函数的问题,如果没有在某个地方指定长度,就会遇到问题。 - tloach
4
strtok函数需要一个指向非常量空字符结尾的字符数组的指针,这在C++代码中不常见...你喜欢用什么方法将其从std::string转换为这种形式? - fuzzyTew
显示剩余5条评论

86

您可以直接使用流、迭代器和复制算法来实现这一点。

#include <string>
#include <vector>
#include <iostream>
#include <istream>
#include <ostream>
#include <iterator>
#include <sstream>
#include <algorithm>

int main()
{
  std::string str = "The quick brown fox";

  // construct a stream from the string
  std::stringstream strstr(str);

  // use stream iterators to copy the stream to the vector as whitespace separated strings
  std::istream_iterator<std::string> it(strstr);
  std::istream_iterator<std::string> end;
  std::vector<std::string> results(it, end);

  // send the vector to stdout.
  std::ostream_iterator<std::string> oit(std::cout);
  std::copy(results.begin(), results.end(), oit);
}

19
我发现那些“std::”很烦人,为什么不用“using”代替呢? - user35978
87
@Vadi说:因为编辑他人的帖子会显得相当冒犯。 @pheze说:我更喜欢让std保持原样,这样我就知道我的对象来自哪里,那只是一种风格问题。 - Matthieu M.
7
我理解你的想法,如果这种方法适合你,那么我认为这实际上是一个不错的选择。但是从教育的角度来看,我同意pheze的观点。在顶部添加"using namespace std"后,像这个完全外国的例子就更容易阅读和理解,因为解释以下行需要更少的努力...特别是在这个例子中,因为所有内容都来自标准库。您可以通过一系列的 "using std :: string;"等语句使其易于阅读,并明确对象来自何处,尤其是函数如此简短的情况下。 - cheshirekow
66
尽管 "std::" 前缀可能会让人感到烦恼或者难看,但最好在示例代码中包含它们,以便清楚地知道这些函数来自哪里。如果它们困扰了你,只需在偷走示例代码并将其声明为自己的代码后使用 "using" 来替换它们是微不足道的。 - dlchambers
21
没错,他所说的没错!最佳实践是使用std前缀。任何大型代码库肯定都会有自己的库和命名空间,使用"using namespace std"可能会在引起命名空间冲突时带来麻烦。 - Miek
显示剩余7条评论

77

使用regex_token_iterator的解决方案:

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

using namespace std;

int main()
{
    string str("The quick brown fox");

    regex reg("\\s+");

    sregex_token_iterator iter(str.begin(), str.end(), reg, -1);
    sregex_token_iterator end;

    vector<string> vec(iter, end);

    for (auto a : vec)
    {
        cout << a << endl;
    }
}

8
这应该是排名最高的答案。这是在 C++ >= 11 中正确的做法。 - Omnifarious
1
我很高兴一直滚动到这个答案(目前只有9个赞)。这正是这个任务所需要的C++11代码! - YePhIcK
优秀的答案不依赖于外部库,而是使用已有的库。 - Andrew
1
非常好的答案,提供了最大的分隔符灵活性。但是有一些注意事项:使用\s+正则表达式可以避免在文本中间出现空令牌,但如果文本以空格开头,则会产生一个空的第一个令牌。此外,正则表达式似乎很慢:在我的笔记本电脑上,对于20 MB的随机文本,它需要0.6秒,而strtok、strsep或Parham的答案使用str.find_first_of只需要0.014秒,Perl需要0.027秒,Python需要0.021秒。对于短文本,速度可能不是问题。 - Mark Gates
这太棒了。谢谢分享。 - Vijay Rajanna
7
或许看起来很酷,但这显然是对正则表达式的过度使用。只有在不关心性能的情况下才是合理的。 - Marek R

50

不冒犯大家,但对于这么简单的问题,你们把事情搞得复杂了。使用Boost有很多原因。但对于这么简单的问题,就像用20#的大锤打苍蝇。

void
split( vector<string> & theStringVector,  /* Altered/returned value */
       const  string  & theString,
       const  string  & theDelimiter)
{
    UASSERT( theDelimiter.size(), >, 0); // My own ASSERT macro.

    size_t  start = 0, end = 0;

    while ( end != string::npos)
    {
        end = theString.find( theDelimiter, start);

        // If at end, use length=maxLength.  Else use length=end-start.
        theStringVector.push_back( theString.substr( start,
                       (end == string::npos) ? string::npos : end - start));

        // If at end, use start=maxSize.  Else use start=end+delimiter.
        start = (   ( end > (string::npos - theDelimiter.size()) )
                  ?  string::npos  :  end + theDelimiter.size());
    }
}

例如(对于道格的情况),
#define SHOW(I,X)   cout << "[" << (I) << "]\t " # X " = \"" << (X) << "\"" << endl

int
main()
{
    vector<string> v;

    split( v, "A:PEP:909:Inventory Item", ":" );

    for (unsigned int i = 0;  i < v.size();   i++)
        SHOW( i, v[i] );
}

是的,我们可以让split()返回一个新向量,而不是传递一个向量。这很容易包装和重载。但根据我的实际操作,我经常发现重复使用预先存在的对象比始终创建新对象更好。只要我不忘记在两者之间清空向量即可。
参考:http://www.cplusplus.com/reference/string/string/
(我最初是在回答Doug的问题:C++ Strings Modifying and Extracting based on Separators (closed)。但由于Martin York用指针将该问题关闭并转到这里...我只需概括我的代码。)

12
为什么要定义一个只在一个地方使用的宏?你的UASSERT与标准assert有什么区别?将比较分成3个标记并没有起到除了需要更多逗号之外的任何作用。 - crelbor
1
也许 UASSERT 宏会在错误信息中显示两个比较值之间的实际关系和值?在我看来,这实际上是一个相当不错的想法。 - GhassanPL
12
为什么 std::string 类没有包含一个 split() 函数? - Mr. Shickadance
我认为while循环中的最后一行应该是start = ((end > (theString.size() - theDelimiter.size())) ? string::npos : end + theDelimiter.size());,而while循环应该是while (start != string::npos)。此外,在将子字符串插入向量之前,我会检查它是否为空。 - John K
如果输入有两个连续的分隔符,那么它们之间的字符串显然为空,应该插入到向量中。如果空值对于特定目的不可接受,那就是另一回事了,但在我看来,这种限制应该在这种非常通用的函数之外强制执行。 - Lauri Nurmi
为什么不允许空字符串作为分隔符呢? - user5818995

39

Boost库提供了一个 powerful 的 split 函数:boost::algorithm::split

示例程序:

#include <vector>
#include <boost/algorithm/string.hpp>

int main() {
    auto s = "a,b, c ,,e,f,";
    std::vector<std::string> fields;
    boost::split(fields, s, boost::is_any_of(","));
    for (const auto& field : fields)
        std::cout << "\"" << field << "\"\n";
    return 0;
}

输出:

"a"
"b"
" c "
""
"e"
"f"
""

29

这是一个只使用STL(大约5行代码)的简单解决方案,使用std::findstd::find_first_not_of处理分隔符的重复(例如空格或句号),以及前导和尾随分隔符:

#include <string>
#include <vector>

void tokenize(std::string str, std::vector<string> &token_v){
    size_t start = str.find_first_not_of(DELIMITER), end=start;

    while (start != std::string::npos){
        // Find next occurence of delimiter
        end = str.find(DELIMITER, start);
        // Push back the token found into vector
        token_v.push_back(str.substr(start, end-start));
        // Skip all occurences of the delimiter to find new start
        start = str.find_first_not_of(DELIMITER, end);
    }
}

试一试在线演示!


4
这是一个不错的建议,我认为您需要使用find_first_of()而不是find(),以便在处理多个分隔符时能够正确运作。 - user755921
2
@user755921 在使用find_first_not_of函数查找起始位置时,会跳过多个分隔符。 - Beginner
我投了Parham的解决方案并对其进行了一些修改:std::vector<string> tokenize(const std::string& str, const string& delimiters) { std::vector<string> result; size_t start = str.find_first_not_of(DELIMITER), end = start; while (start != std::string::npos) { // 找到下一个分隔符的位置 end = str.find(DELIMITER, start); // 将找到的标记添加到向量中 result.push_back(str.substr(start, end - start)); // 跳过所有分隔符的出现以查找新的起点 start = str.find_first_not_of(DELIMITER, end); } return result; } - HMartyrossian
用“delimiters”替换“DELIMITER”: std::vector<string> tokenize(const std::string& str, const string& delimiters) { std::vector<string> result; size_t start = str.find_first_not_of(delimiters), end = start; while (start != std::string::npos) { // 查找下一个分隔符的出现位置 end = str.find(delimiters, start); // 将找到的标记推入向量中 result.push_back(str.substr(start, end - start)); // 跳过所有分隔符的出现以查找新的起始位置 start = str.find_first_not_of(delimiters, end); } return result; } - HMartyrossian
@Parham,不幸的是,这似乎也会跳过空字段,例如:a,b,c,,,f,g 返回向量的5个成员a b c f g,而不包括空字符串。索引内容受到影响。:(在NMEA GPS句子等内容中,具有多个空字段的索引字符分隔字符串数据非常常见。 - guitarpicva

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