最近一直在学习stevens的unix网络编程,对于网络通信有了一定的认识,所以也想练练手。聊天程序之前用winsock做过,这次不想做重复的。 之前看到一哥们写过windows下抓取猫扑的帖子,我觉得抓页面也是一个不错想法。我也喜欢逛猫扑,有时候也去追追里面写的文章,猫扑帖子少了一个很重 要的功能,就是只看楼主的帖子。猫扑水人很多,容易把楼主的帖子淹没在大海里面。
查看了一下猫扑帖子的网页源代码,帖子内容介于<divclass=”box2 js-reply”data-rid=”*”& gt;和</div>之间,只需要解析这段内容,就能得到自己想要的东西。不过里面东西比较多,比较杂,还是先找一个简单页面抓取试试。 csdn博客相对来说就是个不错的选择,第一没广告,内容不算很多,第二,代码风格很好。抓CSDN的页面无非获得博主名,文章名字和URL等,如果想获 得更多的信息,可以把博主的排名,评论数抓取下来。
下面简单分析一下CSDN博客源代码。
博主标题:
1 2 3 4 5 6 7 |
<div id="blog_title"> <h1> <a href="/lanyan822">编程小子的专栏</a></h1> <h2>锲而舍之,朽木不折;锲而不舍,金石可镂</h2> <div class="clear"> </div> </div> |
文章标题和URL:
1 2 3 |
<span class="link_title"><a href="/lanyan822/article/details/7549916"> ubuntu11.10搭建git服务器 </a> |
文章访问次数,评论次数等:
1 2 3 4 5 |
<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> |
博客统计信息:
1 2 3 4 5 |
<ul id="blog_rank"> <li>访问:<span>1218次</span></li> <li>积分:<span>164分</span></li> <li>排名:<span>千里之外</span></li> </ul> |
1 2 3 4 5 6 |
<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方法。函数声明如下:
1 2 |
#include<netdb.h> struct hostent * gethostbyname(const char *hostname) |
执行成功,返回非空指针,失败返回空指针,并设置h_errno,可以通过hstrerror方法查看h_errno对应的错误提示信息。
函数中用到的hostent结构体,如下所示:
1 2 3 4 5 6 7 8 |
struct hostent { char *h_name; /* 查询主机的规范名字 */ char **h_aliases; /* 别名 */ int h_addrtype; /* 地址类型 */ int h_length; /* 地址个数 */ char **h_addr_list; /* 所有的地址 */ }; |
二、获得IP地址后,与CSDN博客服务器建立TCP连接。
解析域名和建立TCP链接,我都放在一个自定义函数buildconnect里面。每次需要建立连接,我只需要调用这个方法即可。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* *功能:获得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; } |
不需要每一次都去解析域名,所以把域名存在一个static变量里面。
三、发送HTTP请求
HTTP请求格式如下所示:
1 2 3 4 5 6 7 |
"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:表明是一个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 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* *功能:发送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响应头如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
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 |
响应头部也是以\r\n结束。所以可以通过\r\n\r\n来判断响应头部的结束位置。
实现源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
/* *功能:将服务端返回的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; } |
五、解析网页源代码,得到所需要信息
1 2 |
#include<string.h> char *strstr (char *haystack, const char *needle); |
函数功能:查找needle在haystack中第一次出现的地址,查找成功,返回第一次出现的地址,查找失败返回0.类似于c++ string的find_first_of函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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;//插入文章时使用,表示插入时的最后一篇文章 }; |
续:
五、解析网页源代码,得到所需要信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#define ARTICLE_TITLE ("<div class=\"article_title\">") #define ARTICLE_MANAGE ("<div class=\"article_manage\">") #define BLOG_TITLE ("<div class=\"blog_title\">") #define URL_LINK ("<a href=") #define LINK_TITLE ("<span class=\"link_title\"><a href=") #define LINK_TITLE_END ("</a></span>") #define LINK_VIEW ("<span class=\"link_view\"") #define LINK_COMMENTS ("<span class=\"link_comments\"") #define LINK_POSTDATE ("<span class=\"link_postdate\">") #define BLOG_RANK ("<ul id=\"blog_rank\">") #define BLOG_STATISTICES (" <ul id=\"blog_statistics\">") #define URL_END ("</a>") #define DIV_END ("</div>") #define SPAN ("<span>") #define SPAN_END ("</span>") #define UL_END ("</ul>") |
1、从保存的首页中读1024字节到数组。
2、strcat到2048个字节的数组,多定义一个2048字节的数组,就是为了解决查找标签有可能只读到一部分的情况。
3、判断状态,自定义5个状态
1 2 3 4 5 |
#define TAG_RESOLV_ARTICLE 0 //解析文章标题、URL #define TAG_RESOLV_ARTICLE_STATIS 1//解析文章访问信息 #define TAG_RESOLV_RANK 2//解析博主博客的访问信息 #define TAG_RESOLV_STATIS 3//解析博主文章统计 #define TAG_RESOLV_END 4//解析结束 |
4、根据状态查找相应的标签位置。 如果状态为TAG_RESOLV_RANK,则查找<ulid=”blog_rank”>和</ul>是否在数组中。只有两个标签同时在数组中,才解析标签所包含的有用信息。当<ulid=”blog_rank”>存在,而</ul>不存在时,则丢掉<ulid=”blog_rank”>之前的数据,在读1024字节数据后在解析。引用我自己写的一段代码来分析一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/* *功能:解析博客排名,访问量等。 */ int resolvBlogRank(char *buf, int len, struct BloggerInfo *blogger, int *tag) { char *blog_rank_start, *blog_rank_end, *span_start, *span_end; char tmpbuf[BUFFERLEN * 2]; int roll_back_loc,buf_len; bzero(tmpbuf, sizeof (tmpbuf)); blog_rank_start = strstr(buf, BLOG_RANK);//找到BLOG_RANK第一次出现的位置(呵呵,只会出现一次) if (blog_rank_start == 0) {//如果buf数组里面没有出现BLOG_RANK,则只保留buf数组最后200个字节。为什么是200?主要是因为BLOG_RANK所包括的字符长度目前不会超过200哈。 buf_len=strlen(buf); roll_back_loc=buf_len-200>0?buf_len-200:0; strcat(tmpbuf, buf + roll_back_loc); bzero(buf, len); strcat(buf, tmpbuf); } else {//BLOG_RANK出现在buf里面 blog_rank_end = strstr(blog_rank_start + strlen(BLOG_RANK), UL_END);//找与<span style="white-space: pre-wrap; ">BLOG_RANK对应的结尾标签,这里是</ul></span> if (blog_rank_end == 0) {//UL标签没有找到,则保留BLOG_RANK之后的数据 strcat(tmpbuf, blog_rank_start); bzero(buf, len); strcat(buf, tmpbuf); } else {//UL标签找到 span_start = strstr(blog_rank_start + strlen(BLOG_RANK), SPAN); span_end = strstr(span_start + strlen(SPAN), SPAN_END); blogger->visits = myatoi(span_start + strlen(SPAN), span_end);//myatoi函数是我自己写的,用于将字符串转换为整数。如“第10000名”=》10000 span_start = strstr(span_end + strlen(SPAN_END), SPAN); span_end = strstr(span_start + strlen(SPAN), SPAN_END); blogger->integral = myatoi(span_start + strlen(SPAN), span_end); span_start = strstr(span_end + strlen(SPAN_END), SPAN); span_end = strstr(span_start + strlen(SPAN), SPAN_END); blogger->ranking = myatoi(span_start + strlen(SPAN), span_end); *tag = TAG_RESOLV_STATIS; strcat(tmpbuf, blog_rank_end + strlen(UL_END)); bzero(buf, len); strcat(buf, tmpbuf); } } return 0; } |
5、循环,直到所解析的内容解析完毕
六、根据解析出来的文章URL,发送HTTP请求,保存返回的网页源代码
接下来的工作:
发表评论
要发表评论,您必须先登录。