C++项目整理:爬虫模块设计

本文深入讲解了爬虫技术,包括项目整体流程、布隆过滤器的实现、URL解析、网络连接建立、资源请求及响应处理、资源下载及数据持久化等关键环节。介绍了布隆过滤器如何有效减少重复URL请求,解析URL获取关键信息,建立与目标网站的连接,发送HTTP请求并处理响应,以及使用正则表达式筛选和保存有用信息。

一、项目简介:

爬虫是一个自动化的网络资源下载工具,可以分析网络上的资源如图片、文章持久化保存到本地。合作的项目可以爬取将读书网站上的信息。

二、项目整体概述:

爬虫爬取网页资源过程:每一个网页资源都有对其他网页的链接,有大量的出链以及入链,通过对网页的分析和下载,可以得到更多指向新网页的链接,从而完成下载的目的。

完成一个爬虫需要有的步骤:

1,准备一个网站地址。

2,解析网站地址通过解析链接获取主机名,主机IP,端口号,资源文件名。

3,完成本机与目标网站的网络链接。

4,向目标网站请求资源。

5,获取响应内容,处理响应内容,提取文件数据。

6,将文件持久化保存到本地。

在不同的协议下通用的端口是不一样的如HTTP为80,HTTPS为443;

三、项目具体实现:

1、布隆过滤器的实现:URL去重,布隆过滤器是一个 bit 向量或者说 bit 数组,如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7。利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。但是,随着存入的元素数量增加,误算率随之增加。

//布隆过滤器的初始化:参数func1,func2是两个字符串哈希函数
void Cspider_BloomFilter_Init(BloomFilter* bf,BloomFunc func1,BloomFunc func2)
{
    if(bf == NULL || func1 == NULL || func2 == NULL)                                                            
    {
        //非法输入
        return;
    }
    //对布隆过滤器中的位图进行初始化
	
    Cspider_BitMap_Init(&bf->bm,MAXSIZE);
    bf->bloomfunc[0] = func1;
    bf->bloomfunc[1] = func2;
    return;
}


//布隆过滤器的插入
void Cspider_BloomFilter_Insert(BloomFilter* bf,const char* str)
{
    if(bf == NULL || str == NULL)
    {
        //非法输入
        return;
    }
    //首先根据字符串哈希算法计算字符串对应的数字
    //然后将计算出的数字根据除留余数法转化为实际对应的比特位下标
    uint64_t bloomnum[FUNCMAXSIZE];//该数组用于存放计算的多个下标值
    int i = 0;
    for(;i < FUNCMAXSIZE;i++)                                                                                   
    {
        bloomnum[i] = bf->bloomfunc[i](str) % MAXSIZE;
    }
    //再将该下标插入到布隆过滤器中的位图中,直接调用位图的插入函数即可
    for(i = 0;i < FUNCMAXSIZE;i++)
    {
        Cspider_BitMap_Set(&bf->bm,bloomnum[i]);
    }
    return;
}

//布隆过滤器的销毁
void Cspider_BloomFilter_Destroy(BloomFilter* bf)
{
    if(bf == NULL)
    {
        //非法输入
        return;
    }
 
    //将布隆过滤器中的成员:位图进行销毁即可
    Cspider_BitMap_Destroy(&bf->bm);
 
    bf->bloomfunc[0] = NULL;
    bf->bloomfunc[1] = NULL;
    return;
}

2、解析URL:定义url_t结构体存放链接信息

typedef struct
{
	char surl[4096];
	char domain[1024];
	char file[1024];
	char path[1024];
	char ip[16];
	int httptype;
	int port;
}url_t;
int Cspider_Analytic_Url(url_t * u)
{
	// 加载库 
	WSADATA wsaData;
    int iResult;

    iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) 
	{
        printf("WSAStartup failed: %d\n", iResult);
        return 1;
    }

	int i = 0 ; 
	int j = 0 ; 
	int filesize = 0;
	int start = 0 ;
	char* arr[] = {"http://","https://",NULL};

	memset(u->domain,0,sizeof(u->domain));    //域名
	memset(u->path,0,sizeof(u->path));        //路径名
	memset(u->file,0,sizeof(u->file));        //文件名
	memset(u->ip,0,sizeof(u->ip));            //ip地址
    
    //判断协议获取端口号
	if( (strncmp(u->surl,arr[0],strlen(arr[0]))) == 0)    //http
	{
		u->httptype = 0;
		u->port = 80;
		start = strlen(arr[0]);
	}
	else                //https
	{
		u->httptype = 1;
		u->port = 443;
		start = strlen(arr[1]);
	}

	 // 获得域名
     for(i = start ; u->surl[i]!='/' && u->surl[i]!='\0' ; i++)
     {
        u->domain[j] = u->surl[i];
        j++;
     }
     // 获取文件名
     j = 0;
     for(i = strlen(u->surl);u->surl[i] != '/';i--,filesize++);
     for(i = strlen(u->surl) - filesize + 1;u->surl[i]!='\0';i ++)
     {
           u->file[j] = u->surl[i];
           j++;
     }

	// 获取路径信息
	j = 0;
	for(i=start+strlen(u->domain);i<strlen(u->surl) - filesize + 1;i++)
	{
		u->path[j] = u->surl[i];
		j++;
	}
	// 获取IP地址
	struct hostent *ent;
	if( (ent = gethostbyname(u->domain)) == NULL )
	{
		printf("gethostbyname call failed....");
		return -1;
	}
	inet_ntop(AF_INET,ent->h_addr_list[0],u->ip,sizeof(u->ip));

	printf(" [1] URL analytical success: \n surl:%s\n domain:%s\n path:%s\n filename:%s\n ip:%s\n type:%d\n port:%d\n",u->surl,u->domain,u->path,u->file,u->ip,u->httptype,u->port);
	return  0;


	
}

3、完成于目标网站的连接:首先创建爬虫套接字,然后将它与目标网站连接

int Cspider_Socket_Create()
{
	int webfd;

	struct sockaddr_in myaddr;
	memset(&myaddr,0,sizeof(myaddr));
	myaddr.sin_family = AF_INET;
	myaddr.sin_port = htons(8000);
	myaddr.sin_addr.s_addr = htonl(INADDR_ANY);

	if( (webfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
	{
		perror("Socket Create Error..");
		return -1;
	}

	bind(webfd,(struct sockaddr*)&myaddr,sizeof(myaddr));

	printf("[2] Socket Create Success..\n");
	return webfd;
}
int Cspider_Connect_Webserver(url_t* u,int webfd)
{
	struct sockaddr_in webaddr;
	memset(&webaddr,0,sizeof(webaddr));
	webaddr.sin_family = AF_INET;
	webaddr.sin_port = htons(u->port);
	inet_pton(AF_INET,u->ip,&webaddr.sin_addr.s_addr);
	if((connect(webfd,(struct sockaddr*)&webaddr,sizeof(webaddr)))!=0)
	{
		perror("Connect Webserver error..");
		return -1;
	}
	printf("[3] Connect Webserver Success..\n");
	return  0;

}

4、资源请求:在浏览器与服务器进行请求时,获取服务器响应状态码,查看资源是否请求成功.

int Cspider_Create_Requesthead(url_t *u,char *head)
{
     memset(head,0,4096);
     sprintf(head,"GET %s HTTP/1.1\r\n"\
             "Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n"\
             "User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537(KHTML, like Gecko) Chrome/47.0.2526Safari/537.36\r\n"\
             "Host: %s \r\n"\
             "Connection:close\r\n\r\n",u->surl,u->domain);
     printf("[4] create_requesthead success,%s\n",head);
     return 0;
 }
int Cspider_Get_StatusCode(const char* reshead)
{
	char sstat[1024];
	int stat;
	memset(&sstat,0,sizeof(sstat));
	regex_t sreg;
	regmatch_t smatch[2];
	regcomp(&sreg,"HTTP/1.1 \\([^\r\n]\\+\\?\\)\r\n",0);
	if((regexec(&sreg,reshead,2,smatch,0))==0)
	{
		_snprintf(sstat,smatch[1].rm_eo - smatch[1].rm_so + 1,"%s",reshead + smatch[1].rm_so);
		sscanf(sstat,"%d",&stat);
		printf("[*] Get statcode success:%d\n",stat);
		return stat;
	}
	regfree(&sreg);
	return -1;
}
//常见服务器响应状态码示例:
//200    服务器响应资源成功
//400,403,404    资源不存在
//501,502    服务器异常
//301,302    被请求的资源已永久移动到新位置

5、提前资源:在请求资源时会有两种链接方式,一种是短链接:一次链接,浏览器与服务器完成一次交互后,服务器主动断开。还有一种是长链接:浏览器与服务器保持连接持续交互。在访问https时必须与服务器完成安全认证和私密的安全链接,否则即使连接成功也无法传输数据。

 ssl_t* Cspider_Create_Openssl(int webfd)
 {
      ssl_t* ssl = (ssl_t*)malloc(sizeof(ssl_t));
      SSL_load_error_strings();  // 加载错误处理接口
      SSL_library_init();   // 初始化SSL库
      OpenSSL_add_ssl_algorithms(); // 初始化加密散列接口
      ssl->sslctx = SSL_CTX_new(SSLv23_method());
      ssl->sslsocket = SSL_new(ssl->sslctx);
      SSL_set_fd(ssl->sslsocket,webfd);
      SSL_connect(ssl->sslsocket);    // 与webserver进行安全认证和重连
      printf("[*] SSL connect successfully..\n");
      return ssl;                    // 完成认证重连后使用sslsocket与webserver进行加密的安全传输
 }
int Cspider_Download(url_t *u,const char* head,int webfd,ssl_t* ssl)
{
  char buf[8192];
  char reshead[4096];
  int len;
  char* pos;
  FILE* fd;    //文件指针
  int statcode;
  memset(buf,0,sizeof(buf));
  memset(reshead,0,sizeof(reshead));
  // 发送HTTP请求头
  if(!ssl)
  {
    send(webfd,head,strlen(head),0);
    printf("[5] Send Requesthead to web success..\n");

    len = recv(webfd,buf,sizeof(buf),0);
    if((pos = strstr(buf,"\r\n\r\n")) == NULL)
    {
      printf("strstr find error..\n");
      return -1;
    }
    _snprintf(reshead,pos-buf+4,"%s",buf);
    printf("[6] Get Response head Successfully..\n");
    printf("Response Head%s\n",reshead);

    if((statcode = Cspider_Get_StatusCode(reshead)) == 200)
    {
      string filepath;
      string strpath(u->file);
      filepath = "D://";
      filepath += strpath;

      fopen_s(&fd,filepath.c_str(),"wb+");
      fwrite( pos+4, 1 , len - (pos - buf + 4) , fd);
      while( (len = recv(webfd,buf,sizeof(buf),0)) > 0)
      {
        fwrite(buf,1,len,fd);
      }
      fclose(fd);

      printf("[7] SSL Download Success..\n");
    }
    else
    {
      return -1;
    }
  }
  else
  {
    SSL_write(ssl->sslsocket,head,strlen(head));
    printf("[5]SSL Send Requesthead to web success..\n");

    len = SSL_read(ssl->sslsocket,buf,sizeof(buf));
    if((pos = strstr(buf,"\r\n\r\n")) == NULL)
    {
      printf("strstr find error..\n");
      return -1;
    }
    _snprintf(reshead,pos-buf+4,"%s",buf);
    printf("[6] SSL Get Response head Successfully..\n");
    printf("Response Head%s\n",reshead);

    if((statcode = Cspider_Get_StatusCode(reshead)) == 200)
    {
      string filepath;
      string strpath(u->file);
      filepath = "D://";
      filepath += strpath;
      fopen_s(&fd,filepath.c_str(),"wb+");

      //fopen_s(&fd,u->file,"wb+");

      fwrite( pos + 4, 1 , len - (pos - buf + 4) , fd);
      while((len = SSL_read(ssl->sslsocket,buf,sizeof(buf))) > 0)
      {

        fwrite(buf,1,len,fd);
      }
      printf("[7] SSL Download Success..\n");
    }
    else
    {
      return -1;
    }
    fclose(fd);
    free(ssl);
    ssl = NULL;
  }
  closesocket(webfd);
  return 0;

}  

6、选取有用信息持久化到本地:需要正则表达式筛选需要信息

MYSQL *sock = new MYSQL;
int Cspider_Analytic_Html(url_t * u)
{

  mysql_init(sock);  
  mysql_set_character_set(sock,"utf8");// 数据库增删改查
  // 数据库测试
  if( Cspider_Mysql_Connect("127.0.0.1","root","root","0419cspider",sock) )
  {
    cout << "数据库连接成功" << endl;
  }

  //char *p = Cspider_File_mmap(u->file);

  string filepath;
  string strpath(u->file);
  filepath = "D://";
  filepath += strpath;

  char * b = (char*)filepath.c_str();
  //char *b = "D://abc.txt";

  WCHAR wszClassName[256];
  memset(wszClassName,0,sizeof(wszClassName));
  MultiByteToWideChar(CP_ACP,0,b,strlen(b)+1,wszClassName,
    sizeof(wszClassName)/sizeof(wszClassName[0]));
  // 步骤1 打开文件FILE_FLAG_WRITE_THROUGH
  HANDLE hFile = CreateFile(
    wszClassName,
    GENERIC_WRITE | GENERIC_READ,// 如果要映射文件:此处必设置为只读(GENERIC_READ)或读写
    0, // 此设为打开文件的任何尝试均将失败
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL, //|FILE_FLAG_WRITE_THROUGH,【解1】
    NULL);
  if (hFile != INVALID_HANDLE_VALUE)// 文件打开失败返回句柄为-1
    // 这步必须测试,详细见步骤2
  {
    cout<<"文件打开成功~!\n";
  }
  else
  {
    cout<<"文件打开失败!\n";
    DWORD d = GetLastError();
    cout<<d<<endl;
    return NULL;
  }



  // 步骤2 建立内存映射文件
  DWORD dwFileSize = GetFileSize(hFile, NULL);
  printf("文件大小为:%d\n", dwFileSize);
  HANDLE hFileMap = CreateFileMapping(
    hFile, // 如果这值为INVALID_HANDLE_VALUE,是合法的,上步一定测试啊
    NULL, // 默认安全性
    PAGE_READWRITE, // 可读写
    0, // 2个32位数示1个64位数,最大文件字节数,
    // 高字节,文件大小小于4G时,高字节永远为0
    0,//dwFileSize, // 此为低字节,也就是最主要的参数,如果为0,取文件真实大小
    NULL);
  if (hFileMap != NULL)
  {
    cout<<"内存映射文件创建成功~!\n";
  }
  else
  {
    cout<<"内存映射文件创建失败~!"<<endl;
  }



  // 步骤3:将文件数据映射到进程的地址空间
  PVOID pvFile = MapViewOfFile( //pvFile就是得到的指针,用它来直接操作文件
    hFileMap,
    FILE_MAP_WRITE, // 可写
    0, // 文件指针头位置 高字节
    0, // 文件指针头位置 低字节 必为分配粒度的整倍数,windows的粒度为64K
    0); // 要映射的文件尾,如果为0,则从指针头到真实文件尾
  if (pvFile != NULL)
  {
    cout<<"文件数据映射到进程的地址成功~!\n";
  }
  else
  {
    cout<<"文件数据映射到进程的地址成功~!\n";
  }

  char *p = (char*)pvFile;
  char *pTemp = p;

  char BookName[4096];
  char AuthName[4096];
  char Aurl[4096];
  memset(BookName,0,sizeof(BookName));
  memset(AuthName,0,sizeof(AuthName));
  memset(Aurl,0,sizeof(Aurl));

  // 正则类型  书名  作者名
  regex_t book_reg , auth_reg ,aurl_reg;
  // 正则匹配结果
  regmatch_t book_match[2] , auth_match[2] , aurl_match[2];
  regcomp(&book_reg,"<a href=\"//book.qidian.com/info/[^>]\\+\\?>\\([^<]\\+\\?\\)</a>",0);
  regcomp(&auth_reg,"<a class=\"name\" href=\"//my.qidian.com/author/[^>]\\+\\?>\\([^<]\\+\\?\\)</a>",0);
  regcomp(&aurl_reg,"<a href=\"\\(//www.qidian.com/all[^\"]\\+\\?\\)\"",0);

  //<a href="//www.qidian.com/all?tag=孤儿&amp;orderId=&amp;page=1&amp;style=1&amp;pageSize=20&amp;siteid=1&amp;pubflag=0&amp;hiddenField=0" >
    // 匹配链接
  while((regexec(&aurl_reg,p,2,aurl_match,0)) == 0)
  {
    _snprintf(Aurl,aurl_match[1].rm_eo - aurl_match[1].rm_so, "%s",p + aurl_match[1].rm_so);
    printf("[*] NEW_URL [%s]\n",Aurl);
    p += aurl_match[0].rm_eo;
    memset(Aurl,0,sizeof(Aurl));
  }
  // p重新偏移
  p = (char*)pvFile;

  while((regexec(&book_reg,p,2,book_match,0)) == 0)
  {
    _snprintf(BookName,book_match[1].rm_eo - book_match[1].rm_so, "%s",p + book_match[1].rm_so);
    printf("[*] BookName [%s]  ",BookName);
    char szsql[3000] = {0};
    p += book_match[0].rm_eo;
    
    if( regexec(&auth_reg,p,2,auth_match,0) == 0 )
    {
      _snprintf(AuthName,auth_match[1].rm_eo - auth_match[1].rm_so, "%s",p + auth_match[1].rm_so);
      printf("[*] AuthorName [%s]  \n",AuthName);
      p += auth_match[0].rm_eo;
    }
    sprintf_s(szsql,"insert into book values('%s','%s')",BookName,AuthName);
    memset(BookName,0,sizeof(BookName));
    memset(AuthName,0,sizeof(AuthName));
    Cspider_Mysql_Update(szsql,sock);
  }
  regfree(&book_reg);
  regfree(&auth_reg);
  regfree(&aurl_reg);

  CloseHandle(hFile);
  CloseHandle(hFileMap);
  UnmapViewOfFile(pvFile);
  return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值