我正在尝试使用 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。
提前致谢!
首先你的表达式
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
// #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_));
现在输出变成
"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。
我在这里有一个相关的答案:在 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()};
}
这会解析您所拥有的以及更多内容(来自参考帮助卡的片段):
值得注意的价值是
[]
)