2010年8月12日 星期四

抓取網頁的利器--libcurl

由於用C++寫了悠閒農夫這個開心農場的外掛,
有時候會有人來問怎麼用C語言寫程式來跟網頁伺服器溝通,
關於這個問題,其實我也不會,
因為我使用了libcurl來處理麻煩的HTTP通訊協定。

libcurl是一個功能強大的網路通訊程式庫,
支援的通訊協定有FTP,FTPS,HTTP,HTTPS,SCP,SFTP,TFTP,TELNET,
DICT,LDAP,LDAPS,FILE,IMAP,SMTP,POP3,RTMP及RTSP。
詳細的介紹就請自行到它的官網看囉~

再來就說一下怎麼用libcurl來模擬登入Facebook吧。
一、libcurl的編譯方式,請參考編譯各Library
二、由於表達能力不太好,直接釋出悠閒農夫的使用者登入程式碼。
User.h
#ifndef USER_H
#define USER_H

#include <wx/string.h>
#include <curl/curl.h>

class User
{
    public:
        User();
        bool Login();
        void SetFBID(wxString &value);
        void SetLoginName(wxString value);
        void SetPassword(wxString value);
        wxString GetFBID(){return wxString(fbid, wxConvUTF8);};
        virtual ~User();
    protected:
    private:
        CURL *handle;
        CURLcode code;
        char lsd[128];
        char fbid[128];
        //FILE *headerfile;
        wxString _name;
        char c_name[128];
        wxString _pwd;
        char c_pwd[128];
};

#endif // USER_H

User.cpp
#include "User.h"

#include <wx/wx.h>

static size_t write_data(char *data, size_t size, size_t nmemb, wxString *writerData)
{
    return nmemb;
}

void getCookies(CURL *handle, wxString name, char *data)
{
    struct curl_slist *cookies;
    struct curl_slist *nc;
    curl_easy_getinfo(handle, CURLINFO_COOKIELIST, &cookies);
    nc = cookies;
    int index = wxNOT_FOUND;
    int len = name.Len() + 2;
    wxString search;
    search += _T("\t") + name + _T("\t");
    while (nc) {

        wxString msg(nc->data, wxConvUTF8);
        index = msg.Find(search);
        if(index != wxNOT_FOUND)
        {
            strcpy(data, (const char*)msg.Mid(index + len, msg.Len() - index - len).mb_str(wxConvUTF8));
            break;
        }
        nc = nc->next;
    }
    curl_slist_free_all(cookies);
}

User::User()
{
    //ctor
    //初始化
    curl_global_init(CURL_GLOBAL_ALL);
    //取得handle
    handle = curl_easy_init();
    code = curl_easy_setopt(handle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-TW; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 GTB6 (.NET CLR 3.5.30729)");
    code = curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_data);
    code = curl_easy_setopt(handle, CURLOPT_ENCODING, "gzip, deflate");
    code = curl_easy_setopt(handle, CURLOPT_WRITEDATA, NULL);
    code = curl_easy_setopt(handle, CURLOPT_URL, "http://apps.facebook.com/farmgame_tw/");
    code = curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 1L);
 code = curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);
}

User::~User()
{
    curl_easy_cleanup(handle);
    curl_global_cleanup();
}
bool isTest = false;
bool isLogin = false;

void User::SetFBID(wxString &value)
{
 isTest = true;
 long res_code = 0;
 char *new_url;
 strcpy(fbid, value.mb_str(wxConvUTF8));
 wxString fname = value;
 fname.Append(_T(".cks"));
 code = curl_easy_setopt(handle, CURLOPT_COOKIEFILE, (const char*)fname.mb_str(wxConvUTF8)); /* just to start the cookie engine */
 code = curl_easy_setopt(handle, CURLOPT_COOKIEJAR, (const char*)fname.mb_str(wxConvUTF8));
 code = curl_easy_perform(handle);
 curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res_code);
 curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &new_url);
 wxString msg(new_url, wxConvUTF8);
 isLogin = (res_code == 200 && msg.StartsWith(_T("http://apps.facebook.com/farmgame_tw/")));
 if(!isLogin) getCookies(handle, _T("lsd"), lsd);
}

bool User::Login()
{
 if(isLogin) {
  return true;
 }
 if(!isTest) {
  code = curl_easy_setopt(handle, CURLOPT_COOKIEFILE, ""); /* just to start the cookie engine */
  code = curl_easy_perform(handle);
  getCookies(handle, _T("lsd"), lsd);
 }
    struct curl_httppost *formpost=NULL;
    struct curl_httppost *lastptr=NULL;

    //塞Form的值
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "charset_test",
               CURLFORM_COPYCONTENTS, "€,´,€,´,水,Д,Є", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "next",
               CURLFORM_COPYCONTENTS, "http://apps.facebook.com/farmgame_tw/", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "version",
               CURLFORM_COPYCONTENTS, "1.0", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "api_key",
               CURLFORM_COPYCONTENTS, "c1f92c58896a86359ac815b2dc7c708a", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "return_session",
               CURLFORM_COPYCONTENTS, "0", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "session_key_only",
               CURLFORM_COPYCONTENTS, "0", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "charset_test",
               CURLFORM_COPYCONTENTS, "€,´,€,´,水,Д,Є", CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "lsd",
               CURLFORM_COPYCONTENTS, lsd, CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "email",
               CURLFORM_COPYCONTENTS, c_name, CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "pass",
               CURLFORM_COPYCONTENTS, c_pwd, CURLFORM_END);
    curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "login",
               CURLFORM_COPYCONTENTS, "登入", CURLFORM_END);


    if(handle)
    {
        code = curl_easy_setopt(handle, CURLOPT_URL, "https://login.facebook.com/login.php?login_attempt=1&canvas=1");
        code = curl_easy_setopt(handle, CURLOPT_HTTPPOST, formpost);

        curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 0L);
        curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 0L);
        curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);

        code = curl_easy_perform(handle);
  curl_formfree(formpost);
        if(code != CURLE_OK) throw wxString::Format(_T("錯誤:%s"), wxString(curl_easy_strerror(code), wxConvUTF8).c_str());
        long res_code = 0;
        char *new_url;
        curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &res_code);
        curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &new_url);
        wxString msg(new_url, wxConvUTF8);
        isLogin = (res_code == 200 && msg.StartsWith(_T("http://apps.facebook.com/farmgame_tw/")));
        if(isLogin) {
         getCookies(handle, _T("c_user"), fbid);
            wxString fname(fbid, wxConvUTF8);
            fname.Append(_T(".cks"));
            code = curl_easy_setopt(handle, CURLOPT_COOKIEJAR, (const char*)fname.mb_str(wxConvUTF8));
        }
        else getCookies(handle, _T("lsd"), lsd);
        //msg.Clear();
        //content.Clear();
    }
    return isLogin;
}

void User::SetPassword(wxString value)
{
    _pwd = value;
    strcpy(c_pwd, (const char*)value.mb_str(wxConvUTF8));
}

void User::SetLoginName(wxString value)
{
    _name = value;
    strcpy(c_name, (const char*)value.mb_str(wxConvUTF8));
}


大部份的網頁伺服器在判斷是不是合法的網頁使用都是靠cookies,
Facebook也不例外,
因此在登入後必須把cookies記起來,
在後續與Facebook的溝通都必須把cookies傳回去,
而libcurl在這部份已經都處理好了,
完全不用自己控制,
只須要把cookies存成檔案後,
叫libcurl去使用就可以了。

三、登入程式的使用方式
User user;
user.SetFBID(_("Facebook編號"));
user.SetLoginName(_("登入帳號"));
user.SetPassword(_("登入密碼"));
user.Login();
  1. 由於太過頻繁的同帳號登入,
    會被Facebook擋掉不給登,
    因此在登入成功後,
    會產生一個以Facebook編號為檔名的.cks檔案來儲存cookies,
    若有這個檔案存在,在登入前請先用SetFBID,
    把Facebook編號傳入,
    程式會先用cookies去跟facebook溝通,
    如果cookies還沒過期,
    那自然就不用再透過帳號、密碼的驗證了。
  2. 把登入帳號及登入密碼傳入
  3. call ser.Login()做登入的動作,若登入成功會回傳true。


四、使用libcurl的重點
  1. 使用前必需先呼叫curl_global_init(CURL_GLOBAL_ALL);做初始化的動作。
  2. 使用curl_easy_init();取得handle
  3. 如果網頁內容很大,
    必需使用
    curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_data);
    curl_easy_setopt(handle, CURLOPT_WRITEDATA, NULL);
    
    讓它的內容有地方輸出,不然會因為buffer不足而出錯。
  4. 如果libcurl的method所回傳的狀態碼不是CURLE_OK時,
    可以用
    curl_easy_strerror(code)
    來取得錯誤訊息


若要知道libcurl還有哪些用法,
可以去看它提供的exampleAPI說明
不過全都是英文的,不要跟我說看不懂,我英文也很差的!!

10 則留言:

  1. libcurl 真是好物,雖然我不是寫 C 的,不過有機會讀人家程式這種事我可不會放過,後來在老外的網站找到 libcurl.vb(封裝給 vb6 的版本),玩了一下... libcurl 真是太變態了,就像您說的很多麻煩事都給處理好了,以前自己用 Winsock + 一堆 API 收回來一些火星文,看了就先軟一半...

    回覆刪除
  2. 大大你好,先感謝你無私的分享!

    小弟目前想做一個task,就是希望能在embedded system (without browser)上利用curl來做facebook login並且取得個人網頁上朋友的動態訊息,並顯示在embedded system上.

    不過在網路上爬了很多文,都沒找到可以在不用browser下,成功login的方式(開發論壇上也說基於安全問題,facebook必須要透過browser login),但我看你開發的悠閒農夫,似乎就沒有透過browser就可以login,讓我看到一線希望.不過我借用你上面c"url的code,執行完curl_easy_perform()後,結果都得到"CURLE_UNSUPPORTED_PROTOCOL"的返回值.無法成功登錄.

    因此有些問題想請教你,希望你不吝回答,謝謝~~
    1.不曉得只用CURL在沒有browser的情況下,是否真的可以執行login的動作,也就是說,大大覺得我這個task可行性如何?
    2.對於user.SetFBID(_("Facebook編號")); 這個function中Facebook編號,應該是要填什麼,是個人的ID,還是指開心農場的ID?

    感謝你抽空閱讀,也希望能夠得到你的回應,謝謝~~~

    回覆刪除
  3. 給Hsu

    1.可行,不過最好了解什麼是HTTP通訊協定及其運作方式。

    2.user.SetFBID的用處請看「登入程式的使用方式」的第一點,Facebook編號不是農場的ID,基本上不填也是可以的。

    回覆刪除
  4. 這個 libcurl 是 ssl 版本嗎?

    回覆刪除
  5. to sega:
    它可以有ssl也可以沒有ssl
    主要是看你怎麼編譯它。

    回覆刪除
  6. 現在這個 code 還可以用嗎?
    我修改了來用似乎遇到問題
    curl_easy_strerror 回傳
    Unsupported protocol
    殘念

    回覆刪除
  7. to sega:
    你用的protocol是什麼呢?
    如果是https,那在編譯libcurl時要把openssl編譯進去才能用唷

    回覆刪除
  8. 不是也..我目前想寫的是http登入facebook
    沒寫過 http 的東西
    公司要我寫在 online game 裡面
    可以打卡 po 留言以及照片到 facebook 的機制
    到處找資料
    所以找到你這
    但似乎沒有很順利

    回覆刪除
  9. 你這麼一提我倒是看到..
    你用"https://login.facebook.com/login.php?login_attempt=1&canvas=1"
    那表示要用ssl版本的是吧
    我來重新安裝一下相關元件

    回覆刪除
  10. 用了有 ssl 的 libcurl 好像成功了...感恩

    回覆刪除