Linux下网页抓取 linux下网页设计
创始人
2024-08-24 11:51:21
0

 最近一直在学习stevens的unix网络编程,对于网络通信有了一定的认识,所以也想练练手。聊天程序之前用winsock做过,这次不想做重复的。之前看到一哥们写过windows下抓取猫扑的帖子,我觉得抓页面也是一个不错想法。我也喜欢逛猫扑,有时候也去追追里面写的文章,猫扑帖子少了一个很重要的功能,就是只看楼主的帖子。猫扑水人很多,容易把楼主的帖子淹没在大海里面。

        查看了一下猫扑帖子的网页源代码,帖子内容介于

之间,只需要解析这段内容,就能得到自己想要的东西。不过里面东西比较多,比较杂,还是先找一个简单页面抓取试试。csdn博客相对来说就是个不错的选择,***没广告,内容不算很多,第二,代码风格很好。抓CSDN的页面无非获得博主名,文章名字和URL等,如果想获得更多的信息,可以把博主的排名,评论数抓取下来。

下面简单分析一下CSDN博客源代码。

博主标题:

 

[html] view plaincopyprint?  
  1.   
  2.   
  3.             

      

  4.   
  5.                 编程小子的专栏  
  6.   
  7.             

    锲而舍之,朽木不折;锲而不舍,金石可镂

      
  8.   
  9.               
  10.   
  11.             
  
  •   
  •         
  •   

    文章标题和URL:

     

     

    [html] view plaincopyprint?  
    1.   
    2.   
    3.       ubuntu11.10搭建git服务器  
    4.         

    文章访问次数,评论次数等:

     

     

    [html] view plaincopyprint?
    1.   
    2.   
    3.     2012-05-14 15:09  
    4.   
    5.     阅读(21)  
    6.   
    7.     评论(0)  
      

    博客统计信息:

     

     

    [html] view plaincopyprint?  
    1.   
    2.   
    3.            
    4. 访问:1218次
    5.   
    6.   
    7.            
    8. 积分:164分
    9.   
    10.   
    11.            
    12. 排名:千里之外
    13.   
    14.   
    15.          
    [html] view plaincopyprint?  
    1.   
    2.   
    3.            
    4. 原创:13篇
    5.   
    6.   
    7.            
    8. 转载:2篇
    9.   
    10.   
    11.            
    12. 译文:0篇
    13.   
    14.   
    15.            
    16. 评论:1条
    17.   
    18.   
    19.          

     

     从上面贴出的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方法。函数声明如下:

     

    [cpp] view plaincopyprint?  
    1. #include  
    2. struct hostent * gethostbyname(const char *hostname)  
    执行成功,返回非空指针,失败返回空指针,并设置h_errno,可以通过hstrerror方法查看h_errno对应的错误提示信息。

     

    函数中用到的hostent结构体,如下所示:

     

    [cpp] view plaincopyprint?  
    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里面。每次需要建立连接,我只需要调用这个方法即可。代码如下:

     

    [cpp] view plaincopyprint?  
    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请求格式如下所示:

     

    [cpp] view plaincopyprint?  
    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结束。
    [cpp] view plaincopyprint?  
    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响应头如下所示:

     

    [cpp] view plaincopyprint?  
    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来判断响应头部的结束位置。

     

    实现源码:

     

    [cpp] view plaincopyprint?  
    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函数。 [cpp] view plaincopyprint?  
    1. #include  
    2. char *strstr (char *haystack, const char *needle);  
    函数功能:查找needle在haystack中***次出现的地址,查找成功,返回***次出现的地址,查找失败返回0.类似于c++ string的find_first_of函数。   信息解析出来,需要存储下来。主要是存在自定义的数据结构里面。每一页(最多50篇文章)存储在struct Articles结构体里面,文章信息则存入struct ArticleInfo里面。页面存储结构如下图所示:
    自定义的结构体:  

     

    [cpp] view plaincopyprint?  
    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. };  

    【编辑推荐】

    1. Chkdsk大跃进:Win8磁盘检测时间大大缩短
    2. Linux下使用mke2fsk格式化分区的方法
    3. Ubuntu 11.10 利用终端环境备份还原

    相关内容

    热门资讯

    如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
    如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
    各种千兆交换机的数据接口类型详... 千兆交换机有很多值得学习的地方,这里我们主要介绍各种千兆交换机的数据接口类型,作为局域网的主要连接设...
    施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...
    Windows恶意软件20年“... 在Windows的早期年代,病毒游走于系统之间,偶尔删除文件(但被删除的文件几乎都是可恢复的),并弹...
    规避非法攻击 用好路由器远程管... 单位在市区不同位置设立了科技服务点,每一个服务点的员工都通过宽带路由器进行共享上网,和单位网络保持联...
    范例解读VB.NET获取环境变... VB.NET编程语言的使用范围非常广泛,可以帮助开发人员处理各种程序中的需求,而且还能对移动设备进行...
    20个非常棒的扁平设计免费资源 Apple设备的平面图标PSD免费平板UI 平板UI套件24平图标Freen平板UI套件PSD径向平...