使用 boost::spirit 进行 URL 解析

问题描述 投票:0回答:1

我正在尝试使用 boost::spirit 来编写 URL 解析器。我的目标是解析输入 URL(有效或无效)并将其分解为前缀、主机和后缀,如下所示:

输入ipv6 URL:https://[::ffff:192.168.1.1]:8080/path/to/resource
将其分为以下部分:
前缀:https://
主机:::ffff:192.168.1.1
后缀::8080/路径/到/资源

输入ipv6 URL:https://::ffff:192.168.1.1/path/to/resource
将其分为以下部分:
前缀:https://
主机:::ffff:192.168.1.1
后缀:/path/to/resource

输入ipv4 URL:https://192.168.1.1:8080/path/to/resource
将其分为以下部分:
前缀:https://
主机:192.168.1.1
后缀::8080/路径/到/资源

冒号字符“:”在 ipv6 地址中用作分隔符,在 ipv4 地址中也用作端口分隔符。由于这种歧义,我很难定义同时适用于 ipv4 和 ipv6 URL 的 boost::spirit 语法。请参考以下代码:

struct UrlParts
{
    std::string scheme;
    std::string host;
    std::string port;
    std::string path;
};

BOOST_FUSION_ADAPT_STRUCT(
    UrlParts,
    (std::string, scheme)
    (std::string, host)
    (std::string, port)
    (std::string, path)
)

void parseUrl_BoostSpirit(const std::string &input, std::string &prefix, std::string &suffix, std::string &host)
{
    namespace qi = boost::spirit::qi;

    // Define the grammar
    qi::rule<std::string::const_iterator, UrlParts()> url = -(+qi::char_("a-zA-Z0-9+-.") >> "://") >> -qi::lit('[') >> +qi::char_("a-fA-F0-9:.") >> -qi::lit(']') >> -(qi::lit(':') >> +qi::digit) >> *qi::char_;


    // Parse the input
    UrlParts parts;
    auto iter = input.begin();
    if (qi::parse(iter, input.end(), url, parts))
    {
        prefix = parts.scheme.empty() ? "" : parts.scheme + "://";
        host = parts.host;
        suffix = (parts.port.empty() ? "" : ":" + parts.port) + parts.path;
    }
    else
    {
        host = input;
    }
}

上面的代码对 ipv4 URL 产生错误的输出,如下所示:

输入网址ipv4:https://192.168.1.1:8080/path/to/resource
破损零件:
前缀:https://
主机:192.168.1.1:8080
后缀:/path/to/resource
即主机有 :8080 而不是后缀。

如果我更改 URL 语法,我可以修复 ipv4,但随后 ipv6 就会损坏。

当然,这可以使用简单的 if-else 解析逻辑来完成,但我正在尝试使用 boost::spirit 更优雅地做到这一点。关于如何更新语法以支持 ipv4 和 ipv6 URL 有什么建议吗?

PS:我知道根据 RFC,带有 ipv6 地址的 URL 不带 [ ] 是无效的,但我正在开发的应用程序也需要处理这些无效 URL。

提前致谢!

url boost-spirit url-parsing
1个回答
0
投票

首先你的表达式

char_("+-.")
意外地允许在方案中使用“,”:https://coliru.stacked-crooked.com/a/14c00775d9f3d99e

为了避免这种情况,请始终将

-
放在字符集中的第一个或最后一个,这样它就不会被误解为范围:
char_("+.-")
。是的,这很微妙。

-'[' >> p >> -']'
允许使用不匹配的括号。而是说
('[' >> p >> ']' | p)

应用这些之后,让我们重写解析器表达式,以便我们看看发生了什么:

// Define the grammar
auto scheme_ = qi::copy(+qi::char_("a-zA-Z0-9+.-") >> "://");
auto host_   = qi::copy(+qi::char_("a-fA-F0-9:."));
auto port_   = qi::copy(':' >> +qi::digit);

qi::rule<std::string::const_iterator, UrlParts()> const url =
    -scheme_ >> ('[' >> host_ >> ']' | host_) >> -port_ >> *qi::char_;

所以我继续创建一个测试台来演示您的问题示例:

注意,我通过添加

raw[]
来包含
://
并仅返回并打印
UrlParts
来简化处理,因为这样可以更深入地了解解析器的作用

住在Coliru

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

struct UrlParts { std::string scheme, host, port, path; };
BOOST_FUSION_ADAPT_STRUCT(UrlParts, scheme, host, port, path)

UrlParts parseUrl_BoostSpirit(std::string_view input) {
    namespace qi = boost::spirit::qi;

    using It = std::string_view::const_iterator;
    qi::rule<It, UrlParts()> url;
    //using R = qi::rule<It, std::string()>;
    //R scheme_, host_, port_;
    auto scheme_ = qi::copy(qi::raw[+qi::char_("a-zA-Z0-9+.-") >> "://"]);
    auto host_   = qi::copy(+qi::char_("a-fA-F0-9:."));
    auto port_   = qi::copy(':' >> +qi::digit);
    url          = -scheme_ >> ('[' >> host_ >> ']' | host_) >> -port_ >> *qi::char_;

    // BOOST_SPIRIT_DEBUG_NODES((scheme_)(host_)(port_)(url));
    BOOST_SPIRIT_DEBUG_NODES((url));

    // Parse the input
    UrlParts parts;
    parse(input.begin(), input.end(), qi::eps > url > qi::eoi, parts);
    return parts;
}

int main() {
    using It        = std::string_view::const_iterator;
    using Exception = boost::spirit::qi::expectation_failure<It>;

    for (std::string_view input : {
             "https://[::ffff:192.168.1.1]:8080/path/to/resource",
             "https://::ffff:192.168.1.1/path/to/resource",
             "https://192.168.1.1:8080/path/to/resource",
         }) {
        try {
            auto parsed = parseUrl_BoostSpirit(input);
            // using boost::fusion::operator<<; // less clear output, without PFR
            // std::cout << std::quoted(input) << " -> " << parsed << std::endl;
            std::cout << std::quoted(input) << " -> " << boost::pfr::io(parsed) << std::endl;
        } catch (Exception const& e) {
            std::cout << std::quoted(input) << " EXPECTED " << e.what_ << " at "
                      << std::quoted(std::string_view(e.first, e.last)) << std::endl;
        }
    }
}

打印:

"https://[::ffff:192.168.1.1]:8080/path/to/resource" -> {"https://", "::ffff:192.168.1.1", "8080", "/path/to/resource"}
"https://::ffff:192.168.1.1/path/to/resource" -> {"https://", "::ffff:192.168.1.1", "", "/path/to/resource"}
"https://192.168.1.1:8080/path/to/resource" -> {"https://", "192.168.1.1:8080", "", "/path/to/resource"}

问题

您已经评估了问题:

:8080
host_
的产生式相匹配。我认为端口规范是奇怪的,因为它必须是
'/'
或输入结束之前的最后一个。换句话说:

auto port_   = qi::copy(':' >> +qi::digit >> &('/' || qi::eoi));

现在您可以在

host_
生产中执行否定的前瞻断言,以避免占用端口规范:

auto host_   = qi::copy(+(qi::char_("a-fA-F0-9:.") - port_));

现在输出变成

住在Coliru

"https://[::ffff:192.168.1.1]:8080/path/to/resource" -> {"https://", "::ffff:192.168.1.1", "8080", "/path/to/resource"}
"https://::ffff:192.168.1.1/path/to/resource" -> {"https://", "::ffff:192.168.1.1", "", "/path/to/resource"}
"https://192.168.1.1:8080/path/to/resource" -> {"https://", "192.168.1.1", "8080", "/path/to/resource"}

请注意,此实现中存在一些效率低下的情况,并且可能违反 RFC。考虑语法的静态实例。也可以考虑使用 X3。

使用X3和Asio

我在这里有一个相关的答案:在 C++ 中解析这个的最好方法是什么?。它展示了使用 Asio 网络原语进行验证的 X3 方法。

提升网址

为什么要自己推出?

UrlParts parseUrl(std::string_view input) {
    auto parsed = boost::urls::parse_uri(input).value();
    return {parsed.scheme(), parsed.host(), parsed.port(), parsed.query()};
}

这会解析您所拥有的以及更多内容(来自参考帮助卡的片段):

值得注意的价值是

  • 一致性(是的,这意味着 IPV6 需要
    []
  • 正确的编码和解码
  • 低分配(许多操作仅在源字符串视图上工作)
  • 维护(无需自己调试/审核)
© www.soinside.com 2019 - 2024. All rights reserved.