如何将字符串解析为std :: map并验证其格式?

时间:2019-06-20 06:42:17

标签: c++ regex parsing

我想将"{{0, 1}, {2, 3}}"之类的字符串解析为std::map。我可以使用<regex>库编写一个用于解析字符串的小函数,但是我不知道如何检查给定的字符串是否为有效格式。如何验证字符串的格式?

#include <list>
#include <map>
#include <regex>
#include <iostream>

void f(const std::string& s) {
  std::map<int, int> m;
  std::regex p {"[\\[\\{\\(](\\d+),\\s*(\\d+)[\\)\\}\\]]"};
  auto begin = std::sregex_iterator(s.begin(), s.end(), p);
  auto end = std::sregex_iterator();
  for (auto x = begin; x != end; ++x) {
    std::cout << x->str() << '\n';
    m[std::stoi(x->str(1))] = std::stoi(x->str(2));
  }
  std::cout << m.size() << '\n';
}

int main() {
  std::list<std::string> l {
    "{{0, 1},   (2,    3)}",
    "{{4,  5, {6, 7}}" // Ill-formed, so need to throw an excpetion.
  };
  for (auto x : l) {
    f(x);
  }
}

注意:我认为没有必要使用regex来解决此问题。任何形式的解决方案,包括通过减去子字符串立即进行验证和插入的某些方式,都是值得的。

5 个答案:

答案 0 :(得分:3)

可能有点太多,但是如果您手边有boost,则可以使用boost-spirit为您完成这项工作。优点可能是该解决方案易于扩展以解析其他种类的地图,例如std::map<std::string, int>

另一个不可小under的优点是,boost-spirit为您提供了理智的例外,以防字符串不满足您的语法要求。用手写的解决方案很难做到这一点。

错误发生的位置也由boost-spirit给出,以便您可以回溯到该位置。

#include <map>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_stl.hpp>
#include <boost/fusion/adapted/std_pair.hpp>

template <typename Iterator, typename Skipper>
struct mapLiteral : boost::spirit::qi::grammar<Iterator, std::map<int,int>(), Skipper>
{
    mapLiteral() : mapLiteral::base_type(map)
    {
        namespace qi = boost::spirit::qi;
        using qi::lit;

        map = (lit("{") >> pair >> *(lit(",") >> pair) >> lit("}"))|(lit("{") >> lit("}"));
        pair = (lit("{") >> boost::spirit::int_ >> lit(",") >> boost::spirit::int_ >> lit("}"));
    }

    boost::spirit::qi::rule<Iterator, std::map<int, int>(), Skipper> map;
    boost::spirit::qi::rule<Iterator, std::pair<int, int>(), Skipper> pair;
};

std::map<int,int> parse(const std::string& expression, bool& ok)
{
    std::map<int, int>  result;
    try {
        std::string formula = expression;
        boost::spirit::qi::space_type space;
        mapLiteral<std::string::const_iterator, decltype(space)> parser;
        auto b = formula.begin();
        auto e = formula.end();
        ok = boost::spirit::qi::phrase_parse(b, e, parser, space, result);
        if (b != e) {
            ok = false;
            return std::map<int, int>();
        }
        return result;
    }
    catch (const boost::spirit::qi::expectation_failure<std::string::iterator>&) {
        ok = false;
        return result;
    }
}


int main(int argc, char** args)
{
    std::vector<std::pair<std::map<int, int>,std::string>> tests = {
        {{ },"{  \t\n}"},
        {{{5,2},{2,1}},"{ {5,2},{2,1} }"},
        {{},"{{2, 6}{}}"} // Bad food
    };
    for (auto iter :tests)
    {
        bool ok;
        auto result = parse(iter.second, ok);
        if (result == iter.first)
        {
            std::cout << "Equal:" << std::endl;
        }
    }
}

答案 1 :(得分:3)

由于Han在评论中提到他想等待进一步的想法,所以我将展示一个附加的解决方案。

和以前每个人一样,我认为这是最合适的解决方案:-)

此外,我将打开“大锤子”的包装,并讨论“语言”和“语法”,以及,哦,乔姆斯基·希拉基。

第一个非常简单的答案:纯正则表达式无法计数。因此,他们无法检查匹配的牙套,例如3个开口牙套和3个闭合牙套。

它们大多被实现为DFA(确定性有限自动机),也称为FSA(有限状态自动机)。这里的相关属性之一是,他们确实只知道其当前状态。他们不能“记住”以前的状态。他们没有记忆。

他们可以产生的语言是所谓的“常规语言”。在乔姆斯基体系中,产生这种常规语言的语法是Type-3。并且“正则表达式”可以用来产生这样的语言。

但是,正则表达式有一些扩展名,这些扩展名也可以用来匹配大括号。看到这里:Regular expression to match balanced parentheses

但是根据原始定义,这些不是正则表达式。

我们真正需要的是Chomsky-Type-2语法。所谓的上下文无关语法。这通常将通过下推自动机来实现。堆栈用于存储其他状态。这是正则表达式所没有的“内存”。

因此,如果我们要检查给定表达式的语法(如您的情况),例如std :: map的输入,则可以定义一个超简单的语法,并使用标准的经典方法来解析输入字符串: Shift / Reduce解析器。

必须执行几个步骤:首先,将输入流拆分为Lexems od令牌。通常,这是通过所谓的Lexer或Scanner完成的。您将始终找到类似getNextToken之类的函数。然后令牌将在堆栈上转移。堆栈顶部将与语法中的结果相匹配。如果与产品右侧匹配,则堆栈中的元素将被产品左侧的无端子替换。重复此过程,直到语法的开始符号被击中(表示一切正常)或发现语法错误为止。

关于您的问题:

  

如何将字符串解析为std :: map并验证其格式?

我将其分为2个任务。

  1. 解析字符串以验证格式
  2. 如果字符串有效,则将数据放入地图中

任务2很简单,通常使用std :: istream_iterator进行单行处理。

不幸的是,任务1需要shift-reduce-parser。这有点复杂。

在下面的代码中,我显示了一种可能的解决方案。请注意:这可以通过将Token与属性一起使用来优化。属性将是整数和括号的类型。具有属性的令牌将存储在解析堆栈中。这样一来,我们就可以消除所有花括号的产生,并可以在解析器中填充地图(在“ {Token :: Pair,{Token :: B1open,Token :: Integer,令牌::逗号,令牌::整数,令牌:: B1close}}”

请参见下面的代码:

#include <iostream>
#include <iterator>
#include <sstream>
#include <map>
#include <vector>
#include <algorithm>

// Tokens:  Terminals and None-Terminals
enum class Token { Pair, PairList, End, OK, Integer, Comma, B1open, B1close, B2open, B2close, B3open, B3close };

// Production type for Grammar
struct Production { Token nonTerminal; std::vector<Token> rightSide; };

// The Context Free Grammar CFG
std::vector<Production>    grammar
{
       {Token::OK, { Token::B1open, Token::PairList, Token::B1close } },
       {Token::OK, { Token::B2open, Token::PairList, Token::B2close } },
       {Token::OK, { Token::B3open, Token::PairList, Token::B3close } },
       {Token::PairList, { Token::PairList, Token::Comma, Token::Pair}    },
       {Token::PairList, { Token::Pair } },
       {Token::Pair, { Token::B1open, Token::Integer, Token::Comma, Token::Integer, Token::B1close} },
       {Token::Pair, { Token::B2open, Token::Integer, Token::Comma, Token::Integer, Token::B2close} },
       {Token::Pair, { Token::B3open, Token::Integer, Token::Comma, Token::Integer, Token::B3close} }
};
// Helper for translating brace characters to Tokens
std::map<const char, Token> braceToToken{
 {'(',Token::B1open},{'[',Token::B2open},{'{',Token::B3open},{')',Token::B1close},{']',Token::B2close},{'}',Token::B3close},
};

// A classical    SHIFT - REDUCE  Parser
class Parser
{
public:
    Parser() : parseString(), parseStringPos(parseString.begin()) {}
    bool parse(const std::string& inputString);
protected:
    // String to be parsed
    std::string parseString{}; std::string::iterator parseStringPos{}; // Iterator for input string

    // The parse stack for the Shift Reduce Parser
    std::vector<Token> parseStack{};

    // Parser Step 1:   LEXER    (lexical analysis / scanner)
    Token getNextToken();
    // Parser Step 2:   SHIFT
    void shift(Token token) { parseStack.push_back(token); }
    // Parser Step 3:   MATCH / REDUCE
    bool matchAndReduce();
};

bool Parser::parse(const std::string& inputString)
{
    parseString = inputString; parseStringPos = parseString.begin(); parseStack.clear();
    Token token{ Token::End };
    do   // Read tokens untils end of string
    {
        token = getNextToken();     // Parser Step 1:   LEXER    (lexical analysis / scanner)                    
        shift(token);               // Parser Step 2:   SHIFT
        while (matchAndReduce())    // Parser Step 3:   MATCH / REDUCE
            ; // Empty body
    } while (token != Token::End);  // Do until end of string reached
    return (!parseStack.empty() && parseStack[0] == Token::OK);
}

Token Parser::getNextToken()
{
    Token token{ Token::End };
    // Eat all white spaces
    while ((parseStringPos != parseString.end()) && std::isspace(static_cast<int>(*parseStringPos))) {
        ++parseStringPos;
    }
    // Check for end of string
    if (parseStringPos == parseString.end()) {
        token = Token::End;
    }
    // Handle digits
    else if (std::isdigit(static_cast<int>(*parseStringPos))) {
        while ((((parseStringPos + 1) != parseString.end()) && std::isdigit(static_cast<int>(*(parseStringPos + 1)))))        ++parseStringPos;
        token = Token::Integer;
    }
    // Detect a comma
    else if (*parseStringPos == ',') {
        token = Token::Comma;
        // Else search for all kind of braces
    }
    else {
        std::map<const char, Token>::iterator foundBrace = braceToToken.find(*parseStringPos);
        if (foundBrace != braceToToken.end()) token = foundBrace->second;
    }
    // In next function invocation the next string element will be checked
    if (parseStringPos != parseString.end())
        ++parseStringPos;

    return token;
}


bool Parser::matchAndReduce()
{
    bool result{ false };
    // Iterate over all productions in the grammar
    for (const Production& production : grammar) {
        if (production.rightSide.size() <= parseStack.size()) {
            // If enough elements on the stack, match the top of the stack with a production
            if (std::equal(production.rightSide.begin(), production.rightSide.end(), parseStack.end() - production.rightSide.size())) {
                // Found production: Reduce
                parseStack.resize(parseStack.size() - production.rightSide.size());
                // Replace right side of production with left side
                parseStack.push_back(production.nonTerminal);
                result = true;
                break;
            }
        }
    }
    return result;
}

using IntMap = std::map<int, int>;
using IntPair = std::pair<int, int>;

namespace std {
    istream& operator >> (istream& is, IntPair& intPair)    {
        return is >> intPair.first >> intPair.second;
    }
    ostream& operator << (ostream& os, const pair<const int, int>& intPair) {
        return os << intPair.first << " --> " << intPair.second;
    }
}

int main()
{   // Test Data. Test Vector with different strings to test
    std::vector <std::string> testVector{
        "({10, 1 1},   (2,  3) , [5 ,6])",
        "({10, 1},   (2,  3) , [5 ,6])",
        "({10, 1})",
        "{10,1}"
    };
    // Define the Parser
    Parser parser{};
    for (std::string& test : testVector)
    {   // Give some nice info to the user
        std::cout << "\nChecking '" << test << "'\n";
        // Parse the test string and test, if it is valid
        bool inputStringIsValid = parser.parse(test);
        if (inputStringIsValid) {               // String is valid. Delete everything but digits
            std::replace_if(test.begin(), test.end(), [](const char c) {return !std::isdigit(static_cast<int>(c)); }, ' ');
            std::istringstream iss(test);       // Copy string with digits int a istringstream, so that we can read with istream_iterator
            IntMap intMap{ std::istream_iterator<IntPair>(iss),std::istream_iterator<IntPair>() };
            // Present the resulting data in the map to the user
            std::copy(intMap.begin(), intMap.end(), std::ostream_iterator<IntPair>(std::cout, "\n"));
        } else {
            std::cerr << "***** Invalid input data\n";
        }
    }
    return 0;
}

我希望这不太复杂。但这是“数学”正确的解决方案。玩得开心 。 。

答案 2 :(得分:3)

在我看来,基于Spirit的解析器始终更加健壮和易读。解析Spirit :-)也更加有趣。因此,除了@ Aleph0的答案,我还要提供一个compact solution based on Spirit-X3

#include <string>
#include <map>
#include <iostream>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/spirit/home/x3.hpp>

int main() {
    std::string input ="{{0, 1},  {2, 3}}";
    using namespace boost::spirit::x3;
    const auto pair = '{' > int_ > ',' > int_ > '}';
    const auto pairs = '{' > (pair % ',')  > '}';
    std::map<int, int> output;
    // ignore spaces, tabs, newlines
    phrase_parse(input.begin(), input.end(), pairs, space, output);

    for (const auto [key, value] : output) {
        std::cout << key << ":" << value << std::endl;
    }
}

请注意,我使用了运算符>,表示“期望”。因此,如果输入与预期不符,Spirit会引发异常。如果您希望静默失败,请使用运算符>>

答案 3 :(得分:1)

您可以像这样检查括号来验证您的字符串,这并不是非常有效,因为它始终会迭代每个字符串,但是可以对其进行优化。

#include <list>
#include <iostream>
#include <string>

bool validate(std::string s)
{
    std::list<char> parens;
    for (auto c : s) {
        if (c == '(' || c == '[' || c == '{') {
            parens.push_back(c);
        }

        if (c == ')' && parens.back() == '(') {
            parens.pop_back();
        } else if (c == ']' && parens.back() == '[') {
            parens.pop_back();
        } else if (c == '}' && parens.back() == '{') {
            parens.pop_back();
        }
    }
    return parens.size() == 0;
}


int main()
{
  std::list<std::string> l {
    "{{0, 1},   (2,    3)}",
    "{{4,  5, {6, 7}}" // Ill-formed, so need to throw an excpetion.
  };

  for (auto s : l) {
      std::cout << "'" << s << "' is " << (validate(s) ? "" : "not ") << "valid" << std::endl;
  }

  return 0;
}

上面的代码的输出是这样的:

'{{0, 1},   (2,    3)}' is valid
'{{4,  5, {6, 7}}' is notvalid

编辑:

此版本应该更高效,因为它会在发现字符串无效后立即返回。

bool validate(std::string s)
{
    std::list<char> parens;
    for (auto c : s) {
        if (c == '(' || c == '[' || c == '{') {
            parens.push_back(c);
        }

        if (c == ')') {
            if (parens.back() != '(') {
                return false;
            }
            parens.pop_back();
        } else if (c == ']') {
            if (parens.back() != '[') {
                return false;
            }
            parens.pop_back();
        } else if (c == '}') {
            if (parens.back() != '{') {
                return false;
            }
            parens.pop_back();
        }
    }
    return parens.size() == 0;
}

答案 4 :(得分:0)

您的正则表达式可以完美解析单个地图元素。我建议您在创建地图并用解析的元素填充之前验证字符串。

让我们使用稍有改进的regex版本:

[\[\{\(](([\[\{\(](\d+),(\s*)(\d+)[\)\}\]])(,?)(\s*))*[\)\}\]]

如果有效,则匹配整个字符串:它以[\[\{\(]开头,以[\)\}\]]结尾,内部包含地图元素的多个(或零个)模式,后跟,和多个(或零)空格。

代码如下:

#include <list>
#include <map>
#include <regex>
#include <sstream>
#include <iostream>

void f(const std::string& s) {
  // part 1: validate string
  std::regex valid_pattern {"[\\[\\{\\(](([\\[\\{\\(](\\d+),(\\s*)(\\d+)[\\)\\}\\]])(,?)(\\s*))*[\\)\\}\\]]"};
  auto valid_begin = std::sregex_iterator(s.begin(), s.end(), valid_pattern);
  auto valid_end = std::sregex_iterator();
  if (valid_begin == valid_end || valid_begin->str().size() != s.size ()) {
    std::stringstream res;
    res << "String \"" << s << "\" doesn't satisfy pattern!";
    throw std::invalid_argument (res.str ());
  } else {
    std::cout << "String \"" << s << "\" satisfies pattern!" << std::endl;
  }

  // part 2: parse map elements
  std::map<int, int> m;
  std::regex pattern {"[\\[\\{\\(](\\d+),\\s*(\\d+)[\\)\\}\\]]"};
  auto parsed_begin = std::sregex_iterator(s.begin(), s.end(), pattern);
  auto parsed_end = std::sregex_iterator();
  for (auto x = parsed_begin; x != parsed_end; ++x) {
    m[std::stoi(x->str(1))] = std::stoi(x->str(2));
  }

  std::cout << "Number of parsed elements: " << m.size() << '\n';
}

int main() {
  std::list<std::string> l {
      "{}",
      "[]",
      "{{0, 153}, (2, 3)}",
      "{{0,      153},   (2,    3)}",
      "{[0, 153],           (2, 3), [154, 33]   }",
      "{[0, 153],           (2, 3), [154, 33]   ", // Ill-formed, so need to throw an exception.
      "{{4,  5, {6, 7}}", // Ill-formed, so need to throw an exception.
      "{{4,  5, {x, 7}}" // Ill-formed, so need to throw an exception.
  };
  for (const auto &x : l) {
    try {
      f(x);
    }
    catch (std::invalid_argument &ex) {
      std::cout << ex.what () << std::endl;
    }
    std::cout << std::endl;
  }
}

以下是输出:

String "{}" satisfies pattern!
Number of parsed elements: 0

String "[]" satisfies pattern!
Number of parsed elements: 0

String "{{0, 153}, (2, 3)}" satisfies pattern!
Number of parsed elements: 2

String "{{0,      153},   (2,    3)}" satisfies pattern!
Number of parsed elements: 2

String "{[0, 153],           (2, 3), [154, 33]   }" satisfies pattern!
Number of parsed elements: 3

String "{[0, 153],           (2, 3), [154, 33]   " doesn't satisfy pattern!

String "{{4,  5, {6, 7}}" doesn't satisfy pattern!

String "{{4,  5, {x, 7}}" doesn't satisfy pattern!

PS它只有一个缺陷。它不会检查相应的右括号是否等于左括号。因此它与以下内容匹配:{]{(1,2])等。如果您认为不合适,最简单的解决方法是在将解析对放入地图中之前添加一些额外的验证代码。

PPS如果您能够避免使用正则表达式,则可以通过对每个字符串进行一次字符串扫描来更有效地解决您的问题。 @SilvanoCerza提出了针对此案例的实现。