Подтвердить что ты не робот

Простой синтаксический анализ строк с С++

Я использую С++ уже довольно давно, но, тем не менее, я склонен возвращаться к scanf, когда мне приходится разбирать простые текстовые файлы. Например, учитывая такую ​​конфигурацию (также предполагая, что порядок полей может меняться):

foo: [3 4 5]
baz: 3.0

Я бы написал что-то вроде:

char line[SOME_SIZE];
while (fgets(line, SOME_SIZE, file)) {
    int x, y, z;
    if (3 == sscanf(line, "foo: [%d %d %d]", &x, &y, &z)) {
        continue;
    }
    float w;
    if (1 == sscanf(line, "baz: %f", &w)) {
        continue;
    }
}

Какой самый сжатый способ достичь этого в С++? Всякий раз, когда я пытаюсь, я получаю много кода леса.

4b9b3361

Ответ 1

Это попытка использовать только стандартный С++.

В большинстве случаев я использую комбинацию std:: istringstream и std:: getline (которая может работать для разделения слов), чтобы получить то, что я хочу. И если я могу сделать мои файлы конфигурации похожими:

Foo = 1,2,3,4

что облегчает процесс.

текстовый файл выглядит так:

foo=1,2,3,4
bar=0

И вы разобрали его так:

int main()
{
    std::ifstream file( "sample.txt" );

    std::string line;
    while( std::getline( file, line ) )   
    {
        std::istringstream iss( line );

        std::string result;
        if( std::getline( iss, result , '=') )
        {
            if( result == "foo" )
            {
                std::string token;
                while( std::getline( iss, token, ',' ) )
                {
                    std::cout << token << std::endl;
                }
            }
            if( result == "bar" )
            {
               //...
    }
}

Ответ 2

С++ String Toolkit Library (StrTk) имеет следующее решение вашей проблемы:

#include <string>
#include <deque>
#include "strtk.hpp"

int main()
{
   std::string file_name = "simple.txt";
   strtk::for_each_line(file_name,
                       [](const std::string& line)
                       {
                          std::deque<std::string> token_list;
                          strtk::parse(line,"[]: ",token_list);
                          if (token_list.empty()) return;

                          const std::string& key = token_list[0];

                          if (key == "foo")
                          {
                            //do 'foo' related thing with token_list[1] 
                            //and token_list[2]
                            return;
                          }

                          if (key == "bar")
                          {
                            //do 'bar' related thing with token_list[1]
                            return;
                          }

                       });

   return 0;
}

Дополнительные примеры можно найти Здесь

Ответ 3

Boost.Spirit не зарезервирован для анализа сложной структуры. Это также хорошо подходит для микро-парсинга и почти соответствует компактности фрагмента С++ scanf:

#include <boost/spirit/include/qi.hpp>
#include <string>
#include <sstream>

using namespace boost::spirit::qi;


int main()
{
   std::string text = "foo: [3 4 5]\nbaz: 3.0";
   std::istringstream iss(text);

   std::string line;
   while (std::getline(iss, line))
   {
      int x, y, z;
      if(phrase_parse(line.begin(), line.end(), "foo: [">> int_ >> int_ >> int_ >> "]", space, x, y, z))
         continue;
      float w;
      if(phrase_parse(line.begin(), line.end(), "baz: ">> float_, space , w))
         continue;
   }
}

(Почему они не добавили "контейнерную" версию вне меня, было бы гораздо удобнее, если бы мы могли просто написать:

if(phrase_parse(line, "foo: [">> int_ >> int_ >> int_ >> "]", space, x, y, z))
   continue;

Но это правда, что:

  • Это добавляет много времени на сборку.
  • Сообщения об ошибках являются грубыми. Если вы делаете небольшую ошибку с помощью scanf, вы просто запускаете свою программу и сразу получаете segfault или абсурдное анализируемое значение. Сделайте небольшую ошибку с духом, и вы получите безнадежные гигантские сообщения об ошибках от компилятора, и для того, чтобы понять их, требуется много практики с boost.spirit.

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

Ответ 4

Регулярные выражения часто могут использоваться для разбора строк. Используйте capture groups (круглые скобки), чтобы обрабатывать различные части строки.

Например, чтобы проанализировать выражение типа foo: [3 4 56], используйте регулярное выражение (.*): \[(\d+) (\d+) (\d+)\]. Первая группа захвата будет содержать "foo", вторая, третья и четвертая будут содержать числа 3, 4 и 56.

Если существует несколько возможных строковых форматов, которые необходимо проанализировать, например, в примере, заданном OP, либо один за другим применяйте отдельные регулярные выражения, и посмотрите, какой из них соответствует, или напишите регулярное выражение, соответствующее всем возможным вариантам, обычно используя оператор | (set union).

Регулярные выражения очень гибкие, поэтому выражение может быть расширено, чтобы позволить больше вариантов, например, произвольное количество пробелов и пробелов после : в примере. Или разрешить только номера содержать определенное количество цифр.

В качестве дополнительного бонуса регулярные выражения обеспечивают неявное подтверждение, поскольку они требуют идеального соответствия. Например, если номер 56 в приведенном выше примере был заменен на 56x, совпадение завершится неудачно. Это также может упростить код, так как в приведенном выше примере группы, содержащие числа, можно безопасно отбрасывать в целые числа без дополнительной проверки, необходимой после успешного совпадения.

Регулярные выражения обычно работают с хорошей производительностью и есть много хороших библиотек для выбора. Например, Boost.Regex.

Ответ 5

Я думаю, Boost.Spirit - хороший способ описать грамматику прямо в коде С++. Требуется некоторое время, чтобы привыкнуть к Boost.Spirit, но после его довольно легко использовать. Это может быть не так лаконично, как вам хочется, но я считаю, что это удобный способ обработки простых грамматик. Его производительность может быть проблемой, поэтому вполне вероятно, что в ситуациях, когда вам нужна скорость, это может быть не очень хороший выбор.

Ответ 6

Я чувствую твою боль. Я регулярно занимаюсь файлами с полями фиксированной ширины (выводится через код Fortran77), поэтому всегда интересно попытаться загрузить их с минимумом суеты. Лично я хотел бы, чтобы boost::format обеспечил реализацию scanf. Но, не выполняя его сам, я делаю что-то похожее на @Nikko, используя boost::tokenizer со смещением разделителей и lexical cast для преобразования. Например,

typedef boost::token_iterator_generator< 
                                boost::char_separator<char> >::type tokenizer;

boost::char_separator<char> sep("=,");

std::string line;
std::getline( file_istream, line );
tokenizer tok = boost::make_token_iterator< std::string > (
                                line.begin(), line.end() sep );

std::string var = *tok;  // need to check for tok.at_end() here
++tok;

std::vector< int > vals;
for(;!tok.at_end();++tok){
 vals.push_back( boost::lexical_cast< int >( trimws( *tok ) );
}

Примечание: boost::lexical_cast не справляется с ведущими пробелами (он бросает), поэтому я рекомендую обрезать пробелы всего, что вы передаете.

Ответ 7

Извините, что не смог дать вам специфику, но С++ 11 имеет функции регулярного выражения. Может быть, вы можете посмотреть на них. Они кажутся родовыми (так как вы можете сопоставлять последовательность всего, а не только несколько символов).

Возможно, есть интересное решение вашей проблемы.