C++跨平台解析URL的简便方法?

84

我需要在编写的C++应用程序中解析URL,以获取协议、主机、路径和查询。该应用程序旨在跨平台使用。我很惊讶在boostPOCO库中找不到任何可执行此操作的内容。是否有显而易见的地方我没有看到?有适当的开源库建议吗?还是这是我必须自己完成的任务?这并不是非常复杂的问题,但似乎是一个非常普遍的需求,我想知道为什么没有共同的解决方案。


2
C++(甚至更是C语言)不像其他一些编程语言那样。它不是那种默认情况下为太阳下的所有事情存在标准库的东西。可能有一些常用的库,但从标准库、语言特性,甚至像POSIX这样的特定于操作系统的API的角度来看,假定你可以自己完成很多工作。 - asveikau
41
我很乐意制造一个轮子,但如果已经有人做过了,我就不明白为什么要再去造一个。因此我的问题是:你说得对,“可能会有一些常用的库”,那就是我想问的。 - Andrew Bucknell
1
这是一种小型实用工具,你可以在你的代码库所依赖的大型框架中找到它。如果没有,那么编写一个小型URL实用程序集就是一个有趣的标准算法练习。 - wilhelmtell
1
http://curl.haxx.se/libcurl/c/curl_unescape.html - hB0
要使用RFC 3986标准解析URL,而且不需要导入任何新库,请查看与此相关的问题的答案:https://dev59.com/6lbTa4cB1Zd3GeqP7hhq#31613265 - Lorien Brune
21个回答

33
有一个库被建议纳入Boost,并允许您轻松解析HTTP URI。它使用Boost.Spirit,并且也是使用Boost软件许可证发布的。该库是cpp-netlib,您可以在这里找到其文档--您可以从这里下载最新版本。
您需要使用的相关类型是boost::network::http::uri,并在此处有文档记录。

2
添加过时原因的注释:由于Boost已经弃用get_io_service,因此该库在2020年无法完整编译。但是,您仍然可以从代码中提取相关功能,因为它是自包含的,不依赖于库的那些部分。 - The_Sympathizer

26

我需要上述内容的w字符串版本,并添加其他我需要的字段。它肯定可以被优化,但对于我的目的来说足够好了。

#include <string>
#include <algorithm>    // find

struct Uri
{
public:
std::wstring QueryString, Path, Protocol, Host, Port;

static Uri Parse(const std::wstring &uri)
{
    Uri result;

    typedef std::wstring::const_iterator iterator_t;

    if (uri.length() == 0)
        return result;

    iterator_t uriEnd = uri.end();

    // get query start
    iterator_t queryStart = std::find(uri.begin(), uriEnd, L'?');

    // protocol
    iterator_t protocolStart = uri.begin();
    iterator_t protocolEnd = std::find(protocolStart, uriEnd, L':');            //"://");

    if (protocolEnd != uriEnd)
    {
        std::wstring prot = &*(protocolEnd);
        if ((prot.length() > 3) && (prot.substr(0, 3) == L"://"))
        {
            result.Protocol = std::wstring(protocolStart, protocolEnd);
            protocolEnd += 3;   //      ://
        }
        else
            protocolEnd = uri.begin();  // no protocol
    }
    else
        protocolEnd = uri.begin();  // no protocol

    // host
    iterator_t hostStart = protocolEnd;
    iterator_t pathStart = std::find(hostStart, uriEnd, L'/');  // get pathStart

    iterator_t hostEnd = std::find(protocolEnd, 
        (pathStart != uriEnd) ? pathStart : queryStart,
        L':');  // check for port

    result.Host = std::wstring(hostStart, hostEnd);

    // port
    if ((hostEnd != uriEnd) && ((&*(hostEnd))[0] == L':'))  // we have a port
    {
        hostEnd++;
        iterator_t portEnd = (pathStart != uriEnd) ? pathStart : queryStart;
        result.Port = std::wstring(hostEnd, portEnd);
    }

    // path
    if (pathStart != uriEnd)
        result.Path = std::wstring(pathStart, queryStart);

    // query
    if (queryStart != uriEnd)
        result.QueryString = std::wstring(queryStart, uri.end());

    return result;

}   // Parse
};  // uri

测试/用法

Uri u0 = Uri::Parse(L"http://localhost:80/foo.html?&q=1:2:3");
Uri u1 = Uri::Parse(L"https://localhost:80/foo.html?&q=1");
Uri u2 = Uri::Parse(L"localhost/foo");
Uri u3 = Uri::Parse(L"https://localhost/foo");
Uri u4 = Uri::Parse(L"localhost:8080");
Uri u5 = Uri::Parse(L"localhost?&foo=1");
Uri u6 = Uri::Parse(L"localhost?&foo=1:2:3");

u0.QueryString, u0.Path, u0.Protocol, u0.Host, u0.Port....

为什么要使用wstring? - yeyimilk
在爬取互联网时,我发现现实世界中的URL通常是损坏或格式不正确的(但大多数浏览器仍然可以正确理解它们)。其中最大的问题是查询。是的,在现实世界中,它应该以?开头,但更常见的情况是以&开头。 - Martin York
你的代码在 ftp://user:passwd@example.com:1555/docs/Java&C++ 返回什么? - Aleksey F.

24

非常抱歉,无能为力。 :s

url.hh

#ifndef URL_HH_
#define URL_HH_    
#include <string>
struct url {
    url(const std::string& url_s); // omitted copy, ==, accessors, ...
private:
    void parse(const std::string& url_s);
private:
    std::string protocol_, host_, path_, query_;
};
#endif /* URL_HH_ */

url.cc

#include "url.hh"
#include <string>
#include <algorithm>
#include <cctype>
#include <functional>
using namespace std;

// ctors, copy, equality, ...

void url::parse(const string& url_s)
{
    const string prot_end("://");
    string::const_iterator prot_i = search(url_s.begin(), url_s.end(),
                                           prot_end.begin(), prot_end.end());
    protocol_.reserve(distance(url_s.begin(), prot_i));
    transform(url_s.begin(), prot_i,
              back_inserter(protocol_),
              ptr_fun<int,int>(tolower)); // protocol is icase
    if( prot_i == url_s.end() )
        return;
    advance(prot_i, prot_end.length());
    string::const_iterator path_i = find(prot_i, url_s.end(), '/');
    host_.reserve(distance(prot_i, path_i));
    transform(prot_i, path_i,
              back_inserter(host_),
              ptr_fun<int,int>(tolower)); // host is icase
    string::const_iterator query_i = find(path_i, url_s.end(), '?');
    path_.assign(path_i, query_i);
    if( query_i != url_s.end() )
        ++query_i;
    query_.assign(query_i, url_s.end());
}

main.cc

// ...
    url u("HTTP://stackoverflow.com/questions/2616011/parse-a.py?url=1");
    cout << u.protocol() << '\t' << u.host() << ...

2
小问题:您不需要在这里使用 ptr_fun,如果您这样做,您需要 #include <functional>。(您可能也不应该使用 using namespace std,但我假设这不是生产代码) - Billy ONeal
我省略了一些琐碎的功能,比如赋值运算符、构造函数、访问器等等。url类不应该有修改器。对于相等运算符,你可以添加一个哈希成员,在解析原始字符串时填充它。然后,比较两个URL是否相等应该非常快。这也意味着一些额外的复杂性;这取决于你。 - wilhelmtell
5
@Billy,我总是将命名空间std引入到我的编译单元中(而不是头文件!)。我认为这完全没问题,并且我认为在所有地方都使用std::会导致更多的污染和眼睛疲劳,而引入命名空间则不会。 - wilhelmtell
15
有趣的是,与Billy ONeal相反,我同意删除我遇到的所有using namespace。如果你真的重复了一个符号,你总是可以使用using std::string;,但我更喜欢使用命名空间限定符,这使得像我这样的可怜人更容易理解该符号来自何处。 - Matthieu M.
你还没有考虑到 example.com:port/pathname 语法。 - Chris K
10
除了example.com:port/pathname以外,还有很多不支持的URI/URL形式。例如http:/pathname,更重要的是http://username:password@example.com/pathname#section,所有组合都列在http://www.ietf.org/rfc/rfc2396.txt中,它们显示以下正则表达式:^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?。 - jdkoftinoff

14

POCO的URI类可以帮助您解析URL。以下示例是POCO URI和UUID幻灯片中的简化版本:

#include "Poco/URI.h"
#include <iostream>

int main(int argc, char** argv)
{
    Poco::URI uri1("http://www.appinf.com:88/sample?example-query#frag");

    std::string scheme(uri1.getScheme()); // "http"
    std::string auth(uri1.getAuthority()); // "www.appinf.com:88"
    std::string host(uri1.getHost()); // "www.appinf.com"
    unsigned short port = uri1.getPort(); // 88
    std::string path(uri1.getPath()); // "/sample"
    std::string query(uri1.getQuery()); // "example-query"
    std::string frag(uri1.getFragment()); // "frag"
    std::string pathEtc(uri1.getPathEtc()); // "/sample?example-query#frag"

    return 0;
}

注意,不确定为什么当您传递类似于“127.0.0.1:443”这样的内容时,它不会按预期工作... - Gelldur

13

为了完整性,有一个用C语言编写的库可以使用(毫无疑问需要稍作封装):https://uriparser.github.io/

[符合RFC标准并支持Unicode]


这是我一直在使用的一个非常基本的封装程序,它仅用于获取解析结果。

#include <string>
#include <uriparser/Uri.h>


namespace uriparser
{
    class Uri //: boost::noncopyable
    {
        public:
            Uri(std::string uri)
                : uri_(uri)
            {
                UriParserStateA state_;
                state_.uri = &uriParse_;
                isValid_   = uriParseUriA(&state_, uri_.c_str()) == URI_SUCCESS;
            }

            ~Uri() { uriFreeUriMembersA(&uriParse_); }

            bool isValid() const { return isValid_; }

            std::string scheme()   const { return fromRange(uriParse_.scheme); }
            std::string host()     const { return fromRange(uriParse_.hostText); }
            std::string port()     const { return fromRange(uriParse_.portText); }
            std::string path()     const { return fromList(uriParse_.pathHead, "/"); }
            std::string query()    const { return fromRange(uriParse_.query); }
            std::string fragment() const { return fromRange(uriParse_.fragment); }

        private:
            std::string uri_;
            UriUriA     uriParse_;
            bool        isValid_;

            std::string fromRange(const UriTextRangeA & rng) const
            {
                return std::string(rng.first, rng.afterLast);
            }

            std::string fromList(UriPathSegmentA * xs, const std::string & delim) const
            {
                UriPathSegmentStructA * head(xs);
                std::string accum;

                while (head)
                {
                    accum += delim + fromRange(head->text);
                    head = head->next;
                }

                return accum;
            }
    };
}

3
+1,我最终克隆了你在Github上的URL解析器库。不用拉进所有的Boost库,感觉好多了... - Alan
@Alan,我不知道Boost中是否有URL解析器。cpp-netlib有一个,但我曾经遇到过一些问题(现在很可能已经修复),所以我使用了这个解析器。 - Elliot Cameron

8
//sudo apt-get install libboost-all-dev; #install boost
//g++ urlregex.cpp -lboost_regex; #compile
#include <string>
#include <iostream>
#include <boost/regex.hpp>

using namespace std;

int main(int argc, char* argv[])
{
    string url="https://www.google.com:443/webhp?gws_rd=ssl#q=cpp";
    boost::regex ex("(http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
    boost::cmatch what;
    if(regex_match(url.c_str(), what, ex)) 
    {
        cout << "protocol: " << string(what[1].first, what[1].second) << endl;
        cout << "domain:   " << string(what[2].first, what[2].second) << endl;
        cout << "port:     " << string(what[3].first, what[3].second) << endl;
        cout << "path:     " << string(what[4].first, what[4].second) << endl;
        cout << "query:    " << string(what[5].first, what[5].second) << endl;
        cout << "fragment: " << string(what[6].first, what[6].second) << endl;
    }
    return 0;
}

工作得很好,但速度较慢。 - Shmil The Cat

7

6

Facebook的Folly库可以轻松地为您完成这项工作。只需使用Uri类即可:

#include <folly/Uri.h>

int main() {
    folly::Uri folly("https://code.facebook.com/posts/177011135812493/");

    folly.scheme(); // https
    folly.host();   // code.facebook.com
    folly.path();   // posts/177011135812493/
}

5

QT有QUrl来实现这个功能。GNOME则在libsoup中使用SoupURI,你会发现它更加轻量级。


4

我知道这是一个非常老的问题,但我发现以下内容很有用:

http://www.zedwood.com/article/cpp-boost-url-regex

它提供了三个例子:

(使用Boost)

//sudo apt-get install libboost-all-dev;
//g++ urlregex.cpp -lboost_regex
#include <string>
#include <iostream>
#include <boost/regex.hpp>

using std::string;
using std::cout;
using std::endl;
using std::stringstream;

void parse_url(const string& url) //with boost
{
    boost::regex ex("(http|https)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
    boost::cmatch what;
    if(regex_match(url.c_str(), what, ex)) 
    {
        string protocol = string(what[1].first, what[1].second);
        string domain   = string(what[2].first, what[2].second);
        string port     = string(what[3].first, what[3].second);
        string path     = string(what[4].first, what[4].second);
        string query    = string(what[5].first, what[5].second);
        cout << "[" << url << "]" << endl;
        cout << protocol << endl;
        cout << domain << endl;
        cout << port << endl;
        cout << path << endl;
        cout << query << endl;
        cout << "-------------------------------" << endl;
    }
}

int main(int argc, char* argv[])
{
    parse_url("http://www.google.com");
    parse_url("https://mail.google.com/mail/");
    parse_url("https://www.google.com:443/webhp?gws_rd=ssl");
    return 0;
}

(不使用 Boost 库)

#include <string>
#include <iostream>

using std::string;
using std::cout;
using std::endl;
using std::stringstream;

string _trim(const string& str)
{
    size_t start = str.find_first_not_of(" \n\r\t");
    size_t until = str.find_last_not_of(" \n\r\t");
    string::const_iterator i = start==string::npos ? str.begin() : str.begin() + start;
    string::const_iterator x = until==string::npos ? str.end()   : str.begin() + until+1;
    return string(i,x);
}

void parse_url(const string& raw_url) //no boost
{
    string path,domain,x,protocol,port,query;
    int offset = 0;
    size_t pos1,pos2,pos3,pos4;
    x = _trim(raw_url);
    offset = offset==0 && x.compare(0, 8, "https://")==0 ? 8 : offset;
    offset = offset==0 && x.compare(0, 7, "http://" )==0 ? 7 : offset;
    pos1 = x.find_first_of('/', offset+1 );
    path = pos1==string::npos ? "" : x.substr(pos1);
    domain = string( x.begin()+offset, pos1 != string::npos ? x.begin()+pos1 : x.end() );
    path = (pos2 = path.find("#"))!=string::npos ? path.substr(0,pos2) : path;
    port = (pos3 = domain.find(":"))!=string::npos ? domain.substr(pos3+1) : "";
    domain = domain.substr(0, pos3!=string::npos ? pos3 : domain.length());
    protocol = offset > 0 ? x.substr(0,offset-3) : "";
    query = (pos4 = path.find("?"))!=string::npos ? path.substr(pos4+1) : "";
    path = pos4!=string::npos ? path.substr(0,pos4) : path;
    cout << "[" << raw_url << "]" << endl;
    cout << "protocol: " << protocol << endl;
    cout << "domain: " << domain << endl;
    cout << "port: " << port << endl;
    cout << "path: " << path << endl;
    cout << "query: " << query << endl;
}

int main(int argc, char* argv[])
{
    parse_url("http://www.google.com");
    parse_url("https://mail.google.com/mail/");
    parse_url("https://www.google.com:443/webhp?gws_rd=ssl");
    return 0;
}

(没有使用Boost的不同方法)

#include <string>
#include <stdint.h>
#include <cstring>
#include <sstream>
#include <algorithm>

#include <iostream> 
using std::cerr; using std::cout; using std::endl;

using std::string;

class HTTPURL
{
    private:
        string _protocol;// http vs https
        string _domain;  // mail.google.com
        uint16_t _port;  // 80,443
        string _path;    // /mail/
        string _query;   // [after ?] a=b&c=b

    public:
        const string &protocol;
        const string &domain;
        const uint16_t &port;
        const string &path;
        const string &query;

        HTTPURL(const string& url): protocol(_protocol),domain(_domain),port(_port),path(_path),query(_query)
        {
            string u = _trim(url);
            size_t offset=0, slash_pos, hash_pos, colon_pos, qmark_pos;
            string urlpath,urldomain,urlport;
            uint16_t default_port;

            static const char* allowed[] = { "https://", "http://", "ftp://", NULL};
            for(int i=0; allowed[i]!=NULL && this->_protocol.length()==0; i++)
            {
                const char* c=allowed[i];
                if (u.compare(0,strlen(c), c)==0) {
                    offset = strlen(c);
                    this->_protocol=string(c,0,offset-3);
                }
            }
            default_port = this->_protocol=="https" ? 443 : 80;
            slash_pos = u.find_first_of('/', offset+1 );
            urlpath = slash_pos==string::npos ? "/" : u.substr(slash_pos);
            urldomain = string( u.begin()+offset, slash_pos != string::npos ? u.begin()+slash_pos : u.end() );
            urlpath = (hash_pos = urlpath.find("#"))!=string::npos ? urlpath.substr(0,hash_pos) : urlpath;
            urlport = (colon_pos = urldomain.find(":"))!=string::npos ? urldomain.substr(colon_pos+1) : "";
            urldomain = urldomain.substr(0, colon_pos!=string::npos ? colon_pos : urldomain.length());
            this->_domain = _tolower(urldomain);
            this->_query = (qmark_pos = urlpath.find("?"))!=string::npos ? urlpath.substr(qmark_pos+1) : "";
            this->_path = qmark_pos!=string::npos ? urlpath.substr(0,qmark_pos) : urlpath;
            this->_port = urlport.length()==0 ? default_port : _atoi(urlport) ;
        };
    private:
        static inline string _trim(const string& input)
        {
            string str = input;
            size_t endpos = str.find_last_not_of(" \t\n\r");
            if( string::npos != endpos )
            {
                str = str.substr( 0, endpos+1 );
            }
            size_t startpos = str.find_first_not_of(" \t\n\r");
            if( string::npos != startpos )
            {
                str = str.substr( startpos );
            }
            return str;
        };
        static inline string _tolower(const string& input)
        {
            string str = input;
            std::transform(str.begin(), str.end(), str.begin(), ::tolower);
            return str;
        };
        static inline int _atoi(const string& input) 
        {
            int r;
            std::stringstream(input) >> r;
            return r;
        };
};

int main(int argc, char **argv)
{
    HTTPURL u("https://Mail.google.com:80/mail/?action=send#action=send");
    cout << "protocol: " << u.protocol << endl;
    cout << "domain: " << u.domain << endl;
    cout << "port: " << u.port << endl;
    cout << "path: " << u.path << endl;
    cout << "query: " << u.query << endl;
    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接