Как токенизировать (слова), классифицируя пунктуацию как пробел - PullRequest
5 голосов
/ 27 мая 2011

На основании этого вопроса, который был закрыт довольно быстро:
Попытка создать программу для чтения ввода пользователя, а затем разбить массив на отдельные слова. Все мои указатели действительны?

Вместо закрытия, я думаю, что некоторая дополнительная работа могла бы помочь ФП прояснить вопрос.

Вопрос:

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

В C я использовал бы strtok(), чтобы разбить массив на токены, а затем вручную создать массив.
Как это:

Основная функция:

char **findwords(char *str);

int main()
{
    int     test;
    char    words[100]; //an array of chars to hold the string given by the user
    char    **word;  //pointer to a list of words
    int     index = 0; //index of the current word we are printing
    char    c;

    cout << "die monster !";
    //a loop to place the charecters that the user put in into the array  

    do
    {
        c = getchar();
        words[index] = c;
    }
    while (words[index] != '\n');

    word = findwords(words);

    while (word[index] != 0) //loop through the list of words until the end of the list
    {
        printf("%s\n", word[index]); // while the words are going through the list print them out
        index ++; //move on to the next word
    }

    //free it from the list since it was dynamically allocated
    free(word);
    cin >> test;

    return 0;
}

Линейный токенизатор:

char **findwords(char *str)
{
    int     size = 20; //original size of the list 
    char    *newword; //pointer to the new word from strok
    int     index = 0; //our current location in words
    char    **words = (char **)malloc(sizeof(char *) * (size +1)); //this is the actual list of words

    /* Get the initial word, and pass in the original string we want strtok() *
     *   to work on. Here, we are seperating words based on spaces, commas,   *
     *   periods, and dashes. IE, if they are found, a new word is created.   */

    newword = strtok(str, " ,.-");

    while (newword != 0) //create a loop that goes through the string until it gets to the end
    {
        if (index == size)
        {
            //if the string is larger than the array increase the maximum size of the array
            size += 10;
            //resize the array
            char **words = (char **)malloc(sizeof(char *) * (size +1));
        }
        //asign words to its proper value
        words[index] = newword;
        //get the next word in the string
        newword = strtok(0, " ,.-");
        //increment the index to get to the next word
        ++index;
    }
    words[index] = 0;

    return words;
}

Будем благодарны за любые комментарии по поводу кода выше.
Но, кроме того, что является лучшим методом для достижения этой цели в C ++?

Ответы [ 2 ]

6 голосов
/ 27 мая 2011

Посмотрите на boost tokenizer для чего-то, что намного лучше в контексте C ++, чем strtok().

5 голосов
/ 27 мая 2011

Уже задано много вопросов о том, как токенизировать поток в C ++.
Пример: Как прочитать файл и получить слова на C ++

Но что сложнее найти, так это получить ту же функциональность, что и strtok ():

В основном strtok () позволяет разделить строку на целую группу пользовательских символов, в то время как поток C ++ позволяет использовать только white space в качестве разделителя. К счастью, определение white space определяется локалью, поэтому мы можем изменить локаль, чтобы другие символы воспринимались как пробел, и это позволит нам токенизировать поток более естественным образом.

#include <locale>
#include <string>
#include <sstream>
#include <iostream>

// This is my facet that will treat the ,.- as space characters and thus ignore them.
class WordSplitterFacet: public std::ctype<char>
{
    public:
        typedef std::ctype<char>    base;
        typedef base::char_type     char_type;

        WordSplitterFacet(std::locale const& l)
            : base(table)
        {
            std::ctype<char> const&  defaultCType  = std::use_facet<std::ctype<char> >(l);

            // Copy the default value from the provided locale
            static  char data[256];
            for(int loop = 0;loop < 256;++loop) { data[loop] = loop;}
            defaultCType.is(data, data+256, table);

            // Modifications to default to include extra space types.
            table[',']  |= base::space;
            table['.']  |= base::space;
            table['-']  |= base::space;
        }
    private:
        base::mask  table[256];
};

Затем мы можем использовать этот аспект в локальном, как это:

    std::ctype<char>*   wordSplitter(new WordSplitterFacet(std::locale()));

    <stream>.imbue(std::locale(std::locale(), wordSplitter));

Следующая часть вашего вопроса - как мне хранить эти слова в массиве? Ну, в C ++ вы бы не стали. Вы бы делегировали эту функциональность std :: vector / std :: string. Прочитав ваш код, вы увидите, что ваш код выполняет две основные функции в одной и той же части кода.

  • Управляет памятью.
  • Это токенизация данных.

Существует базовый принцип Separation of Concerns, когда ваш код должен только попытаться сделать одну из двух вещей. Он должен либо осуществлять управление ресурсами (в данном случае управление памятью), либо бизнес-логику (токенизацию данных). Разделяя их на разные части кода, вы делаете код более простым в использовании и написании. К счастью, в этом примере все управление ресурсами уже осуществляется std :: vector / std :: string, что позволяет нам сосредоточиться на бизнес-логике.

Как уже много раз было показано, простой способ токенизации потока - использование оператора >> и строки. Это разобьет поток на слова. Затем вы можете использовать итераторы для автоматического циклического прохождения потока по токенизации потока.

std::vector<std::string>  data;
for(std::istream_iterator<std::string> loop(<stream>); loop != std::istream_iterator<std::string>(); ++loop)
{
    // In here loop is an iterator that has tokenized the stream using the
    // operator >> (which for std::string reads one space separated word.

    data.push_back(*loop);
}

Если мы объединяем это с некоторыми стандартными алгоритмами для упрощения кода.

std::copy(std::istream_iterator<std::string>(<stream>), std::istream_iterator<std::string>(), std::back_inserter(data));

Теперь объединяем все вышеперечисленное в одно приложение

int main()
{
    // Create the facet.
    std::ctype<char>*   wordSplitter(new WordSplitterFacet(std::locale()));

    // Here I am using a string stream.
    // But any stream can be used. Note you must imbue a stream before it is used.
    // Otherwise the imbue() will silently fail.
    std::stringstream   teststr;
    teststr.imbue(std::locale(std::locale(), wordSplitter));

    // Now that it is imbued we can use it.
    // If this was a file stream then you could open it here.
    teststr << "This, stri,plop";

    cout << "die monster !";
    std::vector<std::string>    data;
    std::copy(std::istream_iterator<std::string>(teststr), std::istream_iterator<std::string>(), std::back_inserter(data));

    // Copy the array to cout one word per line
    std::copy(data.begin(), data.end(), std::ostream_iterator<std::string>(std::cout, "\n"));
}
...