【问题标题】:How to parse csv using boost::spirit如何使用 boost::spirit 解析 csv
【发布时间】:2013-08-24 07:11:09
【问题描述】:

我有这个 csv 行

std::string s = R"(1997,Ford,E350,"ac, abs, moon","some "rusty" parts",3000.00)";

我可以使用boost::tokenizer解析它:

typedef boost::tokenizer< boost::escaped_list_separator<char> , std::string::const_iterator, std::string> Tokenizer;
boost::escaped_list_separator<char> seps('\\', ',', '\"');
Tokenizer tok(s, seps);
for (auto i : tok)
{
    std::cout << i << std::endl;
}

它是正确的,除了标记“生锈”应该有双引号被剥离。

这是我使用 boost::spirit 的尝试

boost::spirit::classic::rule<> list_csv_item = !(boost::spirit::classic::confix_p('\"', *boost::spirit::classic::c_escape_ch_p, '\"') | boost::spirit::classic::longest_d[boost::spirit::classic::real_p | boost::spirit::classic::int_p]);
std::vector<std::string> vec_item;
std::vector<std::string>  vec_list;
boost::spirit::classic::rule<> list_csv = boost::spirit::classic::list_p(list_csv_item[boost::spirit::classic::push_back_a(vec_item)],',')[boost::spirit::classic::push_back_a(vec_list)];
boost::spirit::classic::parse_info<> result = parse(s.c_str(), list_csv);
if (result.hit)
{
  for (auto i : vec_item)
  {
    cout << i << endl;
   }
}

问题:

  1. 不起作用,只打印第一个令牌

  2. 为什么是 boost::spirit::classic?找不到使用 Spirit V2 的示例

  3. 设置很残酷..但我可以忍受

** 我真的很想使用boost::spirit,因为它往往很快

预期输出:

1997
Ford
E350
ac, abs, moon
some "rusty" parts

3000.00

【问题讨论】:

  • 我不明白您将如何将""rusty"" 视为有效输入。如果引用的字符串没问题,那么我希望"embedded ""quotes"" like this",但不会意外""(空字符串)出现在字段中。
  • 我已经编辑了字符串输入,希望双引号更有意义。
  • 我不认为它现在更有意义了。报价数量不平衡。为什么不提供预期的输出
  • 刚刚发布的预期输出
  • 我认为没有一种理智的方式可以以这种方式解释该输入。 '嵌入'引号 will 必须以一种或另一种方式转义("" 或例如\"),否则扫描无法确定是否到达字符串的末尾?我认为没有任何 CSV 引擎会这样对待它。

标签: c++ csv boost boost-spirit boost-spirit-qi


【解决方案1】:

有关解析(可选)引号分隔字段的背景信息,包括不同的引号字符('"),请参见此处:

对于一个非常、非常、非常完整的示例,完整的支持部分引用值和一个

splitInto(input, output, ' ');

采用“任意”输出容器和分隔符表达式的方法,请参见此处:

解决您的确切问题,假设 either 引用 未引用字段(没有部分引号 inside 字段值),使用 Spirit V2:

让我们采用可能可行的最简单的“抽象数据类型”:

using Column  = std::string;
using Columns = std::vector<Column>;
using CsvLine = Columns;
using CsvFile = std::vector<CsvLine>;

而重复的双引号转义双引号语义(正如我在评论中指出的那样),您应该能够使用类似的东西:

static const char colsep = ',';

start  = -line % eol;
line   = column % colsep;
column = quoted | *~char_(colsep);
quoted = '"' >> *("\"\"" | ~char_('"')) >> '"';

以下完整的测试程序打印出来

[1997][Ford][E350][ac, abs, moon][rusty][3001.00]

(注意 BOOST_SPIRIT_DEBUG 定义以便于调试)。看到它Live on Coliru

完整演示

//#define BOOST_SPIRIT_DEBUG
#include <boost/spirit/include/qi.hpp>

namespace qi = boost::spirit::qi;

using Column  = std::string;
using Columns = std::vector<Column>;
using CsvLine = Columns;
using CsvFile = std::vector<CsvLine>;

template <typename It>
struct CsvGrammar : qi::grammar<It, CsvFile(), qi::blank_type>
{
    CsvGrammar() : CsvGrammar::base_type(start)
    {
        using namespace qi;

        static const char colsep = ',';

        start  = -line % eol;
        line   = column % colsep;
        column = quoted | *~char_(colsep);
        quoted = '"' >> *("\"\"" | ~char_('"')) >> '"';

        BOOST_SPIRIT_DEBUG_NODES((start)(line)(column)(quoted));
    }
  private:
    qi::rule<It, CsvFile(), qi::blank_type> start;
    qi::rule<It, CsvLine(), qi::blank_type> line;
    qi::rule<It, Column(),  qi::blank_type> column;
    qi::rule<It, std::string()> quoted;
};

int main()
{
    const std::string s = R"(1997,Ford,E350,"ac, abs, moon","""rusty""",3001.00)";

    auto f(begin(s)), l(end(s));
    CsvGrammar<std::string::const_iterator> p;

    CsvFile parsed;
    bool ok = qi::phrase_parse(f,l,p,qi::blank,parsed);

    if (ok)
    {
        for(auto& line : parsed) {
            for(auto& col : line)
                std::cout << '[' << col << ']';
            std::cout << std::endl;
        }
    } else
    {
        std::cout << "Parse failed\n";
    }

    if (f!=l)
        std::cout << "Remaining unparsed: '" << std::string(f,l) << "'\n";
}

【讨论】:

  • 我无法在我的机器上编译您的代码。 VC12 64-bit Windows 7。它多次使编译器崩溃。但我发现它在 Coliru 中运行良好,所以它一定是我的环境
  • 或许可以查看background answer(或on github)链接的示例。我记得在各种编译器/增强版本上对此进行了测试。
  • 此外,这是此答案中示例的一个版本,它删除了所有使用 c++11 功能:http://ideone.com/VVVTYe。我打赌它编译。 (我最好的猜测是 MSVC 不喜欢 using 子句)
  • 是的,MSVC 不喜欢 using 子句,我已经改变了它,但它仍然不喜欢 CsvGrammer 构造函数中的声明。例如'eol' : 未声明的标识符
  • @kreuzerkrieg 这对你有好处吗? stackoverflow.com/questions/50821925/…
【解决方案2】:

Sehe 的帖子看起来比我的干净一点,但我把它放在一起有点,所以无论如何都是这样:

#include <boost/tokenizer.hpp>
#include <boost/spirit/include/qi.hpp>

namespace qi = boost::spirit::qi;

int main() {
    const std::string s = R"(1997,Ford,E350,"ac, abs, moon",""rusty"",3000.00)";

    // Tokenizer
    typedef boost::tokenizer< boost::escaped_list_separator<char> , std::string::const_iterator, std::string> Tokenizer;
    boost::escaped_list_separator<char> seps('\\', ',', '\"');
    Tokenizer tok(s, seps);
    for (auto i : tok)
        std::cout << i << "\n";
    std::cout << "\n";

    // Boost Spirit Qi
    qi::rule<std::string::const_iterator, std::string()> quoted_string = '"' >> *(qi::char_ - '"') >> '"';
    qi::rule<std::string::const_iterator, std::string()> valid_characters = qi::char_ - '"' - ',';
    qi::rule<std::string::const_iterator, std::string()> item = *(quoted_string | valid_characters );
    qi::rule<std::string::const_iterator, std::vector<std::string>()> csv_parser = item % ',';

    std::string::const_iterator s_begin = s.begin();
    std::string::const_iterator s_end = s.end();
    std::vector<std::string> result;

    bool r = boost::spirit::qi::parse(s_begin, s_end, csv_parser, result);
    assert(r == true);
    assert(s_begin == s_end);

    for (auto i : result)
        std::cout << i << std::endl;
    std::cout << "\n";
}   

这个输出:

1997
Ford
E350
ac, abs, moon
rusty
3000.00

1997
Ford
E350
ac, abs, moon
rusty
3000.00

值得注意的事情:这并没有实现完整的 CSV 解析器。您还需要查看转义字符或实现所需的任何其他内容。

另外:如果您正在查看文档,您会知道,在 Qi 中,'a' 等同于 boost::spirit::qi::lit('a')"abc" 等同于 boost::spirit::qi::lit("abc")

双引号:因此,正如 Sehe 在上面的评论中指出的那样,输入文本中围绕 "" 的规则的含义并不清楚。如果您希望将不在带引号的字符串中的所有"" 实例转换为",则可以使用以下类似的方法。

qi::rule<std::string::const_iterator, std::string()> double_quote_char = "\"\"" >> qi::attr('"');
qi::rule<std::string::const_iterator, std::string()> item = *(double_quote_char | quoted_string | valid_characters );

【讨论】:

  • 这太棒了。我期待标记“生锈”周围有双引号。
  • 我已经添加了一个注释。虽然它确实为您提供了此输入的正确结果,但根据我不知道的一些更大的规则集,我不确定它是否正确。
  • 另外,请注意在构建 CSV 解析器时还有其他有趣的问题。空字符串会导致什么结果?此代码将生成一个看起来像 {''} 的向量,但其他人可能期望一个空向量 {}
  • 您对双引号的建议非常有效。我很惊讶,至少在这个输入上, boost::tokenizer 比 boost::spirit::qi 快。通常后者比我测试过的任何东西都快
  • 您可以使用正则表达式解析 CSV(boost::tokenizer 可能正在这样做),这将比精神更快。
猜你喜欢
  • 2011-01-26
  • 2011-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多