C++字符串解析(类似Python风格)

19

我喜欢在Python中可以这样做:

points = []
for line in open("data.txt"):
    a,b,c = map(float, line.split(','))
    points += [(a,b,c)]

基本上,这是在读取一系列行,每行表示三维空间中的一个点,该点由用逗号分隔的三个数字表示。

如何在C++中实现这一点而不会太过头疼呢?

性能并不是很重要,因为这种解析只发生一次,所以简单性更为重要。

P.S. 我知道这听起来像一个新手问题,但请相信我,在 D(几乎与 C++ 相同)中编写了一个词法分析器,它涉及逐个字符地读取一些文本并识别标记,只是,长时间使用 Python 后回到 C++,让我不想在这些事情上浪费时间。


18
以下是一些范例,它们有些类似于Python:http://www.codeproject.com/KB/recipes/Tokenizer.aspx 此外,这些范例非常高效且有些优雅。 - Matthieu N.
10个回答

24

我的做法会是这样的:

ifstream f("data.txt");
string str;
while (getline(f, str)) {
    Point p;
    sscanf(str.c_str(), "%f, %f, %f\n", &p.x, &p.y, &p.z); 
    points.push_back(p);
}

x、y、z必须是浮点数。

并包括:

#include <iostream>
#include <fstream>

2
如果你决定从使用浮点数转而使用双精度浮点数,不要忘记将每个 %f 更改为 %lf。在这种情况下,使用 operator>>() 而不是 sscanf() 的解决方案不需要更改。 - j_random_hacker
我接受了这个答案,因为它简洁明了 :) - hasen

17

C++ 字符串工具库 (StrTk) 对于您的问题有以下解决方案:

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

struct point { double x,y,z; }

int main()
{
   std::deque<point> points;
   point p;
   strtk::for_each_line("data.txt",
                        [&points,&p](const std::string& str)
                        {
                           strtk::parse(str,",",p.x,p.y,p.z);
                           points.push_back(p);
                        });
   return 0;
}

更多示例可以在这里找到


17

撇开所有这些好的例子不谈,在C++中,你通常会重载operator >>来实现类似这样的操作:

point p;
while (file >> p)
    points.push_back(p);

甚至更多:

copy(
    istream_iterator<point>(file),
    istream_iterator<point>(),
    back_inserter(points)
);

这个操作符的相关实现看起来很像 j_random_hacker 的代码。


如果您的代码中需要在多个不同的位置输入Point对象,则这绝对是正确的方法。 - j_random_hacker

14
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>     // For replace()

using namespace std;

struct Point {
    double a, b, c;
};

int main(int argc, char **argv) {
    vector<Point> points;

    ifstream f("data.txt");

    string str;
    while (getline(f, str)) {
        replace(str.begin(), str.end(), ',', ' ');
        istringstream iss(str);
        Point p;
        iss >> p.a >> p.b >> p.c;
        points.push_back(p);
    }

    // Do something with points...

    return 0;
}

@Iraimbilanja:虽然我两次遍历字符串(首先使用replace(),然后通过iss),但我怀疑这在实践中至少与其他解决方案一样快,可能除了klew的基于sscanf()的方法。CPU擅长replace()。 - j_random_hacker

7

本回答基于 j_random_hacker 的先前答案,并使用了 Boost Spirit。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <boost/spirit.hpp>

using namespace std;
using namespace boost;
using namespace boost::spirit;

struct Point {
    double a, b, c;
};

int main(int argc, char **argv) 
{
    vector<Point> points;

    ifstream f("data.txt");

    string str;
    Point p;
    rule<> point_p = 
           double_p[assign_a(p.a)] >> ',' 
        >> double_p[assign_a(p.b)] >> ',' 
        >> double_p[assign_a(p.c)] ; 

    while (getline(f, str)) 
    {
        parse( str, point_p, space_p );
        points.push_back(p);
    }

    // Do something with points...

    return 0;
}

2
也许是因为使用boost::spirit来解析逗号分隔列表有些过度了?Boost::spirit会显著影响编译时间。 - JBeurer
1
也许是因为您在循环内实例化规则的原因,通常这将是效率低下的巨大来源,最好将其定义在循环之外。- Spirit 过于复杂,增加了大量编译时间,并且几乎不可能调试,编译器警告和错误消息根本无法理解。 - Matthieu N.

4

使用Boost.Tuples让编程更加有趣:

#include <boost/tuple/tuple_io.hpp>
#include <vector>
#include <fstream>
#include <iostream>
#include <algorithm>

int main() {
    using namespace boost::tuples;
    typedef boost::tuple<float,float,float> PointT;

    std::ifstream f("input.txt");
    f >> set_open(' ') >> set_close(' ') >> set_delimiter(',');

    std::vector<PointT> v;

    std::copy(std::istream_iterator<PointT>(f), std::istream_iterator<PointT>(),
             std::back_inserter(v)
    );

    std::copy(v.begin(), v.end(), 
              std::ostream_iterator<PointT>(std::cout)
    );
    return 0;
}

请注意,这与您问题中的Python代码并不完全相同,因为元组不必放在不同的行上。例如,下面这个例子:
1,2,3 4,5,6

将会产生与以下代码相同的输出:

1,2,3
4,5,6

这是由你来决定它是一个漏洞还是一个功能 :)


3

您可以使用 std::iostream 按行读取文件,将每一行放入 std::string 中,然后使用 boost::tokenizer 进行分割。这不会像 Python 一样优雅/简短,但比逐个字符读取要容易得多...


1

这些都是很好的例子。但它们并没有回答以下问题:

  1. 一个CSV文件,其中列数不同(某些行比其他行多列)
  2. 或者当一些值有空格时(ya yb,x1 x2,,x2,)

所以对于那些仍在寻找的人,这个类: http://www.codeguru.com/cpp/tic/tic0226.shtml 非常酷...可能需要进行一些更改


1

Sony Picture Imagework的开源项目之一是Pystring,它应该可以直接转换字符串分割部分:

Pystring是一个C++函数集合,使用std::string匹配python的字符串类方法的接口和行为。它是用C++实现的,不需要或使用python解释器。它为标准C++库中未包含的常见字符串操作提供了方便和熟悉感。

这里有一些示例一些文档


1

这个代码远没有那么简洁,当然我也没有编译它。

float atof_s( std::string & s ) { return atoi( s.c_str() ); }
{ 
ifstream f("data.txt")
string str;
vector<vector<float>> data;
while( getline( f, str ) ) {
  vector<float> v;
  boost::algorithm::split_iterator<string::iterator> e;
  std::transform( 
     boost::algorithm::make_split_iterator( str, token_finder( is_any_of( "," ) ) ),
     e, v.begin(), atof_s );
  v.resize(3); // only grab the first 3
  data.push_back(v);
}

丑陋的,你知道吗。你在读取CSV文件时,让它看起来像某种火箭科学一样复杂。保持简单。 - JBeurer

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