linux網頁抓取-1

    最近一直在學習stevens的unix網絡編程,對於網絡通信有了一定的認識,所以也想練練手。聊天程序之前用winsock做過,這次不想做重複的。之前看到一哥們寫過windows下抓取貓撲的帖子,我覺得抓頁面也是一個不錯想法。我也喜歡逛貓撲,有時候也去追追裏面寫的文章,貓撲帖子少了一個很重要的功能,就是隻看樓主的帖子。貓撲水人很多,容易把樓主的帖子淹沒在大海里面。

        查看了一下貓撲帖子的網頁源代碼,帖子內容介於<divclass="box2 js-reply"data-rid="*"&gt;和</div>之間,只需要解析這段內容,就能得到自己想要的東西。不過裏面東西比較多,比較雜,還是先找一個簡單頁面抓取試試。csdn博客相對來說就是個不錯的選擇,第一沒廣告,內容不算很多,第二,代碼風格很好。抓CSDN的頁面無非獲得博主名,文章名字和URL等,如果想獲得更多的信息,可以把博主的排名,評論數抓取下來。

下面簡單分析一下CSDN博客源代碼。

博主標題:

  1. <div id="blog_title">  
  2.   
  3.             <h1>  
  4.   
  5.                 <a href="/lanyan822">編程小子的專欄</a></h1>  
  6.   
  7.             <h2>鍥而舍之,朽木不折;鍥而不捨,金石可鏤</h2>  
  8.   
  9.             <div class="clear">  
  10.   
  11.             </div>  
  12.   
  13.         </div>  
文章標題和URL:

  1. <span class="link_title"><a href="/lanyan822/article/details/7549916">  
  2.   
  3.       ubuntu11.10搭建git服務器  
  4.       </a>  
文章訪問次數,評論次數等:

  1. <div class="article_manage">  
  2.   
  3.     <span class="link_postdate">2012-05-14 15:09</span>  
  4.   
  5.     <span class="link_view" title="閱讀次數"><a href="/lanyan822/article/details/7549916" title="閱讀次數">閱讀</a>(21)</span>  
  6.   
  7.     <span class="link_comments" title="評論次數"><a href="/lanyan822/article/details/7549916#comments" title="評論次數">評論</a>(0)</span>  
  8. </div>  
博客統計信息:

  1. <ul id="blog_rank">  
  2.   
  3.            <li>訪問:<span>1218次</span></li>  
  4.   
  5.            <li>積分:<span>164分</span></li>  
  6.   
  7.            <li>排名:<span>千里之外</span></li>  
  8.   
  9.        </ul>  
  1. <ul id="blog_statistics">  
  2.   
  3.            <li>原創:<span>13篇</span></li>  
  4.   
  5.            <li>轉載:<span>2篇</span></li>  
  6.   
  7.            <li>譯文:<span>0篇</span></li>  
  8.   
  9.            <li>評論:<span>1條</span></li>  
  10.   
  11.        </ul>  

 從上面貼出的HTML可以看出,所需要的信息都在某一個id下,每個id是唯一的,這對解析是很有利的。我們只需要抓取到網頁,分析相應內容,得到想要的信息即可。

在確定CSDN博客是可以抓取後,就可以着手抓取。如何抓取?簡單來說,就是與CSDN博客服務器簡歷tcp連接,然後發送HTTP請求,得到響應。頁面抓取過程如下圖所示:

主要流程:

  1. 解析域名(csdn.blog.net),得到服務器IP地址
  2. 與服務器端建立TCP連接
  3. 發送HTTP請求
  4. 得到服務器端響應,響應內容裏面含有請求頁面源代碼
  5. 解析網頁源代碼,得到所需要信息,如果需要抓取博主所有的文章,需要解析出每篇文章的URL
  6. 統計博主文章數,判斷是否有分頁,如果又分頁,則請求分頁內容,獲取分頁的文章URL
  7. 跳轉到第一步,請求每篇文章
  8. 把文章保存到本地
  9. 根據需求看是否對文章進行處理  

知道流程後,就可以着手編碼。先來看看我目前作出來的效果圖。



這裏並不只是把文章信息解析出來,也把每篇博客具體內容給存到本地了。存在以博主名命名的文件夾下,每篇文章存在以文章命名的html文件中。



具體實現:

一、解析域名

採用gethostbyname方法。函數聲明如下:

  1. #include<netdb.h>  
  2. struct hostent * gethostbyname(const char *hostname)  
執行成功,返回非空指針,失敗返回空指針,並設置h_errno,可以通過hstrerror方法查看h_errno對應的錯誤提示信息。

函數中用到的hostent結構體,如下所示:

  1. struct hostent  
  2. {  
  3.   char *h_name;         /* 查詢主機的規範名字 */  
  4.   char **h_aliases;     /* 別名 */  
  5.   int h_addrtype;       /* 地址類型  */  
  6.   int h_length;         /* 地址個數  */  
  7.   char **h_addr_list;       /* 所有的地址 */  
  8. };  

二、獲得IP地址後,與CSDN博客服務器建立TCP連接。

解析域名和建立TCP鏈接,我都放在一個自定義函數buildconnect裏面。每次需要建立連接,我只需要調用這個方法即可。代碼如下:

  1. /* 
  2. *功能:獲得CSDN博客IP地址,並與CSDN服務器建立TCP連接 
  3.     *參數:無 
  4. *返回值:非負描述字-成功,-1-出錯 
  5. */  
  6. int buildConnection() {  
  7.     int sockfd;  
  8.     static struct hostent *host = NULL;  
  9.     static struct sockaddr_in csdn_addr;  
  10.     if (host == NULL) {  
  11.         if ((host = gethostbyname(CSDN_BLOG_URL)) == NULL) {//獲取CSDN博客服務器IP地址  
  12.             fprintf(stderr, "gethostbyname error:%s\n", hstrerror(h_errno));  
  13.             exit(-1);  
  14.         }  
  15. #ifdef DEBUG  
  16.         printf("csdn ip:%s\n", inet_ntoa(*((struct in_addr *) host->h_addr_list[0])));  
  17. #endif  
  18.         bzero(&csdn_addr, sizeof (csdn_addr));  
  19.         csdn_addr.sin_family = AF_INET;  
  20.         csdn_addr.sin_port = htons(CSDN_BLOG_PORT);  
  21.         csdn_addr.sin_addr = *((struct in_addr *) host->h_addr_list[0]);  
  22.     }  
  23.     sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  24.     if (sockfd == -1) {  
  25.         fprintf(stderr, "socked error:%s\n", strerror(errno));  
  26.         exit(-1);  
  27.     }  
  28.     if (connect(sockfd, (struct sockaddr *) &csdn_addr, sizeof (csdn_addr)) == -1) {  
  29.         fprintf(stderr, "connect error:%s", strerror(errno));  
  30.         exit(-1);  
  31.     }  
  32.     return sockfd;  
  33. }  
不需要每一次都去解析域名,所以把域名存在一個static變量裏面。

三、發送HTTP請求

HTTP請求格式如下所示:

  1. "GET /lanyan822 HTTP/1.1\r\n  
  2. Accept:*/*\r\n  
  3. Accept-Language:zh-cn\r\n  
  4. User-Agent: Mozilla/4.0 (compatible;MSIE 5.01;Windows NT 5.0)\r\n  
  5. Host: blog.csdn.net:80\r\n  
  6. Connection: Close\r\n  
  7. \r\n  
說明:GET:表明是一個GET請求,還有POST請求(你可以模擬登陸,發送用戶名和密碼到服務端。不過現在CSDN登陸需要一個隨機碼驗證。這個不好辦)/lanyan822表示請求的頁面,HTTP1.1表示使用的版本。\r\n表示結束。
Accept:表示瀏覽器接受的MIME類型
Accept-Language:表示瀏覽器接受的語言類型
User-Agent:指瀏覽器的名字。呵呵,因爲是模擬瀏覽器發請求,所以這裏是假的
Host:服務器的域名和端口
Connection:用來告訴服務器是否可以維持固定的HTTP連接。HTTP/1.1使用Keep-Alive爲默認值,這樣,當瀏覽器需要多個文件時(比如一個HTML文件和相關的圖形文件),不需要每次都建立連接。這裏我每次請求頁面後,我都選擇關閉。
這裏需要注意的是:HTTP請求格式,千萬不能在裏面多寫空格什麼的。我之前一直請求頁面失敗就是因爲裏面多了空格。最後以\r\n結束。
  1. /* 
  2. *功能:發送HTTP請求,HTTP請求格式一定要正確,且不能有多餘的空格. 
  3. *參數:sockfd:套接字,requestParam:http請求路徑 
  4. *返回值:寫入套接口的字節數-成功,-1:失敗 
  5. */  
  6. int sendRequest(int sockfd, const char *requestParam) {  
  7.     char request[BUFFERLEN];  
  8.     int ret;  
  9.     bzero(request, sizeof (request));  
  10.     sprintf(request, "GET %s HTTP/1.1\r\n Accept:*/*\r\n Accept-Language:zh-cn\r\n"  
  11.             "User-Agent: Mozilla/4.0 (compatible;MSIE 5.01;Windows NT 5.0)\r\n"  
  12.             "Host: %s\r\n"  
  13.             "Connection: Close\r\n"  
  14.             "\r\n", requestParam, CSDN_BLOG_URL);  
  15. #ifdef DEBUG  
  16.     printf("請求HTTP格式:%s\n", request);  
  17. #endif  
  18.     ret = write(sockfd, request, sizeof (request));  
  19. #ifdef DEBUG  
  20.     printf("send %d data to server\n", ret);  
  21. #endif  
  22.     return ret;  
  23. }  

四、接受服務端響應,並存儲請求頁面

HTTP響應包括響應頭和所請求頁面的源代碼。

HTTP響應頭如下所示:

  1. HTTP/1.1 200 OK  
  2. Server: nginx/0.7.68  
  3. Date: Wed, 16 May 2012 06:28:28 GMT  
  4. Content-Type: text/html; charset=utf-8  
  5. Connection: close  
  6. Vary: Accept-Encoding  
  7. X-Powered-By: ASP.NET  
  8. Set-Cookie: uuid=344c2ad0-b060-448b-b75f-2c9dd308e5a5; expires=Thu, 17-May-2012 06:24:49 GMT; path=/  
  9. Set-Cookie: avh=yKfd8EgMOqw1YuvAzcgrbQ%3d%3d; expires=Wed, 16-May-2012 06:29:49 GMT; path=/  
  10. Cache-Control: private  
  11. Content-Length: 18202  
響應頭部也是以\r\n結束。所以可以通過\r\n\r\n來判斷響應頭部的結束位置。

實現源碼:

  1. /* 
  2. *功能:將服務端返回的html內容存入filePath中.這裏使用了select函數. 
  3. *參數:sockfd:套接字,filePath:文件存儲路徑 
  4. *返回值:讀入套接字字節數-成功,-1-失敗,-2請求頁面返回狀態值非200 
  5. */  
  6. int saveRequestHtml(int sockfd, const char *filePath) {  
  7.     int headerTag, ret, fileFd = -1,contentLen,count=0;  
  8.     char receiveBuf[BUFFERLEN];  
  9.     fd_set rset;  
  10.     struct timeval timeout;  
  11.     memset(&timeout, 0, sizeof (timeout));  
  12.     timeout.tv_sec = 60;  
  13.     timeout.tv_usec = 0;  
  14.     char *first, *last,*ok_loc,*pContentLenStart,*pContentLenEnd;  
  15.     while (TRUE) {  
  16.         FD_SET(sockfd, &rset);  
  17.         ret = select(sockfd + 1, &rset, NULL, NULL, &timeout);  
  18.         if (ret == 0) {  
  19.             fprintf(stderr, "select time out:%s\n", strerror(errno));  
  20.             return ret;  
  21.         } else  
  22.             if (ret == -1) {  
  23.             fprintf(stderr, "select error :%s\n", strerror(errno));  
  24.             return ret;  
  25.         }  
  26.         headerTag = 0;  
  27.         if (FD_ISSET(sockfd, &rset)) {  
  28.   
  29.   
  30.             while (ret = read(sockfd, receiveBuf, BUFFERLEN - 1)) {  
  31.                 if (headerTag == 0) {  
  32.                     if (access(filePath, F_OK) == 0) {  
  33.                         if (remove(filePath) == -1)  
  34.                             fprintf(stderr, "remove error:%s\n", strerror(errno));  
  35.                     } else {  
  36. #ifdef DEBUG  
  37.                         printf("%s not exist\n", filePath);  
  38. #endif  
  39.                     }                  
  40.                     receiveBuf[ret] = '\0';  
  41.                     first = strstr(receiveBuf, "\r\n\r\n");//服務端返回消息頭部和網頁html內容.消息頭部也是以\r\n\r\n結尾.  
  42.                     if (first != 0) {          
  43.                         last = first + strlen("\r\n\r\n");  
  44.                         ok_loc=strstr(receiveBuf,"OK");//如果請求成功,狀態碼是200,並且有OK  
  45.                         if(ok_loc!=0)  
  46.                         {  
  47. #ifdef DEBUG  
  48.                             printf("頁面請求成功\n");  
  49. #endif  
  50.                             fileFd = open(filePath, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);  
  51.                             if (fileFd == -1) {  
  52.                                 fprintf(stderr, "open error:%s\n", strerror(errno));  
  53.                                 return -1;  
  54.                             }  
  55.                             pContentLenStart=strstr(receiveBuf,CONTENT_LENGTH);//這裏是爲了獲取HTTP響應頭content-length大小。  
  56.                             if(pContentLenStart!=0)  
  57.                             {  
  58.                                 pContentLenEnd=strstr(pContentLenStart+strlen(CONTENT_LENGTH),"\r\n");  
  59.                                 if(pContentLenEnd!=0)  
  60.                                 {  
  61.                                     contentLen=myatoi(pContentLenStart,pContentLenEnd);  
  62. #ifdef DEBUG  
  63.                                     printf("content-length:%d\n",contentLen);  
  64. #endif  
  65.                                     count+= write(fileFd, last, ret - (last - receiveBuf));  
  66.                                     headerTag = 1;  
  67.                                 }else  
  68.                                     return -1;  
  69.                             }else  
  70.                             {  
  71.                                 return -1;  
  72.                             }  
  73.                         }else  
  74.                         {  
  75.                             return -2;//頁面請求失敗。  
  76.                         }  
  77.                          
  78.                     }  
  79. #ifdef DEBUG  
  80.                     printf("%s\n", receiveBuf);  
  81. #endif  
  82.                 } else {  
  83.                    count+= write(fileFd, receiveBuf, ret);  
  84.                 }  
  85.             }  
  86.             close(fileFd);  
  87.         }  
  88.         break;  
  89.     }  
  90.     if(count!=contentLen)  
  91.     {  
  92.         printf("接受長度與HTTP響應頭長度不一致\n");  
  93.         return -1;  
  94.     }  
  95.     return count;  
  96. }  

五、解析網頁源代碼,得到所需要信息

我主要解析了博客的文章名,文章URL,訪問次數,排名,積分,原創文章數,轉載文章數,翻譯文章數,評論數。
源代碼解析是按照所需要的信息在源代碼中出現的順序依次解析,先出現文章名,接着是文章的評論,發表日期等信息,接着解析博主的積分,等級等,最後解析博主發表的文章數。
解析用的最多的是strstr函數。
  1. #include<string.h>  
  2. char *strstr (char *haystack, const char *needle);  
函數功能:查找needle在haystack中第一次出現的地址,查找成功,返回第一次出現的地址,查找失敗返回0.類似於c++ string的find_first_of函數。

信息解析出來,需要存儲下來。主要是存在自定義的數據結構裏面。每一頁(最多50篇文章)存儲在struct Articles結構體裏面,文章信息則存入struct ArticleInfo裏面。頁面存儲結構如下圖所示:

自定義的結構體:

  1. struct BloggerInfo  
  2. {  
  3.     int visits;//訪問次數  
  4.     int integral;//積分  
  5.     int ranking;//排名  
  6.     int artical_original;//原創文章數  
  7.     int artical_reproduce;//轉載文章數  
  8.     int artical_translation;//翻譯文章數  
  9.     int comments;//評論  
  10. };  
  11.   
  12. struct ArticleInfo  
  13. {  
  14.     char articleName[SMALLLEN];//文章標題  
  15.     char URL[SMALLLEN];//URL  
  16.     char createDate[25];//創建時間  
  17.     int visits;//訪問時間  
  18.     int comments;//評論次數  
  19.     struct ArticleInfo *next;//下一篇文章地址  
  20. };  
  21.   
  22. struct Articles  
  23. {  
  24.     int page;//頁數  
  25.     struct Articles * pageNext;//下一頁所在地址  
  26.     struct ArticleInfo *firstArticle;//該頁第一篇文章地址  
  27.     struct ArticleInfo *currentArticle;//插入文章時使用,表示插入時的最後一篇文章  
  28. }; 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章