查看了一下貓撲帖子的網頁源代碼,帖子內容介於<divclass="box2 js-reply"data-rid="*">和</div>之間,只需要解析這段內容,就能得到自己想要的東西。不過裏面東西比較多,比較雜,還是先找一個簡單頁面抓取試試。csdn博客相對來說就是個不錯的選擇,第一沒廣告,內容不算很多,第二,代碼風格很好。抓CSDN的頁面無非獲得博主名,文章名字和URL等,如果想獲得更多的信息,可以把博主的排名,評論數抓取下來。
下面簡單分析一下CSDN博客源代碼。
博主標題:
- <div id="blog_title">
- <h1>
- <a href="/lanyan822">編程小子的專欄</a></h1>
- <h2>鍥而舍之,朽木不折;鍥而不捨,金石可鏤</h2>
- <div class="clear">
- </div>
- </div>
- <span class="link_title"><a href="/lanyan822/article/details/7549916">
- ubuntu11.10搭建git服務器
- </a>
- <div class="article_manage">
- <span class="link_postdate">2012-05-14 15:09</span>
- <span class="link_view" title="閱讀次數"><a href="/lanyan822/article/details/7549916" title="閱讀次數">閱讀</a>(21)</span>
- <span class="link_comments" title="評論次數"><a href="/lanyan822/article/details/7549916#comments" title="評論次數">評論</a>(0)</span>
- </div>
- <ul id="blog_rank">
- <li>訪問:<span>1218次</span></li>
- <li>積分:<span>164分</span></li>
- <li>排名:<span>千里之外</span></li>
- </ul>
- <ul id="blog_statistics">
- <li>原創:<span>13篇</span></li>
- <li>轉載:<span>2篇</span></li>
- <li>譯文:<span>0篇</span></li>
- <li>評論:<span>1條</span></li>
- </ul>
從上面貼出的HTML可以看出,所需要的信息都在某一個id下,每個id是唯一的,這對解析是很有利的。我們只需要抓取到網頁,分析相應內容,得到想要的信息即可。
在確定CSDN博客是可以抓取後,就可以着手抓取。如何抓取?簡單來說,就是與CSDN博客服務器簡歷tcp連接,然後發送HTTP請求,得到響應。頁面抓取過程如下圖所示:
主要流程:
- 解析域名(csdn.blog.net),得到服務器IP地址
- 與服務器端建立TCP連接
- 發送HTTP請求
- 得到服務器端響應,響應內容裏面含有請求頁面源代碼
- 解析網頁源代碼,得到所需要信息,如果需要抓取博主所有的文章,需要解析出每篇文章的URL
- 統計博主文章數,判斷是否有分頁,如果又分頁,則請求分頁內容,獲取分頁的文章URL
- 跳轉到第一步,請求每篇文章
- 把文章保存到本地
- 根據需求看是否對文章進行處理
知道流程後,就可以着手編碼。先來看看我目前作出來的效果圖。
這裏並不只是把文章信息解析出來,也把每篇博客具體內容給存到本地了。存在以博主名命名的文件夾下,每篇文章存在以文章命名的html文件中。
具體實現:
一、解析域名
採用gethostbyname方法。函數聲明如下:
執行成功,返回非空指針,失敗返回空指針,並設置h_errno,可以通過hstrerror方法查看h_errno對應的錯誤提示信息。
- #include<netdb.h>
- struct hostent * gethostbyname(const char *hostname)
函數中用到的hostent結構體,如下所示:
- struct hostent
- {
- char *h_name; /* 查詢主機的規範名字 */
- char **h_aliases; /* 別名 */
- int h_addrtype; /* 地址類型 */
- int h_length; /* 地址個數 */
- char **h_addr_list; /* 所有的地址 */
- };
二、獲得IP地址後,與CSDN博客服務器建立TCP連接。
解析域名和建立TCP鏈接,我都放在一個自定義函數buildconnect裏面。每次需要建立連接,我只需要調用這個方法即可。代碼如下:
不需要每一次都去解析域名,所以把域名存在一個static變量裏面。
- /*
- *功能:獲得CSDN博客IP地址,並與CSDN服務器建立TCP連接
- *參數:無
- *返回值:非負描述字-成功,-1-出錯
- */
- int buildConnection() {
- int sockfd;
- static struct hostent *host = NULL;
- static struct sockaddr_in csdn_addr;
- if (host == NULL) {
- if ((host = gethostbyname(CSDN_BLOG_URL)) == NULL) {//獲取CSDN博客服務器IP地址
- fprintf(stderr, "gethostbyname error:%s\n", hstrerror(h_errno));
- exit(-1);
- }
- #ifdef DEBUG
- printf("csdn ip:%s\n", inet_ntoa(*((struct in_addr *) host->h_addr_list[0])));
- #endif
- bzero(&csdn_addr, sizeof (csdn_addr));
- csdn_addr.sin_family = AF_INET;
- csdn_addr.sin_port = htons(CSDN_BLOG_PORT);
- csdn_addr.sin_addr = *((struct in_addr *) host->h_addr_list[0]);
- }
- sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd == -1) {
- fprintf(stderr, "socked error:%s\n", strerror(errno));
- exit(-1);
- }
- if (connect(sockfd, (struct sockaddr *) &csdn_addr, sizeof (csdn_addr)) == -1) {
- fprintf(stderr, "connect error:%s", strerror(errno));
- exit(-1);
- }
- return sockfd;
- }
三、發送HTTP請求
HTTP請求格式如下所示:
說明:GET:表明是一個GET請求,還有POST請求(你可以模擬登陸,發送用戶名和密碼到服務端。不過現在CSDN登陸需要一個隨機碼驗證。這個不好辦)/lanyan822表示請求的頁面,HTTP1.1表示使用的版本。\r\n表示結束。
- "GET /lanyan822 HTTP/1.1\r\n
- Accept:*/*\r\n
- Accept-Language:zh-cn\r\n
- User-Agent: Mozilla/4.0 (compatible;MSIE 5.01;Windows NT 5.0)\r\n
- Host: blog.csdn.net:80\r\n
- Connection: Close\r\n
- \r\n
Accept:表示瀏覽器接受的MIME類型
Accept-Language:表示瀏覽器接受的語言類型
User-Agent:指瀏覽器的名字。呵呵,因爲是模擬瀏覽器發請求,所以這裏是假的
Host:服務器的域名和端口
Connection:用來告訴服務器是否可以維持固定的HTTP連接。HTTP/1.1使用Keep-Alive爲默認值,這樣,當瀏覽器需要多個文件時(比如一個HTML文件和相關的圖形文件),不需要每次都建立連接。這裏我每次請求頁面後,我都選擇關閉。
這裏需要注意的是:HTTP請求格式,千萬不能在裏面多寫空格什麼的。我之前一直請求頁面失敗就是因爲裏面多了空格。最後以\r\n結束。
- /*
- *功能:發送HTTP請求,HTTP請求格式一定要正確,且不能有多餘的空格.
- *參數:sockfd:套接字,requestParam:http請求路徑
- *返回值:寫入套接口的字節數-成功,-1:失敗
- */
- int sendRequest(int sockfd, const char *requestParam) {
- char request[BUFFERLEN];
- int ret;
- bzero(request, sizeof (request));
- sprintf(request, "GET %s HTTP/1.1\r\n Accept:*/*\r\n Accept-Language:zh-cn\r\n"
- "User-Agent: Mozilla/4.0 (compatible;MSIE 5.01;Windows NT 5.0)\r\n"
- "Host: %s\r\n"
- "Connection: Close\r\n"
- "\r\n", requestParam, CSDN_BLOG_URL);
- #ifdef DEBUG
- printf("請求HTTP格式:%s\n", request);
- #endif
- ret = write(sockfd, request, sizeof (request));
- #ifdef DEBUG
- printf("send %d data to server\n", ret);
- #endif
- return ret;
- }
四、接受服務端響應,並存儲請求頁面
HTTP響應包括響應頭和所請求頁面的源代碼。
HTTP響應頭如下所示:
響應頭部也是以\r\n結束。所以可以通過\r\n\r\n來判斷響應頭部的結束位置。
- HTTP/1.1 200 OK
- Server: nginx/0.7.68
- Date: Wed, 16 May 2012 06:28:28 GMT
- Content-Type: text/html; charset=utf-8
- Connection: close
- Vary: Accept-Encoding
- X-Powered-By: ASP.NET
- Set-Cookie: uuid=344c2ad0-b060-448b-b75f-2c9dd308e5a5; expires=Thu, 17-May-2012 06:24:49 GMT; path=/
- Set-Cookie: avh=yKfd8EgMOqw1YuvAzcgrbQ%3d%3d; expires=Wed, 16-May-2012 06:29:49 GMT; path=/
- Cache-Control: private
- Content-Length: 18202
實現源碼:
- /*
- *功能:將服務端返回的html內容存入filePath中.這裏使用了select函數.
- *參數:sockfd:套接字,filePath:文件存儲路徑
- *返回值:讀入套接字字節數-成功,-1-失敗,-2請求頁面返回狀態值非200
- */
- int saveRequestHtml(int sockfd, const char *filePath) {
- int headerTag, ret, fileFd = -1,contentLen,count=0;
- char receiveBuf[BUFFERLEN];
- fd_set rset;
- struct timeval timeout;
- memset(&timeout, 0, sizeof (timeout));
- timeout.tv_sec = 60;
- timeout.tv_usec = 0;
- char *first, *last,*ok_loc,*pContentLenStart,*pContentLenEnd;
- while (TRUE) {
- FD_SET(sockfd, &rset);
- ret = select(sockfd + 1, &rset, NULL, NULL, &timeout);
- if (ret == 0) {
- fprintf(stderr, "select time out:%s\n", strerror(errno));
- return ret;
- } else
- if (ret == -1) {
- fprintf(stderr, "select error :%s\n", strerror(errno));
- return ret;
- }
- headerTag = 0;
- if (FD_ISSET(sockfd, &rset)) {
- while (ret = read(sockfd, receiveBuf, BUFFERLEN - 1)) {
- if (headerTag == 0) {
- if (access(filePath, F_OK) == 0) {
- if (remove(filePath) == -1)
- fprintf(stderr, "remove error:%s\n", strerror(errno));
- } else {
- #ifdef DEBUG
- printf("%s not exist\n", filePath);
- #endif
- }
- receiveBuf[ret] = '\0';
- first = strstr(receiveBuf, "\r\n\r\n");//服務端返回消息頭部和網頁html內容.消息頭部也是以\r\n\r\n結尾.
- if (first != 0) {
- last = first + strlen("\r\n\r\n");
- ok_loc=strstr(receiveBuf,"OK");//如果請求成功,狀態碼是200,並且有OK
- if(ok_loc!=0)
- {
- #ifdef DEBUG
- printf("頁面請求成功\n");
- #endif
- fileFd = open(filePath, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
- if (fileFd == -1) {
- fprintf(stderr, "open error:%s\n", strerror(errno));
- return -1;
- }
- pContentLenStart=strstr(receiveBuf,CONTENT_LENGTH);//這裏是爲了獲取HTTP響應頭content-length大小。
- if(pContentLenStart!=0)
- {
- pContentLenEnd=strstr(pContentLenStart+strlen(CONTENT_LENGTH),"\r\n");
- if(pContentLenEnd!=0)
- {
- contentLen=myatoi(pContentLenStart,pContentLenEnd);
- #ifdef DEBUG
- printf("content-length:%d\n",contentLen);
- #endif
- count+= write(fileFd, last, ret - (last - receiveBuf));
- headerTag = 1;
- }else
- return -1;
- }else
- {
- return -1;
- }
- }else
- {
- return -2;//頁面請求失敗。
- }
- }
- #ifdef DEBUG
- printf("%s\n", receiveBuf);
- #endif
- } else {
- count+= write(fileFd, receiveBuf, ret);
- }
- }
- close(fileFd);
- }
- break;
- }
- if(count!=contentLen)
- {
- printf("接受長度與HTTP響應頭長度不一致\n");
- return -1;
- }
- return count;
- }
五、解析網頁源代碼,得到所需要信息
我主要解析了博客的文章名,文章URL,訪問次數,排名,積分,原創文章數,轉載文章數,翻譯文章數,評論數。源代碼解析是按照所需要的信息在源代碼中出現的順序依次解析,先出現文章名,接着是文章的評論,發表日期等信息,接着解析博主的積分,等級等,最後解析博主發表的文章數。解析用的最多的是strstr函數。函數功能:查找needle在haystack中第一次出現的地址,查找成功,返回第一次出現的地址,查找失敗返回0.類似於c++ string的find_first_of函數。
- #include<string.h>
- char *strstr (char *haystack, const char *needle);
信息解析出來,需要存儲下來。主要是存在自定義的數據結構裏面。每一頁(最多50篇文章)存儲在struct Articles結構體裏面,文章信息則存入struct ArticleInfo裏面。頁面存儲結構如下圖所示:自定義的結構體:
- struct BloggerInfo
- {
- int visits;//訪問次數
- int integral;//積分
- int ranking;//排名
- int artical_original;//原創文章數
- int artical_reproduce;//轉載文章數
- int artical_translation;//翻譯文章數
- int comments;//評論
- };
- struct ArticleInfo
- {
- char articleName[SMALLLEN];//文章標題
- char URL[SMALLLEN];//URL
- char createDate[25];//創建時間
- int visits;//訪問時間
- int comments;//評論次數
- struct ArticleInfo *next;//下一篇文章地址
- };
- struct Articles
- {
- int page;//頁數
- struct Articles * pageNext;//下一頁所在地址
- struct ArticleInfo *firstArticle;//該頁第一篇文章地址
- struct ArticleInfo *currentArticle;//插入文章時使用,表示插入時的最後一篇文章
- };