如何在Spirit X3中正确指定锚定条件?

时间:2017-08-04 03:38:59

标签: c++ parsing c++14 boost-spirit boost-spirit-x3

我是编写解析器的新手。我正在尝试创建一个可以从输入文本中提取美国邮政编码的解析器。我创建了以下解析器模式,它们完成了我想要的大部分工作。我可以按预期匹配5位邮政编码或9位邮政编码(90210-1234)。

然而,它不允许我避免匹配如下:

  

246764(返回46764)
  578397(返回78397)

我想为上述模式的右侧和左侧指定一些锚定条件,希望我可以消除上面的例子。更具体地说,我希望在数字或短划线与候选邮政编码的开头或结尾相邻时禁止匹配。

测试数据(粗体条目应匹配)

  

12345

     

foo456

     

ba58r

     

246764anc

     

578397

     

90210-
  15206-1
  的 15222-1825
  15212-4267-53410-2807

完整代码:

using It = std::string::const_iterator;
using ZipCode = boost::fusion::vector<It, It>;

namespace boost { namespace spirit { namespace x3 { namespace traits {
    template <>
    void move_to<It, ZipCode>(It b, It e, ZipCode& z)
    {
        z =
        {
            b,
            e
        };
}}}}}

void Parse(std::string const& input)
{
    auto start = std::begin(input);
    auto begin = start;
    auto end = std::end(input);

    ZipCode current;
    std::vector<ZipCode> matches;

    auto const fiveDigits = boost::spirit::x3::repeat(5)[boost::spirit::x3::digit];
    auto const fourDigits = boost::spirit::x3::repeat(4)[boost::spirit::x3::digit];
    auto const dash = boost::spirit::x3::char_('-');
    auto const notDashOrDigit = boost::spirit::x3::char_ - (dash | boost::spirit::x3::digit);

    auto const zipCode59 = 
        boost::spirit::x3::lexeme
        [
            -(&notDashOrDigit) >> 
            boost::spirit::x3::raw[fiveDigits >> -(dash >> fourDigits)] >> 
            &notDashOrDigit
        ];

    while (begin != end)
    {
        if (!boost::spirit::x3::phrase_parse(begin, end, zipCode59, boost::spirit::x3::blank, current))
        {
            ++begin;
        }
        else
        {
            auto startOffset = std::distance(start, boost::fusion::at_c<0>(current));
            auto endOffset = std::distance(start, boost::fusion::at_c<1>(current));
            auto length = std::distance(boost::fusion::at_c<0>(current), boost::fusion::at_c<1>(current));
            std::cout << "Matched (\"" << startOffset
                << "\", \"" 
                << endOffset
                << "\") => \""
                << input.substr(startOffset, length)
                << "\""
                << std::endl;
        }
    }
}

带有上述测试数据的代码产生以下输出:

  

匹配(“0”,“5”)=&gt; “12345”
  匹配(“29”,“34”)=&gt; “46764”
  匹配(“42”,“47”)=&gt; “78397”
  匹配(“68”,“78”)=&gt; “15222-1825”

如果我将zipCode59更改为以下内容,则无法回复:

auto const zipCode59 = 
    boost::spirit::x3::lexeme
    [
        &notDashOrDigit >> 
        boost::spirit::x3::raw[fiveDigits >> -(dash >> fourDigits)] >> 
        &notDashOrDigit
    ];

我已经阅读了这个问题:Stop X3 symbols from matching substrings。但是,这个问题使用符号表。我不认为这对我有用,因为我缺乏指定硬编码字符串的能力。我也不清楚该问题的答案是如何阻止主要内容的。

1 个答案:

答案 0 :(得分:2)

使用-(parser)只会使(parser)成为可选项。与-(&parser)一起使用它几乎没有任何效果。

也许你想要一个否定断言(“lookahead”),它是!(parser)(与&(parser)相反)。

  

请注意,潜在的混淆可能是因为一元减号(负断言)和二元减号(减少字符集)之间存在差异。

断言邮政编码以短划线/数字开头似乎......混淆了。如果你想肯定断言除了破折号或数字之外的其他东西&~char_("-0-9")(使用一元~来否定字符集)但它会在输入开始时阻止匹配。

积极方法

左右摆脱一些复杂性我天真地开始用这样的东西:

using It = std::string::const_iterator;
using ZipCode = boost::iterator_range<It>;

auto Parse(std::string const& input) {
    using namespace boost::spirit::x3;
    auto dig = [](int n) { return repeat(n)[digit]; };
    auto const zip59 = dig(5) >> -('-' >> dig(4));
    auto const valid = zip59 >> !graph;

    std::vector<ZipCode> matches;
    if (!parse(begin(input), end(input), *seek[raw[valid]], matches))
        throw std::runtime_error("parser failure");

    return matches;
}

当然哪个匹配太多了:

<强> Live On Coliru

Matched '12345'
Matched '78397'
Matched '15222-1825'
Matched '53410-2807'

做英雄

要限制它(并且仍然在输入开始时匹配),您可以seek[&('-'|digit)]然后要求一个有效的拉链。

我坦率地承认,在让它“正确”之前必须先调整一下。在这个过程中,我创建了一个调试助手:

auto trace_as = [&input](std::string const& caption, auto parser) { 
    return raw[parser] [([=,&input](auto& ctx) { 
        std::cout << std::setw(12) << (caption+":") << " '";
        auto range = _attr(ctx);
        for (auto ch : range) switch (ch) {
            case '\0': std::cout << "\\0"; break;
            case '\r': std::cout << "\\r"; break;
            case '\n': std::cout << "\\n"; break;
            default: std::cout << ch;
        }
        std::cout << "' at " << std::distance(input.begin(), range.begin()) << "\n";
    })]; 
};

auto const valid = seek[&trace_as("seek", '-' | digit)] >> raw[zip59] >> !graph;

std::vector<ZipCode> matches;
if (!parse(begin(input), end(input), -valid % trace_as("skip", *graph >> +space), matches))
    throw std::runtime_error("parser failure");

产生以下附加诊断输出:

<强> Live On Coliru

       seek: '1' at 0
       skip: '\n    ' at 5
       seek: '4' at 13
       skip: 'foo456\n    ' at 10
       seek: '5' at 23
       skip: 'ba58r\n    ' at 21
       seek: '2' at 31
       skip: '246764anc\n    ' at 31
       seek: '5' at 45
       skip: '578397\n    ' at 45
       seek: '9' at 56
       skip: '90210-\n    ' at 56
       seek: '1' at 67
       skip: '15206-1\n    ' at 67
       seek: '1' at 79
       skip: '\n    ' at 89
       seek: '1' at 94
Matched '12345'
Matched '15222-1825'

现在输出是我们想要的,让我们再次切断脚手架:

完整列表

<强> Live On Coliru

#include <boost/spirit/home/x3.hpp>

using It = std::string::const_iterator;
using ZipCode = boost::iterator_range<It>;

auto Parse(std::string const& input) {
    using namespace boost::spirit::x3;
    auto dig = [](int n) { return repeat(n)[digit]; };
    auto const zip59 = dig(5) >> -('-' >> dig(4));
    auto const valid = seek[&('-' | digit)] >> raw[zip59] >> !graph;

    std::vector<ZipCode> matches;
    if (!parse(begin(input), end(input), -valid % (*graph >> +space), matches))
        throw std::runtime_error("parser failure");

    return matches;
}

#include <iostream>
int main() {
    std::string const sample = R"(12345
foo456
ba58r
246764anc
578397
90210-
15206-1
15222-1825
15212-4267-53410-2807)";

    for (auto zip : Parse(sample))
        std::cout << "Matched '" << zip << "'\n";
}

打印:

Matched '12345'
Matched '15222-1825'