來源:【51cto.com】
***近一直在學(xué)習(xí)stevens的unix網(wǎng)絡(luò)編程,對(duì)于網(wǎng)絡(luò)通信有了一定的認(rèn)識(shí),所以也想練練手。聊天程序之前用winsock做過,這次不想做重復(fù)的。之前看到一哥們寫過windows下抓取貓撲的帖子,我覺得抓頁面也是一個(gè)不錯(cuò)想法。我也喜歡逛貓撲,有時(shí)候也去追追里面寫的文章,貓撲帖子少了一個(gè)很重要的功能,就是只看樓主的帖子。貓撲水人很多,容易把樓主的帖子淹沒在大海里面。
查看了一下貓撲帖子的網(wǎng)頁源代碼,帖子內(nèi)容介于和
之間,只需要解析這段內(nèi)容,就能得到自己想要的東西。不過里面東西比較多,比較雜,還是先找一個(gè)簡單頁面抓取試試。csdn博客相對(duì)來說就是個(gè)不錯(cuò)的選擇,******沒廣告,內(nèi)容不算很多,第二,代碼風(fēng)格很好。抓CSDN的頁面無非獲得博主名,文章名字和URL等,如果想獲得更多的信息,可以把博主的排名,評(píng)論數(shù)抓取下來。
自定義的結(jié)構(gòu)體:
[cpp] view plaincopyprint?
struct BloggerInfo
{
int visits;//訪問次數(shù)
int integral;//積分
int ranking;//排名
int artical_original;//原創(chuàng)文章數(shù)
int artical_reproduce;//轉(zhuǎn)載文章數(shù)
int artical_translation;//翻譯文章數(shù)
int comments;//評(píng)論
};
struct ArticleInfo
{
char articleName[SMALLLEN];//文章標(biāo)題
char URL[SMALLLEN];//URL
char createDate[25];//創(chuàng)建時(shí)間
int visits;//訪問時(shí)間
int comments;//評(píng)論次數(shù)
struct ArticleInfo *next;//下一篇文章地址
};
struct Articles
{
int page;//頁數(shù)
struct Articles * pageNext;//下一頁所在地址
struct ArticleInfo *firstArticle;//該頁******篇文章地址
struct ArticleInfo *currentArticle;//插入文章時(shí)使用,表示插入時(shí)的***后一篇文章
};
下面簡單分析一下CSDN博客源代碼。
博主標(biāo)題:
[html] view plaincopyprint?
編程小子的專欄
鍥而舍之,朽木不折;鍥而不舍,金石可鏤
文章標(biāo)題和URL:
[html] view plaincopyprint?
ubuntu11.10搭建git服務(wù)器
文章訪問次數(shù),評(píng)論次數(shù)等:
[html] view plaincopyprint?
博客統(tǒng)計(jì)信息:
[html] view plaincopyprint?
[html] view plaincopyprint?
-
原創(chuàng):13篇
-
轉(zhuǎn)載:2篇
-
譯文:0篇
-
評(píng)論:1條
從上面貼出的HTML可以看出,所需要的信息都在某一個(gè)id下,每個(gè)id是******的,這對(duì)解析是很有利的。我們只需要抓取到網(wǎng)頁,分析相應(yīng)內(nèi)容,得到想要的信息即可。
在確定CSDN博客是可以抓取后,就可以著手抓取。如何抓取?簡單來說,就是與CSDN博客服務(wù)器簡歷tcp連接,然后發(fā)送HTTP請(qǐng)求,得到響應(yīng)。頁面抓取過程如下圖所示:
主要流程:
解析域名(csdn.blog.net),得到服務(wù)器IP地址
與服務(wù)器端建立TCP連接
發(fā)送HTTP請(qǐng)求
得到服務(wù)器端響應(yīng),響應(yīng)內(nèi)容里面含有請(qǐng)求頁面源代碼
解析網(wǎng)頁源代碼,得到所需要信息,如果需要抓取博主所有的文章,需要解析出每篇文章的URL
統(tǒng)計(jì)博主文章數(shù),判斷是否有分頁,如果又分頁,則請(qǐng)求分頁內(nèi)容,獲取分頁的文章URL
跳轉(zhuǎn)到******步,請(qǐng)求每篇文章
把文章保存到本地
根據(jù)需求看是否對(duì)文章進(jìn)行處理
知道流程后,就可以著手編碼。先來看看我目前作出來的效果圖。
這里并不只是把文章信息解析出來,也把每篇博客具體內(nèi)容給存到本地了。存在以博主名命名的文件夾下,每篇文章存在以文章命名的html文件中。
具體實(shí)現(xiàn):
一、解析域名
采用gethostbyname方法。函數(shù)聲明如下:
[cpp] view plaincopyprint?
#include
struct hostent * gethostbyname(const char *hostname)
執(zhí)行成功,返回非空指針,失敗返回空指針,并設(shè)置h_errno,可以通過hstrerror方法查看h_errno對(duì)應(yīng)的錯(cuò)誤提示信息。
函數(shù)中用到的hostent結(jié)構(gòu)體,如下所示:
[cpp] view plaincopyprint?
struct hostent
{
char *h_name; /* 查詢主機(jī)的規(guī)范名字 */
char **h_aliases; /* 別名 */
int h_addrtype; /* 地址類型 */
int h_length; /* 地址個(gè)數(shù) */
char **h_addr_list; /* 所有的地址 */
};
二、獲得IP地址后,與CSDN博客服務(wù)器建立TCP連接。
解析域名和建立TCP鏈接,我都放在一個(gè)自定義函數(shù)buildconnect里面。每次需要建立連接,我只需要調(diào)用這個(gè)方法即可。代碼如下:
[cpp] view plaincopyprint?
/*
*功能:獲得CSDN博客IP地址,并與CSDN服務(wù)器建立TCP連接
*參數(shù):無
*返回值:非負(fù)描述字-成功,-1-出錯(cuò)
*/
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博客服務(wù)器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;
}
不需要每一次都去解析域名,所以把域名存在一個(gè)static變量里面。
三、發(fā)送HTTP請(qǐng)求
HTTP請(qǐng)求格式如下所示:
[cpp] view plaincopyprint?
"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
說明:GET:表明是一個(gè)GET請(qǐng)求,還有POST請(qǐng)求(你可以模擬登陸,發(fā)送用戶名和密碼到服務(wù)端。不過現(xiàn)在CSDN登陸需要一個(gè)隨機(jī)碼驗(yàn)證。這個(gè)不好辦)/lanyan822表示請(qǐng)求的頁面,HTTP1.1表示使用的版本。\r\n表示結(jié)束。
Accept:表示瀏覽器接受的MIME類型
Accept-Language:表示瀏覽器接受的語言類型
User-Agent:指瀏覽器的名字。呵呵,因?yàn)槭悄M瀏覽器發(fā)請(qǐng)求,所以這里是假的
Host:服務(wù)器的域名和端口
Connection:用來告訴服務(wù)器是否可以維持固定的HTTP連接。HTTP/1.1使用Keep-Alive為默認(rèn)值,這樣,當(dāng)瀏覽器需要多個(gè)文件時(shí)(比如一個(gè)HTML文件和相關(guān)的圖形文件),不需要每次都建立連接。這里我每次請(qǐng)求頁面后,我都選擇關(guān)閉。
這里需要注意的是:HTTP請(qǐng)求格式,千萬不能在里面多寫空格什么的。我之前一直請(qǐng)求頁面失敗就是因?yàn)槔锩娑嗔丝崭瘛?**后以\r\n結(jié)束。
[cpp] view plaincopyprint?
/*
*功能:發(fā)送HTTP請(qǐng)求,HTTP請(qǐng)求格式一定要正確,且不能有多余的空格.
*參數(shù):sockfd:套接字,requestParam:http請(qǐng)求路徑
*返回值:寫入套接口的字節(jié)數(shù)-成功,-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("請(qǐng)求HTTP格式:%s\n", request);
#endif
ret = write(sockfd, request, sizeof (request));
#ifdef DEBUG
printf("send %d data to server\n", ret);
#endif
return ret;
}
四、接受服務(wù)端響應(yīng),并存儲(chǔ)請(qǐng)求頁面
HTTP響應(yīng)包括響應(yīng)頭和所請(qǐng)求頁面的源代碼。
HTTP響應(yīng)頭如下所示:
[cpp] view plaincopyprint?
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
響應(yīng)頭部也是以\r\n結(jié)束。所以可以通過\r\n\r\n來判斷響應(yīng)頭部的結(jié)束位置。
實(shí)現(xiàn)源碼:
[cpp] view plaincopyprint?
/*
*功能:將服務(wù)端返回的html內(nèi)容存入filePath中.這里使用了select函數(shù).
*參數(shù):sockfd:套接字,filePath:文件存儲(chǔ)路徑
*返回值:讀入套接字字節(jié)數(shù)-成功,-1-失敗,-2請(qǐng)求頁面返回狀態(tài)值非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");//服務(wù)端返回消息頭部和網(wǎng)頁html內(nèi)容.消息頭部也是以\r\n\r\n結(jié)尾.
if (first != 0) {
last = first + strlen("\r\n\r\n");
ok_loc=strstr(receiveBuf,"OK");//如果請(qǐng)求成功,狀態(tài)碼是200,并且有OK
if(ok_loc!=0)
{
#ifdef DEBUG
printf("頁面請(qǐng)求成功\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響應(yīng)頭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;//頁面請(qǐng)求失敗。
}
}
#ifdef DEBUG
printf("%s\n", receiveBuf);
#endif
} else {
count+= write(fileFd, receiveBuf, ret);
}
}
close(fileFd);
}
break;
}
if(count!=contentLen)
{
printf("接受長度與HTTP響應(yīng)頭長度不一致\n");
return -1;
}
return count;
}
五、解析網(wǎng)頁源代碼,得到所需要信息
我主要解析了博客的文章名,文章URL,訪問次數(shù),排名,積分,原創(chuàng)文章數(shù),轉(zhuǎn)載文章數(shù),翻譯文章數(shù),評(píng)論數(shù)。
源代碼解析是按照所需要的信息在源代碼中出現(xiàn)的順序依次解析,先出現(xiàn)文章名,接著是文章的評(píng)論,發(fā)表日期等信息,接著解析博主的積分,等級(jí)等,***后解析博主發(fā)表的文章數(shù)。
解析用的***多的是strstr函數(shù)。
[cpp] view plaincopyprint?
#include
char *strstr (char *haystack, const char *needle);
函數(shù)功能:查找needle在haystack中******次出現(xiàn)的地址,查找成功,返回******次出現(xiàn)的地址,查找失敗返回0.類似于c++ string的find_first_of函數(shù)。
信息解析出來,需要存儲(chǔ)下來。主要是存在自定義的數(shù)據(jù)結(jié)構(gòu)里面。每一頁(***多50篇文章)存儲(chǔ)在struct Articles結(jié)構(gòu)體里面,文章信息則存入struct ArticleInfo里面。頁面存儲(chǔ)結(jié)構(gòu)如下圖所示: