一、项目简介:
爬虫是一个自动化的网络资源下载工具,可以分析网络上的资源如图片、文章持久化保存到本地。合作的项目可以爬取将读书网站上的信息。
二、项目整体概述:
爬虫爬取网页资源过程:每一个网页资源都有对其他网页的链接,有大量的出链以及入链,通过对网页的分析和下载,可以得到更多指向新网页的链接,从而完成下载的目的。
完成一个爬虫需要有的步骤:
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=孤儿&orderId=&page=1&style=1&pageSize=20&siteid=1&pubflag=0&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;
}