Простой синтаксический анализ строк с С++
Я использую С++ уже довольно давно, но, тем не менее, я склонен возвращаться к 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;
}
}
Какой самый сжатый способ достичь этого в С++? Всякий раз, когда я пытаюсь, я получаю много кода леса.
Ответы
Ответ 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 имеет функции регулярного выражения. Может быть, вы можете посмотреть на них. Они кажутся родовыми (так как вы можете сопоставлять последовательность всего, а не только несколько символов).
Возможно, есть интересное решение вашей проблемы.