Як зробити токенізацію рядка в C ++?


414

У Java є зручний метод розділення:

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

Чи є простий спосіб зробити це в C ++?


172
Я не можу повірити, що це звичайне завдання таке головний біль у c ++
wfbarksdale

6
Це не головний біль у с ++ - існують різні способи її досягнення. програмісти менше знають c ++, ніж c # - про маркетинг та інвестиції ... див. це для різних варіантів c ++, щоб досягти того самого: cplusplus.com/faq/sequences/strings/split
hB0

9
@ hB0 переживаючи багато відповідей на запитання і все ще не вирішуючи засоби, це головний біль. одному потрібна ця бібліотека, інша - просто для пробілів, інша не обробляє пробіли ..
Paschalis

1
Можливий дублікат розділення рядка на C ++?
KOB

2
Чому все в C ++ має бути боротьбою?
Wael Assaf

Відповіді:


145

Стандартні алгоритми бібліотеки C ++ є досить універсальними на основі ітераторів, а не конкретних контейнерів. На жаль, це ускладнює надання Java-подібної splitфункції у стандартній бібліотеці C ++, хоча ніхто не стверджує, що це було б зручно. Але яким би був його тип повернення? 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{}
);

53
На жаль, стимул не завжди доступний для всіх проектів. Мені доведеться шукати невідповідну відповідь.
FuzzyBunnySlippers

36
Не кожен проект відкритий для "відкритого коду". Я працюю в сильно регульованих галузях. Це не проблема, насправді. Це просто факт життя. Підвищення доступне не скрізь.
FuzzyBunnySlippers

5
@NonlinearIdeas Інше питання / відповідь взагалі не стосувався проектів з відкритим кодом. Те саме стосується будь-якого проекту. З цього приводу я, звичайно, розумію такі обмежені стандарти, як MISRA C, але тоді розуміється, що ви все одно будуєте з нуля (якщо ви не знайдете сумісну бібліотеку - рідкість). У будь-якому разі, навряд чи справа в тому, що "Boost недоступний" - це те, що у вас є спеціальні вимоги, щодо яких майже будь -яка відповідь загального призначення була б непридатною.
Конрад Рудольф

1
@NonlinearIdeas Справа в тому, що інші відповіді, що не підсилюються, також не відповідають стандартам MISRA.
Конрад Рудольф

3
@Dmitry Що таке "STL barf" ?! І вся громада дуже підтримує заміну препроцесора C - насправді є пропозиції зробити це. Але ваша пропозиція використовувати PHP або якусь іншу мову замість цього буде величезним кроком назад.
Конрад Рудольф

188

Клас токенізаторів Boost може зробити подібні речі досить простими:

#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 є незвичайний відбій, поки я не використовую пробіл для розділення двох ">" символів перед бітом токенів (текст, sep): (помилка C2947: очікує '>', щоб припинити шаблон-аргумент-список, знайдено '> > ')
AndyUK

@AndyUK так, без місця компілятор аналізує його як оператор вилучення, а не два шаблони закриття.
EnabrenTane

Теоретично це було зафіксовано в C ++ 0x
Девід Сутер

3
остерігайтеся третіх параметрів char_separatorконструктора ( drop_empty_tokensза замовчуванням є альтернатива keep_empty_tokens).
Бенуа

5
@puk - це поширений суфікс для файлів заголовків C ++. (як .hдля заголовків C)
Ferruccio

167

Ось справжній простий:

#include <vector>
#include <string>
using namespace std;

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

    do
    {
        const char *begin = str;

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

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

    return result;
}

мені потрібно додати прототип цього методу у файл .h?
Сухроб Самієв

5
Це не зовсім "найкраща" відповідь, оскільки вона все ще використовує літеральний рядок, який є простим масивом постійних символів C. Я вважаю, що запитуючий запитував, чи зможе він токенізувати рядок C ++, який вводиться останнім типу "string".
Vijay Kumar Kanta

На це потрібна нова відповідь, оскільки я сильно підозрюю, що включення регулярних виразів у C ++ 11 змінило найкращу відповідь.
всезнайко

113

Використовуйте 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 версію рядка. Ви також можете змінити список роздільників середнього розбору.

На мою думку, наведений вище код набагато простіший і простіший у використанні, ніж написання для нього окремого класу. Для мене це одна з тих функцій, яку надає мова, і вона виконує це добре і чисто. Це просто рішення на основі "С". Це підходить, це легко, і вам не доведеться писати багато зайвого коду :-)


42
Не те, що мені не подобається C, проте strtok не є безпечним для потоків, і ви повинні бути впевнені, що рядок, яку ви надсилаєте, містить нульовий символ, щоб уникнути можливого переповнення буфера.
голосувати

11
Існує strtok_r, але це було питання C ++.
Контракт професора Фолкена порушено

3
@tloach: у компіляторі MS C ++ strtok є безпечним для потоків, оскільки внутрішня статична змінна створюється на TLS (локальне зберігання потоку) (фактично це залежить від компілятора)
Ахмед Саїд

3
@ahmed: безпечний потік означає більше, ніж просто можливість двічі запускати функцію в різних потоках. У цьому випадку, якщо нитка модифікована під час запуску strtok, можливо, рядок буде дійсним протягом усього запуску strtok, але strtok все одно буде псуватися, оскільки рядок змінено, вона вже минула нульовим символом, і вона буде зберігайте пам'ять читання, поки вона або не отримає порушення безпеки або не знайде нульовий символ. Це проблема з оригінальними функціями рядка C, якщо ви не вказуєте довжину десь у вас виникають проблеми.
голосувати

4
strtok вимагає вказівника на масив char, що не закінчується null, що не є звичайною істотою, яку можна знайти в коді c ++ ... який ваш улюблений спосіб перетворити на це з std :: string?
fuzzyTew

105

Ще один швидкий спосіб - це використовувати getline. Щось на зразок:

stringstream ss("bla bla");
string s;

while (getline(ss, s, ' ')) {
 cout << s << endl;
}

Якщо ви хочете, ви можете зробити простий split()метод повернення a vector<string>, який справді корисний.


2
У мене виникли проблеми з використанням цієї методики з символами 0x0A в рядку, які змусили цикл тимчасового виходу передчасно. В іншому випадку це приємне просте і швидке рішення.
Райан Х.

4
Це добре, але просто майте на увазі, що при цьому розмежувач за замовчуванням '\ n' не враховується. Цей приклад спрацює, але якщо ви використовуєте щось на кшталт: while (getline (inFile, word, '')), де inFile є об'єктом ifstream, що містить кілька рядків, ви отримаєте забавні результати ..
hackrock

занадто погано getline повертає потік, а не рядок, що робить його непридатним у списках ініціалізації без тимчасового зберігання
fuzzyTew

1
Класно! Без підвищення та C ++ 11, гарне рішення для тих застарілих проектів там!
Декін

1
ЦЕ відповідь, назва функції просто трохи незручна.
Нілс

82

Ви можете використовувати потоки, ітератори та алгоритм копіювання, щоб зробити це досить безпосередньо.

#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);
}

17
Я вважаю, що std :: дратує читати .. чому б не використати "використання"?
користувач35978

80
@Vadi: адже редагування чужого допису досить нав'язливе. @pheze: Я вважаю за краще дозволити, щоб stdя знав, звідки мій об'єкт, це лише питання стилю.
Матьє М.

7
Я розумію вашу причину і думаю, що це насправді хороший вибір, якщо він працює для вас, але з педагогічної точки зору я фактично згоден з фезою. Простіше читати та розуміти цілком іноземний приклад на зразок цього з використанням "std простору імен" вгорі, оскільки для інтерпретації наступних рядків потрібно менше зусиль ... особливо в цьому випадку, оскільки все відбувається зі стандартної бібліотеки. Ви можете зробити його легким для читання і очевидним, звідки беруться об'єкти за допомогою серії "using std :: string;" Тим більше, що функція така коротка.
cheshirekow

61
Незважаючи на те, що префікси "std ::" є дратівливими або некрасивими, краще включити їх у приклад код, щоб було абсолютно зрозуміло, звідки ці функції беруться. Якщо вони вас турбують, тривіально замінити їх на "використання" після того, як ви вкрадете приклад і заявите про це як власний.
dlchambers

20
так! що він сказав! найкраща практика - використовувати префікс std. Будь-яка велика база коду, без сумніву, матиме власні бібліотеки та простори імен, і використання "std простору імен" призведе до головних болів, коли ви починаєте викликати конфлікти у просторі імен.
Miek

48

Не ображайтеся , хлопці, але для такого простого завдання, ви робите речі шлях дуже складний. Є багато причин використовувати 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] );
}

І так, ми могли б розділити () повернути новий вектор, а не передати його. Тривожно обертати і перевантажувати. Але залежно від того, що я роблю, мені часто краще повторно використовувати наявні об’єкти, а не завжди створювати нові. (Тільки до тих пір, поки я не забуду спорожнити вектор між ними!)

Довідка: http://www.cplusplus.com/reference/string/string/ .

(Я спочатку писав відповідь на запитання Дуга: Змінення рядків C ++ і витягування на основі роздільників (закрито) . Але оскільки Мартін Йорк закрив це питання вказівником сюди ... я просто узагальнив свій код.)


12
Навіщо визначати макрос, який ви використовуєте лише в одному місці. І як ваш UASSERT кращий за стандартне твердження. Розбиття порівняння на 3 лексеми, як це, не вимагає більше коми, ніж вам потрібно.
Крелбор

1
Може бути, макрос UASSERT показує (у повідомленні про помилку) фактичну залежність між (і значеннями) двох порівняних значень? Це насправді досить гарна ідея, ІМХО.
GhassanPL

10
Фу, чому std::stringклас не включає функцію split ()?
Містер Шикаданс

Я думаю, що останній рядок у циклі while повинен бути, start = ((end > (theString.size() - theDelimiter.size())) ? string::npos : end + theDelimiter.size());а цикл while повинен бути while (start != string::npos). Також я перевіряю підрядку, щоб переконатися, що вона не порожня, перш ніж вставляти її у вектор.
Джон К

@JohnK Якщо вхід має два послідовних роздільника, то чітко рядок між ними порожній, і його слід вставити у вектор. Якщо порожні значення не прийнятні для певної мети, це вже інша річ, але IMHO такі обмеження слід застосовувати поза цією функцією дуже загального призначення.
Lauri Nurmi

46

Рішення з використанням regex_token_iterators:

#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;
    }
}

5
Це має бути відповідь, що займає перше місце. Це правильний спосіб зробити це в C ++> = 11.
Всезначний

1
Я радий, що я прокрутився аж до цієї відповіді (наразі було лише 9 відгуків). Саме так повинен виглядати код C ++ 11 для цього завдання!
YePhIcK

Відмінна відповідь, що не покладається на зовнішні бібліотеки та використовує вже доступні бібліотеки
Андрій

1
Чудова відповідь, що дає найбільшу гнучкість у роздільниках. Кілька застережень: Використання \ s + regex уникає порожніх лексем у середині тексту, але дає порожній перший маркер, якщо текст починається з пробілу. Крім того, регулярний вигляд здається повільним: на моєму ноутбуці для 20 Мб випадкового тексту потрібно 0,6 секунди, порівняно з 0,014 сек для strtok, strsep або відповіді Пархама, використовуючи str.find_first_of, або 0,027 сек для Perl, або 0,021 сек для Python . Для короткого тексту швидкість може не турбувати.
Марк Гейтс

2
Добре, можливо, це виглядає круто, але це явно перевитрати регулярних виразів. Розумно, лише якщо ви не дбаєте про продуктивність.
Marek R

35

Boost має сильну функцію спліт: boost :: алгоритм :: 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"
""

26

Я знаю, що ви попросили рішення на C ++, але ви можете вважати це корисним:

Qt

#include <QString>

...

QString str = "The quick brown fox"; 
QStringList results = str.split(" "); 

Перевага перед Boost у цьому прикладі полягає в тому, що це пряме зіставлення коду вашої публікації.

Більше див. У документації Qt


22

Ось зразок класу токенізаторів, який може робити те, що ви хочете

//Header file
class Tokenizer 
{
    public:
        static const std::string DELIMITERS;
        Tokenizer(const std::string& str);
        Tokenizer(const std::string& str, const std::string& delimiters);
        bool NextToken();
        bool NextToken(const std::string& delimiters);
        const std::string GetToken() const;
        void Reset();
    protected:
        size_t m_offset;
        const std::string m_string;
        std::string m_token;
        std::string m_delimiters;
};

//CPP file
const std::string Tokenizer::DELIMITERS(" \t\n\r");

Tokenizer::Tokenizer(const std::string& s) :
    m_string(s), 
    m_offset(0), 
    m_delimiters(DELIMITERS) {}

Tokenizer::Tokenizer(const std::string& s, const std::string& delimiters) :
    m_string(s), 
    m_offset(0), 
    m_delimiters(delimiters) {}

bool Tokenizer::NextToken() 
{
    return NextToken(m_delimiters);
}

bool Tokenizer::NextToken(const std::string& delimiters) 
{
    size_t i = m_string.find_first_not_of(delimiters, m_offset);
    if (std::string::npos == i) 
    {
        m_offset = m_string.length();
        return false;
    }

    size_t j = m_string.find_first_of(delimiters, i);
    if (std::string::npos == j) 
    {
        m_token = m_string.substr(i);
        m_offset = m_string.length();
        return true;
    }

    m_token = m_string.substr(i, j - i);
    m_offset = j;
    return true;
}

Приклад:

std::vector <std::string> v;
Tokenizer s("split this string", " ");
while (s.NextToken())
{
    v.push_back(s.GetToken());
}

19

Це простий СТЛ-єдиним рішення (~ 5 рядків!) З використанням std::findта std::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);
    }
}

Спробуйте це наживо !


3
Це добре, але я думаю, що вам потрібно використовувати find_first_of (), а не find (), щоб правильно працювати з кількома роздільниками.

2
@ user755921 декілька роздільників пропускаються при знаходженні початкової позиції за допомогою find_first_not_of.
Початківцю

16

pystring - це невелика бібліотека, яка реалізує купу струнних функцій Python, включаючи метод розділення:

#include <string>
#include <vector>
#include "pystring.h"

std::vector<std::string> chunks;
pystring::split("this string", chunks);

// also can specify a separator
pystring::split("this-string", chunks, "-");

3
Нічого собі, ви відповіли на моє безпосереднє запитання і на багато майбутніх питань. Я розумію, що c ++ є потужним. Але, коли розбиття рядка призводить до вихідного коду, як наведені вище відповіді, це очевидно зневажливо. Мені б хотілося знати про інші подібні бібліотеки, які тягнуть зручності мов до вищого рівня.
Росс

вау, ви серйозно щойно зробили мій день !! не знав про пістринг. це допоможе мені заощадити багато часу!
акредитувати

11

Я розмістив цю відповідь на подібне запитання.
Не винаходити колесо. Я використав декілька бібліотек, і найшвидша і гнучка, з якою я зіткнулася, це: C ++ String Toolkit Library .

Ось приклад того, як його використовувати, який я розмістив ще десь у stackoverflow.

#include <iostream>
#include <vector>
#include <string>
#include <strtk.hpp>

const char *whitespace  = " \t\r\n\f";
const char *whitespace_and_punctuation  = " \t\r\n\f;,=";

int main()
{
    {   // normal parsing of a string into a vector of strings
       std::string s("Somewhere down the road");
       std::vector<std::string> result;
       if( strtk::parse( s, whitespace, result ) )
       {
           for(size_t i = 0; i < result.size(); ++i )
            std::cout << result[i] << std::endl;
       }
    }

    {  // parsing a string into a vector of floats with other separators
       // besides spaces

       std::string s("3.0, 3.14; 4.0");
       std::vector<float> values;
       if( strtk::parse( s, whitespace_and_punctuation, values ) )
       {
           for(size_t i = 0; i < values.size(); ++i )
            std::cout << values[i] << std::endl;
       }
    }

    {  // parsing a string into specific variables

       std::string s("angle = 45; radius = 9.9");
       std::string w1, w2;
       float v1, v2;
       if( strtk::parse( s, whitespace_and_punctuation, w1, v1, w2, v2) )
       {
           std::cout << "word " << w1 << ", value " << v1 << std::endl;
           std::cout << "word " << w2 << ", value " << v2 << std::endl;
       }
    }

    return 0;
}

8

Перевірте цей приклад. Це може допомогти вам ..

#include <iostream>
#include <sstream>

using namespace std;

int main ()
{
    string tmps;
    istringstream is ("the dellimiter is the space");
    while (is.good ()) {
        is >> tmps;
        cout << tmps << "\n";
    }
    return 0;
}

1
Я б зробивwhile ( is >> tmps ) { std::cout << tmps << "\n"; }
jordix

6

MFC / ATL має дуже приємний токенізатор. Від MSDN:

CAtlString str( "%First Second#Third" );
CAtlString resToken;
int curPos= 0;

resToken= str.Tokenize("% #",curPos);
while (resToken != "")
{
   printf("Resulting token: %s\n", resToken);
   resToken= str.Tokenize("% #",curPos);
};

Output

Resulting Token: First
Resulting Token: Second
Resulting Token: Third

1
Ця функція Tokenize () буде пропускати порожні маркери, наприклад, якщо в основному рядку є підряд "%%", порожній маркер не повертається. Він пропускається.
Шин

4

Якщо ви готові використовувати C, ви можете використовувати функцію strtok . Слід звернути увагу на проблеми з багатопотоковою передачею під час її використання.


3
Зауважте, що strtok змінює рядок, який ви перевіряєте, тому ви не можете використовувати його на строках const char *, не роблячи копії.
Graeme Perrow

9
Проблема багатопотокових запитів полягає в тому, що strtok використовує глобальну змінну для того, щоб відслідковувати, де вона знаходиться, тому якщо у вас є два потоки, які використовують кожен strtok, ви отримаєте невизначене поведінку.
JohnMcG

@JohnMcG Або просто використовувати, strtok_sяке в основному strtokз явним проходженням стану.
Маттіас

4

Для простих речей я просто використовую наступне:

unsigned TokenizeString(const std::string& i_source,
                        const std::string& i_seperators,
                        bool i_discard_empty_tokens,
                        std::vector<std::string>& o_tokens)
{
    unsigned prev_pos = 0;
    unsigned pos = 0;
    unsigned number_of_tokens = 0;
    o_tokens.clear();
    pos = i_source.find_first_of(i_seperators, pos);
    while (pos != std::string::npos)
    {
        std::string token = i_source.substr(prev_pos, pos - prev_pos);
        if (!i_discard_empty_tokens || token != "")
        {
            o_tokens.push_back(i_source.substr(prev_pos, pos - prev_pos));
            number_of_tokens++;
        }

        pos++;
        prev_pos = pos;
        pos = i_source.find_first_of(i_seperators, pos);
    }

    if (prev_pos < i_source.length())
    {
        o_tokens.push_back(i_source.substr(prev_pos));
        number_of_tokens++;
    }

    return number_of_tokens;
}

Відмови від страху: я пишу програмне забезпечення для обробки даних у режимі реального часу, куди дані надходять через бінарні файли, сокети або якісь дзвінки API (карти вводу / виводу, камери). Я ніколи не використовую цю функцію для чогось більш складного або критичного для часу, ніж читання зовнішніх файлів конфігурації при запуску.


4

Ви можете просто використовувати бібліотеку регулярних виразів і вирішити це, використовуючи регулярні вирази.

Використовуйте вираз (\ w +) та змінну в \ 1 (або $ 1 залежно від реалізації бібліотекою регулярних виразів).


+1 за пропозицію регулярного вираження, якщо вам не потрібна швидкість деформації, це найскладніше рішення, яке ще не підтримується скрізь, але з часом це стане менш важливим.
odinthenerd

+1 від мене, щойно спробував <regex> в c ++ 11. Настільки просте та елегантне
StahlRat

4

Тут багато надмірно складних пропозицій. Спробуйте це просте рішення std :: string:

using namespace std;

string someText = ...

string::size_type tokenOff = 0, sepOff = tokenOff;
while (sepOff != string::npos)
{
    sepOff = someText.find(' ', sepOff);
    string::size_type tokenLen = (sepOff == string::npos) ? sepOff : sepOff++ - tokenOff;
    string token = someText.substr(tokenOff, tokenLen);
    if (!token.empty())
        /* do something with token */;
    tokenOff = sepOff;
}

4

Я думав, що для цього >>потрібен оператор у потокових потоках:

string word; sin >> word;

1
Моя провина в тому, що я дав поганий (занадто простий) приклад. Наскільки я знаю, це працює лише тоді, коли вашим роздільником є ​​пробіл.
Білл Ящірка

4

Відповідь Адама Пірса надає рукодільний токенізатор, який приймає a const char*. З ітераторами це дещо проблематичніше, тому що збільшення stringітератора кінця кінця не визначене . Це сказало, враховуючи, що string str{ "The quick brown fox" }ми, безумовно, можемо досягти цього:

auto start = find(cbegin(str), cend(str), ' ');
vector<string> tokens{ string(cbegin(str), start) };

while (start != cend(str)) {
    const auto finish = find(++start, cend(str), ' ');

    tokens.push_back(string(start, finish));
    start = finish;
}

Live Example


Якщо ви шукаєте абстрактну складність, використовуючи стандартну функціональність, як пропонує On Freund strtok , це простий варіант:

vector<string> tokens;

for (auto i = strtok(data(str), " "); i != nullptr; i = strtok(nullptr, " ")) tokens.push_back(i);

Якщо у вас немає доступу до C ++ 17, вам потрібно буде замінити, data(str)як у цьому прикладі: http://ideone.com/8kAGoa

Хоча це не продемонстровано в прикладі, strtokне потрібно використовувати однаковий роздільник для кожного маркера. Поряд з цією перевагою, є й кілька недоліків:

  1. strtokне можна використовувати одночасно декілька strings: або nullptrпотрібно пройти для продовження токенізації потоку, stringабо нову char*токенізацію потрібно пропустити (є деякі нестандартні реалізації, які підтримують це, однак, наприклад:strtok_s )
  2. З тієї ж причини strtokне можна використовувати одночасно на декількох потоках (однак це може бути визначено реалізацією, наприклад: Реалізація Visual Studio безпечна для потоків )
  3. Виклик strtokмодифікує операцію, над якою stringвона працює, тому її не можна використовувати на const strings, const char*s або в прямому рядку, щоб токенізувати будь-яке з них strtokабо оперувати stringвмістом, який має бути збережений, strпотрібно було б скопіювати, тоді копія могла б працювати на

надає нам split_viewтокенізацію рядків неруйнівним чином: https://topanswers.xyz/cplusplus?q=749#a874


Попередні методи не можуть генерувати токенізовані vectorмісця, тобто без абстрагування їх функцією помічника, яку вони не можуть ініціалізувати const vector<string> tokens. Ця функціональність і можливість приймати будь -який роздільник пробілів можна використовувати за допомогою istream_iterator. Для прикладу: const string str{ "The quick \tbrown \nfox" }ми можемо це зробити:

istringstream is{ str };
const vector<string> tokens{ istream_iterator<string>(is), istream_iterator<string>() };

Live Example

Потрібна конструкція istringstreamдля цього варіанту має набагато більшу вартість, ніж попередні два варіанти, однак ці витрати зазвичай приховуються за рахунок stringрозподілу.


Якщо жоден з перерахованих вище варіантів не є досить гнучким для ваших потреб в токенізації, найбільш гнучким варіантом є використання, regex_token_iteratorзвичайно, такої гнучкості приводить до великих витрат, але знову ж таки, це, ймовірно, приховано у stringвартості розподілу. Скажімо, наприклад, що ми хочемо токенізувати на основі не втечених коми, також поїдаючи пробіл, враховуючи наступний вклад: const string str{ "The ,qu\\,ick ,\tbrown, fox" }ми можемо це зробити:

const regex re{ "\\s*((?:[^\\\\,]|\\\\.)*?)\\s*(?:,|$)" };
const vector<string> tokens{ sregex_token_iterator(cbegin(str), cend(str), re, 1), sregex_token_iterator() };

Live Example


strtok_sце, до речі, стандарт С11. strtok_rє стандартом POSIX2001. Між обома з них існує стандартна версія strtokдля більшості платформ.
Андон М. Коулман

@ AndonM.Coleman Але це питання c ++ , а в C ++ #include <cstring>входить лише версія c99strtok . Тож моє припущення полягає в тому, що ви просто надаєте цей коментар як допоміжний матеріал, демонструючи доступність strtokрозширень щодо конкретної реалізації ?
Джонатан Мі

1
Просто, що це не так нестандартно, як люди можуть інакше вірити. strtok_sнадається як C11, так і як окреме розширення в процесі виконання Microsoft C. Тут є цікава історія, де _sфункції Microsoft стали стандартом C.
Андон М. Коулман

@ AndonM.Coleman Правильно, я з тобою. Очевидно, що якщо це в стандарті C11, інтерфейс та реалізація мають обмеження, які вимагають однакової поведінки незалежно від платформи. Тепер єдиною проблемою є забезпечення функцій C11 доступними для нас на всіх платформах. Сподіваємось, стандарт C11 буде чимось, що C ++ 17 або C ++ 20 обирає для підбору.
Джонатан Мі

3

Я знаю, що на це питання вже відповіли, але я хочу зробити свій внесок. Можливо, моє рішення трохи просте, але ось що я придумав:

vector<string> get_words(string const& text, string const& separator)
{
    vector<string> result;
    string tmp = text;

    size_t first_pos = 0;
    size_t second_pos = tmp.find(separator);

    while (second_pos != string::npos)
    {
        if (first_pos != second_pos)
        {
            string word = tmp.substr(first_pos, second_pos - first_pos);
            result.push_back(word);
        }
        tmp = tmp.substr(second_pos + separator.length());
        second_pos = tmp.find(separator);
    }

    result.push_back(tmp);

    return result;
}

Будь ласка, прокоментуйте, чи є кращий підхід до чогось у моєму коді чи якщо щось не так.

ОНОВЛЕННЯ: додано загальний роздільник


Використовували ваше рішення з натовпу :) Чи можу я змінити ваш код, щоб додати будь-який роздільник?
Зак

1
@Zac радий, що тобі сподобалось, і ти можеш змінити його ... просто додай у мій відповідь жирний розділ оновлення ...
NutCracker

2

Ось підхід, який дозволяє контролювати, чи включені порожні маркери (як strsep) чи виключені (як strtok).

#include <string.h> // for strchr and strlen

/*
 * want_empty_tokens==true  : include empty tokens, like strsep()
 * want_empty_tokens==false : exclude empty tokens, like strtok()
 */
std::vector<std::string> tokenize(const char* src,
                                  char delim,
                                  bool want_empty_tokens)
{
  std::vector<std::string> tokens;

  if (src and *src != '\0') // defensive
    while( true )  {
      const char* d = strchr(src, delim);
      size_t len = (d)? d-src : strlen(src);

      if (len or want_empty_tokens)
        tokens.push_back( std::string(src, len) ); // capture token

      if (d) src += len+1; else break;
    }

  return tokens;
}

2

Мені здається дивним, що з усіма нам свідомими швидкості тут, на SO, ніхто не представив версію, яка використовує створену часом компіляції таблицю пошуку для роздільника (приклад реалізації далі вниз). Використовуючи таблицю пошуку та ітератори, слід перемагати std :: regex за ефективністю, якщо вам не потрібно бити регулярний гекс, просто використовуйте його, його стандарт на C ++ 11 і супер гнучкий.

Деякі з них вже запропонували регулярний вираз, але для нообів ось приклад, який повинен робити саме те, що очікує ОП:

std::vector<std::string> split(std::string::const_iterator it, std::string::const_iterator end, std::regex e = std::regex{"\\w+"}){
    std::smatch m{};
    std::vector<std::string> ret{};
    while (std::regex_search (it,end,m,e)) {
        ret.emplace_back(m.str());              
        std::advance(it, m.position() + m.length()); //next start position = match position + match length
    }
    return ret;
}
std::vector<std::string> split(const std::string &s, std::regex e = std::regex{"\\w+"}){  //comfort version calls flexible version
    return split(s.cbegin(), s.cend(), std::move(e));
}
int main ()
{
    std::string str {"Some people, excluding those present, have been compile time constants - since puberty."};
    auto v = split(str);
    for(const auto&s:v){
        std::cout << s << std::endl;
    }
    std::cout << "crazy version:" << std::endl;
    v = split(str, std::regex{"[^e]+"});  //using e as delim shows flexibility
    for(const auto&s:v){
        std::cout << s << std::endl;
    }
    return 0;
}

Якщо нам потрібно бути швидшими і приймати обмеження, що всі символи повинні бути 8 біт, ми можемо скласти таблицю пошуку під час компіляції за допомогою метапрограмування:

template<bool...> struct BoolSequence{};        //just here to hold bools
template<char...> struct CharSequence{};        //just here to hold chars
template<typename T, char C> struct Contains;   //generic
template<char First, char... Cs, char Match>    //not first specialization
struct Contains<CharSequence<First, Cs...>,Match> :
    Contains<CharSequence<Cs...>, Match>{};     //strip first and increase index
template<char First, char... Cs>                //is first specialization
struct Contains<CharSequence<First, Cs...>,First>: std::true_type {}; 
template<char Match>                            //not found specialization
struct Contains<CharSequence<>,Match>: std::false_type{};

template<int I, typename T, typename U> 
struct MakeSequence;                            //generic
template<int I, bool... Bs, typename U> 
struct MakeSequence<I,BoolSequence<Bs...>, U>:  //not last
    MakeSequence<I-1, BoolSequence<Contains<U,I-1>::value,Bs...>, U>{};
template<bool... Bs, typename U> 
struct MakeSequence<0,BoolSequence<Bs...>,U>{   //last  
    using Type = BoolSequence<Bs...>;
};
template<typename T> struct BoolASCIITable;
template<bool... Bs> struct BoolASCIITable<BoolSequence<Bs...>>{
    /* could be made constexpr but not yet supported by MSVC */
    static bool isDelim(const char c){
        static const bool table[256] = {Bs...};
        return table[static_cast<int>(c)];
    }   
};
using Delims = CharSequence<'.',',',' ',':','\n'>;  //list your custom delimiters here
using Table = BoolASCIITable<typename MakeSequence<256,BoolSequence<>,Delims>::Type>;

З цим на місці зробити getNextTokenфункцію легко:

template<typename T_It>
std::pair<T_It,T_It> getNextToken(T_It begin,T_It end){
    begin = std::find_if(begin,end,std::not1(Table{})); //find first non delim or end
    auto second = std::find_if(begin,end,Table{});      //find first delim or end
    return std::make_pair(begin,second);
}

Використовувати його також легко:

int main() {
    std::string s{"Some people, excluding those present, have been compile time constants - since puberty."};
    auto it = std::begin(s);
    auto end = std::end(s);
    while(it != std::end(s)){
        auto token = getNextToken(it,end);
        std::cout << std::string(token.first,token.second) << std::endl;
        it = token.second;
    }
    return 0;
}

Ось живий приклад: http://ideone.com/GKtkLQ


1
Чи можливо токенізувати роздільником рядка?
Галігатор

ця версія оптимізована лише для розділових знаків з одним символом, використовуючи таблицю пошуку, не підходить для багатознакових (рядкових) роздільників, тому складніше перемогти регулярний вираз по ефективності.
odinthenerd

1

ви можете скористатися boost :: make_find_iterator. Щось подібне до цього:

template<typename CH>
inline vector< basic_string<CH> > tokenize(
    const basic_string<CH> &Input,
    const basic_string<CH> &Delimiter,
    bool remove_empty_token
    ) {

    typedef typename basic_string<CH>::const_iterator string_iterator_t;
    typedef boost::find_iterator< string_iterator_t > string_find_iterator_t;

    vector< basic_string<CH> > Result;
    string_iterator_t it = Input.begin();
    string_iterator_t it_end = Input.end();
    for(string_find_iterator_t i = boost::make_find_iterator(Input, boost::first_finder(Delimiter, boost::is_equal()));
        i != string_find_iterator_t();
        ++i) {
        if(remove_empty_token){
            if(it != i->begin())
                Result.push_back(basic_string<CH>(it,i->begin()));
        }
        else
            Result.push_back(basic_string<CH>(it,i->begin()));
        it = i->end();
    }
    if(it != it_end)
        Result.push_back(basic_string<CH>(it,it_end));

    return Result;
}

1

Ось мій швейцарський армійський нож шнурових токенізаторів для розбиття рядків на пробіл, облік одиночних і подвійних цитат, обвитих рядків, а також вилучення цих символів з результатів. Я використовував RegexBuddy 4.x для генерування більшості фрагменту коду, але я додав власну обробку для зняття котирувань та декілька інших речей.

#include <string>
#include <locale>
#include <regex>

std::vector<std::wstring> tokenize_string(std::wstring string_to_tokenize) {
    std::vector<std::wstring> tokens;

    std::wregex re(LR"(("[^"]*"|'[^']*'|[^"' ]+))", std::regex_constants::collate);

    std::wsregex_iterator next( string_to_tokenize.begin(),
                                string_to_tokenize.end(),
                                re,
                                std::regex_constants::match_not_null );

    std::wsregex_iterator end;
    const wchar_t single_quote = L'\'';
    const wchar_t double_quote = L'\"';
    while ( next != end ) {
        std::wsmatch match = *next;
        const std::wstring token = match.str( 0 );
        next++;

        if (token.length() > 2 && (token.front() == double_quote || token.front() == single_quote))
            tokens.emplace_back( std::wstring(token.begin()+1, token.begin()+token.length()-1) );
        else
            tokens.emplace_back(token);
    }
    return tokens;
}

1
(Вниз) голоси можуть бути такими ж конструктивними, як і відгуки, але не тоді, коли ви не залишаєте коментарів щодо того, чому ...
kayleeFrye_onDeck

1
Я зрівнявся з вами, але це може бути тому, що код виглядає програмістом гуглінгу "як розділити рядок", особливо без документації
матцху

Дякую @mattshu! Це сегменти регулярних виразів роблять це жахливим чи щось інше?
kayleeFrye_onDeck

0

Якщо відома максимальна довжина вхідного рядка, що підлягає токенізації, можна скористатися цим і реалізувати дуже швидку версію. Я накидаю основну ідею нижче, яка була натхнена як strtok (), так і структура "суфіксний масив" -даних, описана "Програмуванням Перлів" другого видання Джона Бентлі, глава 15. Клас C ++ в цьому випадку дає лише певну організацію та зручність використання. Показана реалізація може бути легко розширена для видалення провідних та кінцевих символів пробілів у маркерах.

В основному можна замінити символи роздільника символами, що закінчуються рядками '\ 0'-символами, і встановити покажчики на лексеми, змінюючи змінений рядок. У крайньому випадку, коли рядок складається лише з роздільників, отримується довжина рядка плюс 1 в результаті порожні лексеми. Практично дублювати рядок, що підлягає модифікації.

Файл заголовка:

class TextLineSplitter
{
public:

    TextLineSplitter( const size_t max_line_len );

    ~TextLineSplitter();

    void            SplitLine( const char *line,
                               const char sep_char = ',',
                             );

    inline size_t   NumTokens( void ) const
    {
        return mNumTokens;
    }

    const char *    GetToken( const size_t token_idx ) const
    {
        assert( token_idx < mNumTokens );
        return mTokens[ token_idx ];
    }

private:
    const size_t    mStorageSize;

    char           *mBuff;
    char          **mTokens;
    size_t          mNumTokens;

    inline void     ResetContent( void )
    {
        memset( mBuff, 0, mStorageSize );
        // mark all items as empty:
        memset( mTokens, 0, mStorageSize * sizeof( char* ) );
        // reset counter for found items:
        mNumTokens = 0L;
    }
};

Файл реалізації:

TextLineSplitter::TextLineSplitter( const size_t max_line_len ):
    mStorageSize ( max_line_len + 1L )
{
    // allocate memory
    mBuff   = new char  [ mStorageSize ];
    mTokens = new char* [ mStorageSize ];

    ResetContent();
}

TextLineSplitter::~TextLineSplitter()
{
    delete [] mBuff;
    delete [] mTokens;
}


void TextLineSplitter::SplitLine( const char *line,
                                  const char sep_char   /* = ',' */,
                                )
{
    assert( sep_char != '\0' );

    ResetContent();
    strncpy( mBuff, line, mMaxLineLen );

    size_t idx       = 0L; // running index for characters

    do
    {
        assert( idx < mStorageSize );

        const char chr = line[ idx ]; // retrieve current character

        if( mTokens[ mNumTokens ] == NULL )
        {
            mTokens[ mNumTokens ] = &mBuff[ idx ];
        } // if

        if( chr == sep_char || chr == '\0' )
        { // item or line finished
            // overwrite separator with a 0-terminating character:
            mBuff[ idx ] = '\0';
            // count-up items:
            mNumTokens ++;
        } // if

    } while( line[ idx++ ] );
}

Сценарієм використання буде:

// create an instance capable of splitting strings up to 1000 chars long:
TextLineSplitter spl( 1000 );
spl.SplitLine( "Item1,,Item2,Item3" );
for( size_t i = 0; i < spl.NumTokens(); i++ )
{
    printf( "%s\n", spl.GetToken( i ) );
}

вихід:

Item1

Item2
Item3

0

boost::tokenizerє вашим другом, але подумайте про те, щоб зробити свій код портативним з посиланням на проблеми інтернаціоналізації (i18n), використовуючи wstring/ wchar_tзамість спадщини string/ charтипів.

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

using namespace std;
using namespace boost;

typedef tokenizer<char_separator<wchar_t>,
                  wstring::const_iterator, wstring> Tok;

int main()
{
  wstring s;
  while (getline(wcin, s)) {
    char_separator<wchar_t> sep(L" "); // list of separator characters
    Tok tok(s, sep);
    for (Tok::iterator beg = tok.begin(); beg != tok.end(); ++beg) {
      wcout << *beg << L"\t"; // output (or store in vector)
    }
    wcout << L"\n";
  }
  return 0;
}

"Спадщина", безумовно, не є правильною і wchar_tє жахливим залежним від впровадження типом, яким ніхто не повинен користуватися, якщо це абсолютно не потрібно.
CoffeeandCode

Використання wchar_t якимось чином автоматично не вирішує жодних проблем із i18n. Ви використовуєте кодування для вирішення цієї проблеми. Якщо ви розділяєте рядок за роздільником, мається на увазі, що роздільник не стикається з кодованим вмістом будь-якого маркера всередині рядка. Утеча може знадобитися тощо. Wchar_t не є магічним рішенням цього.
йоніл

0

Простий код C ++ (стандартний C ++ 98), приймає кілька роздільників (вказано в std :: string), використовує лише вектори, рядки та ітератори.

#include <iostream>
#include <vector>
#include <string>
#include <stdexcept> 

std::vector<std::string> 
split(const std::string& str, const std::string& delim){
    std::vector<std::string> result;
    if (str.empty())
        throw std::runtime_error("Can not tokenize an empty string!");
    std::string::const_iterator begin, str_it;
    begin = str_it = str.begin(); 
    do {
        while (delim.find(*str_it) == std::string::npos && str_it != str.end())
            str_it++; // find the position of the first delimiter in str
        std::string token = std::string(begin, str_it); // grab the token
        if (!token.empty()) // empty token only when str starts with a delimiter
            result.push_back(token); // push the token into a vector<string>
        while (delim.find(*str_it) != std::string::npos && str_it != str.end())
            str_it++; // ignore the additional consecutive delimiters
        begin = str_it; // process the remaining tokens
        } while (str_it != str.end());
    return result;
}

int main() {
    std::string test_string = ".this is.a.../.simple;;test;;;END";
    std::string delim = "; ./"; // string containing the delimiters
    std::vector<std::string> tokens = split(test_string, delim);           
    for (std::vector<std::string>::const_iterator it = tokens.begin(); 
        it != tokens.end(); it++)
            std::cout << *it << std::endl;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.