【问题标题】:What is the nicest way to parse this in C++?在 C++ 中解析这个的最好方法是什么?
【发布时间】:2010-06-02 14:40:42
【问题描述】:

在我的程序中,我有一个“服务器地址”列表,格式如下:

host[:port]

这里的括号,表示port是可选的。

  • host 可以是主机名、IPv4 或 IPv6 地址(可能采用“括号括起来”表示法)。
  • port,如果存在,可以是数字端口号或服务字符串(例如:“http”或“ssh”)。

如果port 存在且host 是IPv6 地址,则host 必须使用“括号括起来”表示法(例如:[::1]

以下是一些有效的示例:

localhost
localhost:11211
127.0.0.1:http
[::1]:11211
::1
[::1]

还有一个无效的例子:

::1:80 // Invalid: Is this the IPv6 address ::1:80 and a default port, or the IPv6 address ::1 and the port 80 ?
::1:http // This is not ambigous, but for simplicity sake, let's consider this is forbidden as well.

我的目标是将这些条目分成两部分(显然是hostport)。我不在乎hostport 是否无效,只要它们不包含非括号括起来的:290.234.34.34.5host 是可以的,它将被拒绝下一个过程);我只想将这两个部分分开,或者如果没有 port 部分,以某种方式知道

我尝试用std::stringstream 做点什么,但我想出的一切都显得老套,而且不是很优雅。

您将如何在C++ 中执行此操作?

我不介意C 中的答案,但首选C++。也欢迎任何boost 解决方案。

谢谢。

【问题讨论】:

  • 我不确定我理解您所说的“选择”是什么意思。我自己设计了格式(如果这是您要问的),但我相信这是一种非常常见的格式。我本可以使用另一个分隔符,但我认为它不会很优雅。示例:“连接到 localhost$http”似乎不如“连接到 localhost:http”直观。
  • 如果您不想更改分隔符(不过 localhost-http 对我来说似乎很好),那么您可以强制所有主机都用括号括起来,否则正则表达式会这样做
  • - 是主机名的合法字符;我真的不想用另一个来代替歧义;)
  • 好问题,我正在考虑用 C 语言完成同样的任务。

标签: c++ c parsing boost stl


【解决方案1】:

你看过boost::spirit吗?不过,这对您的任务来说可能有点过头了。

【讨论】:

  • 不知道它存在。谢谢。然而,正如你刚才所说,这对我的任务来说似乎有点矫枉过正。我没有其他人想出更直接的方法,我一定会深入研究它。
  • 由于社区似乎喜欢这个解决方案,有人可以给我一些指导,以便在我的具体情况下开始使用 boost::spirit 吗?
  • @ereOn:不幸的是,我从来没有理由与精神一起工作,它也从未成为我想玩的东西的首位,所以我不能给你任何建议. AFAIK 它是一个又大又重的模板元机器,很可能对于您的目的来说是超重的。正则表达式在这里没有帮助吗?如果没有,我可能只是使用字符串流编写一个简单的解析器。也就是说,精神教程开始的方式似乎很有趣......
  • @Matthieu:我担心这个问题超出了我的想象。 towel 是干什么用的?
  • 你不知道 h2g2 吗?你应该随身携带一条毛巾。
【解决方案2】:

这是一个简单的类,它使用 boost::xpressive 来完成验证 IP 地址类型的工作,然后您可以解析其余部分以获得结果。

用法:

const std::string ip_address_str = "127.0.0.1:3282";
IpAddress ip_address = IpAddress::Parse(ip_address_str);
std::cout<<"Input String: "<<ip_address_str<<std::endl;
std::cout<<"Address Type: "<<IpAddress::TypeToString(ip_address.getType())<<std::endl;
if (ip_address.getType() != IpAddress::Unknown)
{
    std::cout<<"Host Address: "<<ip_address.getHostAddress()<<std::endl;
    if (ip_address.getPortNumber() != 0)
    {
        std::cout<<"Port Number: "<<ip_address.getPortNumber()<<std::endl;
    }
}

类的头文件,IpAddress.h

#pragma once
#ifndef __IpAddress_H__
#define __IpAddress_H__


#include <string>

class IpAddress
{
public:
    enum Type
    {
        Unknown,
        IpV4,
        IpV6
    };
    ~IpAddress(void);

    /**
     * \brief   Gets the host address part of the IP address.
     * \author  Abi
     * \date    02/06/2010
     * \return  The host address part of the IP address.
    **/
    const std::string& getHostAddress() const;

    /**
     * \brief   Gets the port number part of the address if any.
     * \author  Abi
     * \date    02/06/2010
     * \return  The port number.
    **/
    unsigned short getPortNumber() const;

    /**
     * \brief   Gets the type of the IP address.
     * \author  Abi
     * \date    02/06/2010
     * \return  The type.
    **/
    IpAddress::Type getType() const;

    /**
     * \fn  static IpAddress Parse(const std::string& ip_address_str)
     *
     * \brief   Parses a given string to an IP address.
     * \author  Abi
     * \date    02/06/2010
     * \param   ip_address_str  The ip address string to be parsed.
     * \return  Returns the parsed IP address. If the IP address is
     *          invalid then the IpAddress instance returned will have its
     *          type set to IpAddress::Unknown
    **/
    static IpAddress Parse(const std::string& ip_address_str);

    /**
     * \brief   Converts the given type to string.
     * \author  Abi
     * \date    02/06/2010
     * \param   address_type    Type of the address to be converted to string.
     * \return  String form of the given address type.
    **/
    static std::string TypeToString(IpAddress::Type address_type);
private:
    IpAddress(void);

    Type m_type;
    std::string m_hostAddress;
    unsigned short m_portNumber;
};

#endif // __IpAddress_H__

类的源文件,IpAddress.cpp

#include "IpAddress.h"
#include <boost/xpressive/xpressive.hpp>

namespace bxp = boost::xpressive;

static const std::string RegExIpV4_IpFormatHost = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]+(\\:[0-9]{1,5})?$";
static const std::string RegExIpV4_StringHost = "^[A-Za-z0-9]+(\\:[0-9]+)?$";

IpAddress::IpAddress(void)
:m_type(Unknown)
,m_portNumber(0)
{
}

IpAddress::~IpAddress(void)
{
}

IpAddress IpAddress::Parse( const std::string& ip_address_str )
{
    IpAddress ipaddress;
    bxp::sregex ip_regex = bxp::sregex::compile(RegExIpV4_IpFormatHost);
    bxp::sregex str_regex = bxp::sregex::compile(RegExIpV4_StringHost);
    bxp::smatch match;
    if (bxp::regex_match(ip_address_str, match, ip_regex) || bxp::regex_match(ip_address_str, match, str_regex))
    {
        ipaddress.m_type = IpV4;
        // Anything before the last ':' (if any) is the host address
        std::string::size_type colon_index = ip_address_str.find_last_of(':');
        if (std::string::npos == colon_index)
        {
            ipaddress.m_portNumber = 0;
            ipaddress.m_hostAddress = ip_address_str;
        }else{
            ipaddress.m_hostAddress = ip_address_str.substr(0, colon_index);
            ipaddress.m_portNumber = atoi(ip_address_str.substr(colon_index+1).c_str());
        }
    }
    return ipaddress;
}

std::string IpAddress::TypeToString( Type address_type )
{
    std::string result = "Unknown";
    switch(address_type)
    {
    case IpV4:
        result = "IP Address Version 4";
        break;
    case IpV6:
        result = "IP Address Version 6";
        break;
    }
    return result;
}

const std::string& IpAddress::getHostAddress() const
{
    return m_hostAddress;
}

unsigned short IpAddress::getPortNumber() const
{
    return m_portNumber;
}

IpAddress::Type IpAddress::getType() const
{
    return m_type;
}

我只为 IPv4 设置了规则,因为我不知道 IPv6 的正确格式。但我很确定实现它并不难。 Boost Xpressive 只是一个基于模板的解决方案,因此不需要将任何 .lib 文件编译到您的 exe 中,我认为这是一个加分项。

顺便说一下,简单地分解正则表达式的格式...
^ = 字符串开头
$ = 字符串结尾
[] = 可以出现的一组字母或数字
[0-9] = 0 到 9 之间的任何一位数字
[0-9]+ = 0 到 9 之间的一位或多位数字
这 '。'对于正则表达式有特殊含义,但由于我们的格式在 ip 地址格式中有 1 个点,我们需要指定我们想要一个 '.'使用'\.'在数字之间。但由于 C++ 需要 '\' 的转义序列,我们必须使用 "\\."
? = 可选组件

所以,简而言之,"^[0-9]+$" 代表一个正则表达式,对于整数也是如此。
"^[0-9]+ \.$" 表示以 '.' 结尾的整数
"^[0-9]+\.[0-9]?$" 是整数以“。”结尾或小数。
对于整数或实数,正则表达式为 "^[0-9]+(\.[0-9]*)?$"
RegEx 一个介于 2 到 3 个数字之间的整数是 "^[0-9]{2,3}$"

现在来分解一下ip地址的格式:

"^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]+(\\:[0-9]{1,5})?$"

这是同义词:“^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9 ]+(\:[0-9]{1,5})?$",表示:

[start of string][1-3 digits].[1-3 digits].[1-3 digits].[1-3 digits]<:[1-5 digits]>[end of string]
Where, [] are mandatory and <> are optional

第二个 RegEx 比这更简单。它只是一个字母数字值后跟可选冒号和端口号的组合。

顺便说一句,如果您想测试 RegEx,您可以使用 this site

编辑:我没有注意到您可以选择使用 http 而不是端口号。为此,您可以将表达式更改为:

"^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]+(\\:([0-9]{1,5}|http|ftp|smtp))?$"

这接受以下格式:
127.0.0.1
127.0.0.1:3282
127.0.0.1:http
217.0.0.1:ftp
18.123.2.1:smtp

【讨论】:

  • 当人们遇到问题时,他们会说:我知道,我会使用正则表达式。现在他们有两个问题。
  • 哈哈。弄清楚它并不难。我不到 2 小时就学会了。这并不是说他不知道格式并且他还没有解决方案。如果我是对的,他已经有了一个使用 std::stringstream 的解决方案,他想要一个优雅的解决方案。我将在帖子中添加正则表达式的细分。
  • 臃肿。正则表达式对我来说一直很好用。抛开我所有懒惰的同事吹毛求疵。
  • 臃肿?你是说正则表达式还是性能?如果是性能,也许这可能会改变你的想法? boost-sandbox.sourceforge.net/libs/xpressive/doc/html/…
  • 因为是 2021 年,所以添加了我对 X3 的看法。它也可以优雅地处理“模棱两可”的情况。并抛出验证/DNS 解析:stackoverflow.com/a/66827620/85371
【解决方案3】:

我迟到了,但我正在谷歌上搜索如何做到这一点。 Spirit 和 C++ 成长了很多,所以让我添加一个 2021 年的观点:

Live On Compiler Explorer

#include <fmt/ranges.h>
#include <boost/spirit/home/x3.hpp>
#include <boost/fusion/adapted/std_tuple.hpp>

auto parse_server_address(std::string_view address_spec,
                          std::string_view default_service = "https")
{
    using namespace boost::spirit::x3;
    auto service = ':' >> +~char_(":") >> eoi;
    auto host    = '[' >> *~char_(']') >> ']' // e.g. for IPV6
        | raw[*("::" | (char_ - service))];

    std::tuple<std::string, std::string> result;
    parse(begin(address_spec), end(address_spec),
          expect[host >> (service | attr(default_service))], result);

    return result;
}

int main() {
    for (auto input : {
             "localhost",
             "localhost:11211",
             "127.0.0.1:http",
             "[::1]:11211",
             "::1", "[::1]",
             "::1:80", // Invalid: Is this the IPv6 address ::1:80 and a default
                       // port, or the IPv6 address ::1 and the port 80 ?
             "::1:http", // This is not ambigous, but for simplicity sake, let's
                         // consider this is forbidden as well.
         })
    {
        // auto [host, svc] = parse_server_address(input);
        fmt::print("'{}' -> {}\n", input, parse_server_address(input));
    }
}

打印

'localhost' -> ("localhost", "https")
'localhost:11211' -> ("localhost", "11211")
'127.0.0.1:http' -> ("127.0.0.1", "http")
'[::1]:11211' -> ("::1", "11211")
'::1' -> ("::1", "https")
'[::1]' -> ("::1", "https")
'::1:80' -> ("::1", "80")
'::1:http' -> ("::1", "http")

奖金

验证/解析地址。解析 100% 不变,只是使用 Asio 来解析结果,同时验证它们:

#include <boost/asio.hpp>
#include <iostream>
#include <iomanip>
using boost::asio::ip::tcp;
using boost::asio::system_executor;
using boost::system::error_code;

int main() {
    tcp::resolver r(system_executor{});
    error_code    ec;

    for (auto input : {
             "localhost",
             "localhost:11211",
             "127.0.0.1:http",
             "[::1]:11211",
             "::1", "[::1]",
             "::1:80", // Invalid: Is this the IPv6 address ::1:80 and a default
                       // port, or the IPv6 address ::1 and the port 80 ?
             "::1:http", // This is not ambigous, but for simplicity sake, let's
                         // consider this is forbidden as well.
             "stackexchange.com",
             "unknown-host.xyz",
         })
    {
        auto [host, svc] = parse_server_address(input);

        for (auto&& endpoint : r.resolve({host, svc}, ec)) {
            std::cout << input << " -> " << endpoint.endpoint() << "\n";
        }

        if (ec.failed()) {
            std::cout << input << " -> unresolved: " << ec.message() << "\n";
        }
    }
}

打印(有限网络Live On Wandbox和Coliruhttp://coliru.stacked-crooked.com/a/497d8091b40c9f2d

localhost -> 127.0.0.1:443
localhost:11211 -> 127.0.0.1:11211
127.0.0.1:http -> 127.0.0.1:80
[::1]:11211 -> [::1]:11211
::1 -> [::1]:443
[::1] -> [::1]:443
::1:80 -> [::1]:80
::1:http -> [::1]:80
stackexchange.com -> 151.101.129.69:443
stackexchange.com -> 151.101.1.69:443
stackexchange.com -> 151.101.65.69:443
stackexchange.com -> 151.101.193.69:443
unknown-host.xyz -> unresolved: Host not found (authoritative)

【讨论】:

    【解决方案4】:
    std::string host, port;
    std::string example("[::1]:22");
    
    if (example[0] == '[')
    {
        std::string::iterator splitEnd =
            std::find(example.begin() + 1, example.end(), ']');
        host.assign(example.begin(), splitEnd);
        if (splitEnd != example.end()) splitEnd++;
        if (splitEnd != example.end() && *splitEnd == ':')
            port.assign(splitEnd, example.end());
    }
    else
    {
        std::string::iterator splitPoint =
            std::find(example.rbegin(), example.rend(), ':').base();
        if (splitPoint == example.begin())
            host = example;
        else
        {
            host.assign(example.begin(), splitPoint);
            port.assign(splitPoint, example.end());
        }
    }
    

    【讨论】:

    • 在 IPV6 部分,splitEnd 上的 &amp;&amp; 条件似乎很可疑。您正在调用未定义的行为......由于我们正在寻找 ],我不明白迭代器如何指向 :
    • 我还是不明白怎么可能是 ':',你不是说 (*(splitEnd++) == ':') 吗? (尽管会有再次出现未定义行为的风险)。
    • 我可能会出现......很严重......但我恐怕你还是有点不对劲。每当您分配给端口时,您都会忘记递增迭代器,因此端口的第一个字符将始终为:(如果有)。这是故意的吗?
    • @Matthieu M.:你说得对,我完全搞砸了 ipv6 地址。但是 ipv4 是正确的。反向迭代器的base() 成员是该反向迭代器前面的一个元素。
    【解决方案5】:

    如前所述,Boost.Spirit.Qi 可以处理这个问题。

    如前所述,这太过分了(真的)。

    const std::string line = /**/;
    
    if (line.empty()) return;
    
    std::string host, port;
    
    if (line[0] == '[')           // IP V6 detected
    {
      const size_t pos = line.find(']');
      if (pos == std::string::npos) return;  // Error handling ?
      host = line.substr(1, pos-1);
      port = line.substr(pos+2);
    }
    else if (std::count(line.begin(), line.end(), ':') > 1) // IP V6 without port
    {
      host = line;
    }
    else                          // IP V4
    {
      const size_t pos = line.find(':');
      host = line.substr(0, pos);
      if (pos != std::string::npos)
        port = line.substr(pos+1);
    }
    

    我真的不认为这需要一个解析库,因为: 的过度使用,它可能不会提高可读性。

    现在我的解决方案肯定不是完美无缺的,例如有人可能会怀疑它的效率......但我真的认为这已经足够了,至少你不会失去下一个维护者,因为根据经验,Qi 表达式几乎可以清楚!

    【讨论】:

    • 谢谢!可能不是最佳的,但绝对可读。但是,如果我提供以下字符串会发生什么情况:"[::1:22"
    • ::1:22 将被视为主机:这里根本没有错误处理,您可以验证在第一种情况下,有一个右括号 assert(pos != std::string::npos) 或任何您想要的 :)
    • std::string 是否有一个名为 count() 的函数?它给了我VC2008中的错误。错误 C2039:“count”:不是“std::basic_string<_elem>”的成员
    • 我创建了一个函数来计算字符串中的字符数,现在它使用"[::1:22" 给出了错误的结果。我得到了Host = ::1:22Port = ::1:22
    • @sehe:不错。你需要进行一场艰苦的战斗才能让你的答案达到顶峰;让我们希望其他人也能投票。
    【解决方案6】:
    #pragma once
    #ifndef ENDPOINT_HPP
    #define ENDPOINT_HPP
    
    #include <string>
    
    using std::string;
    
    struct Endpoint {
      string
        Host,
        Port;
      enum : char {
        V4,
        V6
      } Type = V4;
      __inline Endpoint(const string& text) {
        bind(text);
      }
    private:
      void __fastcall bind(const string& text) {
        if (text.empty())
          return;
        auto host { text };
        string::size_type bias = 0;
        constexpr auto NONE = string::npos;
        while (true) {
          bias = host.find_first_of(" \n\r\t", bias);
          if (bias == NONE)
            break;
          host.erase(bias, 1);
        }
        if (host.empty())
          return;
        auto port { host };
        bias = host.find(']');
        if (bias != NONE) {
          host.erase(bias);
          const auto skip = text.find('[');
          if (skip == NONE)
            return;
          host.erase(0, skip + 1);
          Type = V6;
          ++bias;
        }
        else {
          bias = host.find(':');
          if (bias == NONE)
            port.clear();
          else {
            const auto next = bias + 1;
            if (host.length() == next)
              return;
            if (host[next] == ':') {
              port.clear();
              Type = V6;
            }
            else if (! bias)
              host.clear();
            else
              host.erase(bias);
          }
        }
        if (! port.empty())
          Port = port.erase(0, bias + 1);
        if (! host.empty())
          Host = host;
      }
    };
    
    #endif // ENDPOINT_HPP
    

    【讨论】:

      【解决方案7】:

      如果您通过字符串或 C++ 中的字符数组获取端口和主机;你可以得到字符串的长度。执行一个 for 循环,直到字符串的末尾,直到找到一个冒号,然后在该位置将字符串分成两部分。

      for (int i=0; i<string.length; i++) {
           if (string[i] == ':') {
                if (string[i+1] != ':') {
                     if (i > 0) {
                          if (string[i-1] != ':') {
                               splitpoint = i;
      }    }    }    }    }
      

      只是一个有点深奥的建议,我相信有一种更有效的方法,但希望这会有所帮助, 大风

      【讨论】:

      • 您知道可以将条件句与&amp;&amp; 结合使用吗? if (string[i] == ':' &amp;&amp; string[i+1] != ':' &amp;&amp; i &gt; 0 &amp;&amp; string[i-1] != ':')
      • @Michael - 是的,我知道你可以,但是,如果你在检查 i>0 的同时尝试对 string[i-1] 进行比较,那么你会抛出错误,因为你不能访问 string[-1] 我只是把它放在一起 =P @ereOn - 没问题只是想我会给出我脑海中突然出现的第一件事
      • i 的值为string.length() - 1(最后一个循环)时,string[i+1] 也会解析为string[string.length()],我认为这超出了范围。
      • @GESchafer 不会短路句柄 (i >0) && (string[i-1] != ':') ?
      • @GESchafer 你可以做if (0 &amp;&amp; format_entire_harddrive());,什么都不会发生; evaluation stops 如果&amp;&amp; 的左侧为假,则不执行右侧
      猜你喜欢
      • 2020-06-28
      • 1970-01-01
      • 2016-10-27
      • 2018-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-28
      相关资源
      最近更新 更多