项目日记 - 开发图片爬虫 Ⅰ

项目日记 - 开发图片爬虫 Ⅰ

用C++开发一个图片爬虫 Ⅰ :获取网页内容

前言

这个项目很久前就开始写了,但一直没有很完善。最近想起来,打算重新完善这个项目。

本教程仅适用于C++。内容较为复杂,可以根据自己需要来阅读。

目的

做一个项目,首先需要知道你的目标是什么。这个项目名为图片爬虫,当然是为了下载图片。

再具体一点,用户先给出一个网址,我们先保存网页,抓取网页上的图片链接和其他网址链接。然后保存每一张图片到用户设置的文件夹中。深入其他网址链接,循环这一步骤。

那么我们去爬一个图片网站,就可以找到很多好()看的图片了。

理论

先了解爬虫的定义,如下:

网络爬虫(英语:web crawler),也叫网络蜘蛛(spider),是一种用来自动浏览万维网的网络机器人。其目的一般为编纂网络索引。

对于这个东西我还想补充一下,爬虫操作不当可能是违法的,但对于我们这些小项目来说,请求服务器资源的速度一般达不到攻击服务器的要求。爬虫有一个国际互联网界通行的道德规范,名叫robots协议,有兴趣可以去了解一下。我们就先不管那么多,还是技术更要紧些,不干扰网站正常运行就行了。

那么如何去开发呢?我们目的很明确,所以大概分为几个步骤。我制作了一个流程图来演示:
项目流程图

现在对每个步骤进行解释,先设置爬取的网站,这个很简单。第一个要实现的就是如何获取网页,关于这个我做一个较为详细的理论说明。

关于用C++实现获取网页内容,我所知有两种方法,使用Winsock或WinInet。相信大家对这两个东西一定有所了解,不了解的可以细读定义。

先大致了解Winsock的定义:

Windows Sockets API (WSA), 简短记为Winsock, 是Windows的TCP/IP网络编程接口(API)。

具体是什么可以再自行了解,我这个项目选择使用的是WinInet。

WinInet(“Windows Internet”)API帮助程序员使用三个常见的Internet协议,这三个协议是用于World Wide Web万维网的超文本传输协议(HTTP:Hypertext Transfer Protocol)、文件传输协议(FTP:File Transfer Protocol)和另一个称为Gopher的文件传输协议。WinInet函数的语法与常用的Win32 API函数的语法类似,这使得使用这些协议就像使用本地硬盘上的文件一样容易。

定义中已经说了,这个API非常容易使用,这也是我为什么选择使用它。

再补充一下,我最近又查阅到了第三种方法,使用WinHTTP。我会在以后的章节对它进行详细的说明。相比而言,它其实更适合对网页的操作。定义:

Microsoft Windows HTTP Services(WinHTTP)为开发者提供了HTTP客户端应用程序编程接口(API),用于通过HTTP协议向其他HTTP服务器发送请求。

在官方有WinHTTP与WinInet的对比,如果用简短的话来说明他俩的关系,那就是:

除了少数例外,WinInet是WinHTTP的超集。在两者之间进行选择时,应使用WinInet,除非您计划在需要模拟和会话隔离的服务或类似服务的进程中运行。

如果还想了解更多WinInet与WinHTTP的区别,可以看官方英文文档

实战

相信大家已经对这些东西都有了大致了解,那么就开始写代码了。输入网址的主程序代码就不给出了,我先放出获取页面的代码实例,是从网上找到并改编之后的。

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
#include <wininet.h>

#define MAXBLOCKSIZE 1024 * 1024
void downloadUrl(const char *Url, const char *FilePath) {
static byte Temp[MAXBLOCKSIZE];
HINTERNET hSession= InternetOpenA("DownloadKit", INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0), hOpenUrl= 0;
if(hSession != NULL) {
hOpenUrl= InternetOpenUrlA(hSession, Url, NULL, 0, INTERNET_FLAG_DONT_CACHE, NULL);
if(hOpenUrl == NULL) goto fail;
FILE *stream= fopen(FilePath, "wb");
ULONG Number= 1;
while(Number > 0) {
InternetReadFile(hOpenUrl, Temp, MAXBLOCKSIZE, &Number);
fwrite(Temp, sizeof(char), Number, stream);
}
fclose(stream);
if(hOpenUrl) InternetCloseHandle(hOpenUrl);
if(hSession) InternetCloseHandle(hSession);
return;
}
fail:
if(hOpenUrl) InternetCloseHandle(hOpenUrl);
if(hSession) InternetCloseHandle(hSession);
printf("Internet Error\n");
return;
}

先大致说明一下函数参数,Url为你想下载网页的网址,filePath为保存到电脑的路径。

你可以找一个网址,选一个存文件的路径,调用函数并编译,你会发现这段代码是无法通过编译的,因为还需要连接静态链接库。

如果使用Visual Studio,需要在函数之前加入以下这句,并且忽略编号为4996的警告。

1
#pragma comment(lib, "wininet.lib")

如果使用MinGW,需要添加编译命令来/MinGW64/lib/libwininet.a文件。如果使用DevC++还可以通过项目属性来添加,这里不作细说。如果遇到了其他困难可以下方留言,我将会在第一时间解答。

InternetOpenA

先来讲解InternetOpenA函数。这个是我头文件里的定义。如果你使用Unicode遍码就改成InternetOpenW,用法类似。

1
2
3
4
5
6
7
INTERNETAPI_(HINTERNET) InternetOpenA(
_In_opt_ LPCSTR lpszAgent,
_In_ DWORD dwAccessType,
_In_opt_ LPCSTR lpszProxy,
_In_opt_ LPCSTR lpszProxyBypass,
_In_ DWORD dwFlags
);

lpszAgent指向一个空结束的字符串,用来设置UserAgent。对于我们用处不大,可以随意设置。

dwAccessType指定访问类型,用于是否使用代理或注册表配置。我们选择使用IE代理,如果想设置代理服务器则使用INTERNET_OPEN_TYPE_PROXY。数值表:

数值 说明
INTERNET_OPEN_TYPE_DIRECT 使用直接连接网络。
INTERNET_OPEN_TYPE_PRECONFIG 获取代理或直接从注册表中的配置,使用代理连接网络。
INTERNET_OPEN_TYPE_PRECONFIG_WITH_NO_AUTOPROXY 获取代理或直接从注册表中的配置,并防止启动Microsoft JScript或Internet设置(INS)文件的使用。
INTERNET_OPEN_TYPE_PROXY 通过代理的请求,除非代理旁路列表中提供的名称解析绕过代理,在这种情况下,该功能的使用。

lpszProxy指向一个空结束的字符串,该字符串指定的代理服务器的名称,我们此参数应该设置为NULL。设置代理还要在这里深入研究,由于比较麻烦,我们先不去了解了。

lpszProxyBypass指向一个空结束的字符串,该字符串指定的可选列表的主机名或IP地址。我们此参数也设置为NULL。

dwFlags是设定一些要求的,值可以组合组合,我们设置为0。数值表:

数值 说明
INTERNET_FLAG_ASYNC 使异步请求处理的后裔从这个函数返回的句柄。
INTERNET_FLAG_FROM_CACHE 不进行网络请求,从缓存返回的所有实体,如果请求的项目不在缓存中,则返回一个合适的错误,如ERROR_FILE_NOT_FOUND。
INTERNET_FLAG_OFFLINE 同INTERNET_FLAG_FROM_CACHE。

Return Value即返回值是一个HINTERNET句柄,用于接下来的WinINet函数。计算机正常且函数参数规范的情况下,都能够成功返回。如果返回NULL就是失败了,那么请检查自己的参数。

InternetOpenUrlA

接下来就是用于打开网页的InternetOpenA函数了,如果使用Unicode编码请改为InternetOpenUrlW。

相比而言,这个函数参数比较多。

1
2
3
4
5
6
7
8
INTERNETAPI_(HINTERNET) InternetOpenUrlA(
_In_ HINTERNET hInternet,
_In_ LPCSTR lpszUrl,
_In_reads_opt_(dwHeadersLength) LPCSTR lpszHeaders,
_In_ DWORD dwHeadersLength,
_In_ DWORD dwFlags,
_In_opt_ DWORD_PTR dwContext
);

hInternet为当前的Internet会话句柄,也就是我们刚刚使用InternetOpen 函数返回的句柄。

lpszUrl指向一个空结束的字符串,用于指定读取的网址。非常容易理解。

lpszHeaders指向一个空结束的字符串,指定发送到HTTP服务器的头信息。我们不必做过多了解,设为NULL即可。

dwHeadersLength指定的长度,字符,额外的标头。对于我们说也没什么用,设为0即可。

dwFlags设定一些要求,我们设置的INTERNET_FLAG_NO_CACHE,与INTERNET_FLAG_NO_CACHE_WRITE有相同作用。有较多数值,可以了解后选择其中一个。数值表:

数值 说明
INTERNET_FLAG_EXISTING_CONNECT 如果使用相同的必须属性创建会话,会尝试利用现有的InternetConnect对象。这只对FTP操作非常有用,因为FTP是唯一在同一会话中执行多种操作的协议。WinINet API 为每个由InternetOpen产生的HINTERNET句柄缓冲一个单独链接句柄。InternetOpenUrl使用此标志的HTTP和FTP连接。
INTERNET_FLAG_HYPERLINK 当决定何时从网络重载时,如果服务器没有返回 Expires time 和 LastModified,那么强制重载。
INTERNET_FLAG_IGNORE_CERT_CN_INVALID 停用检查从服务器对必须的主机名称返回的SSL/PCT-based证书。 WinINet函数使用简单的比较匹配主机名称和通配符的规则检查证书。
INTERNET_FLAG_IGNORE_CERT_DATE_INVALID 停用检查的SSL/PCT-based的证书的适当的有效日期。
INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP 禁用检测这中特殊的重定向。当使用此标志, WinINet 透明允许从HTTPS到HTTP URL的重定向。
INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS 禁用检测这中特殊的重定向。当使用此标志, WinINet 透明的允许的HTTP到HTTPS URL的重定向。
INTERNET_FLAG_KEEP_CONNECTION 如果可能的话,为连接使用保活语义。这个标志要求微软网络( MSN ),NTLM和其他类型的身份验证。
INTERNET_FLAG_NEED_FILE 如果要创建的文件不能被缓存,创建临时文件。
INTERNET_FLAG_NO_AUTH 不试图自动验证。
INTERNET_FLAG_NO_AUTO_REDIRECT 不自动处理HttpSendRequest中的重定向。s
INTERNET_FLAG_NO_CACHE_WRITE 不添加返回实体到缓存。
INTERNET_FLAG_NO_COOKIES 不会自动添加的Cookie头到请求,并且不自动添加返回的cookie到cookie数据库。
INTERNET_FLAG_NO_UI 禁用Cookie的对话框。
INTERNET_FLAG_PASSIVE 使用被动FTP语义。InternetOpenUrl为FTP的文件和目录使用此标志。
INTERNET_FLAG_PRAGMA_NOCACHE 即使代理中存在缓存副本,也强制要求由源服务器返回。
INTERNET_FLAG_RAW_DATA 检索的Gopher目录信息时,传回的数据作为GOPHER_FIND_DATA结构,如果检索的FTP目录信息时,作为一个WIN32_FIND_DATA结构。如果此标志没有指定,或者请求通过CERN代理创建, InternetOpenUrl返回的HTML版本的目录。
INTERNET_FLAG_RELOAD 从原服务器强制下载所要求的文件,对象,或目录列表,而不是从缓存下载。
INTERNET_FLAG_RESYNCHRONIZE 重新加载的HTTP资源,如果资源在最后一次下载后已被修改。所有FTP和Gopher资源将被重载。
INTERNET_FLAG_SECURE 使用安全传输语义。这次传输使用安全套字节层/专用通信技术(的SSL / PCT ),这只有在HTTP请求时有意义。

dwContext为指向一个变量的指针,将随着返回的句柄,一起传递给回调函数。有兴趣可以自行查阅,我们设为NULL即可。

Return Value即返回值,若成功连接则返回一个有效的HINTERNET句柄,如果失败将放回NULL。如果失败可以使用GetLastError检索特定的错误讯息。请调用InternetGetLastResponseInfo确定为什么对服务器的访问被拒绝,这些函数大家有兴趣可以查阅更多资料来了解。

FILE

这一步就不作过多讲解了,只简单介绍这几个函数。

fopen用来打开文件,返回文件指针。

1
FILE * fopen(const char * path, const char * mode);

fwrite用于向文件中写入一个数据块,可以看实际代码自行理解。

1
size_t fwrite(const void * ptr, size_t size, size_t count, FILE * stream);

fclose用于关闭一个流。

1
int fclose(FILE * stream);

InternetReadFile

InternetReadFile的官方定义为,从一个由InternetOpenUrl,FtpOpenFile或HttpOpenRequest函数打开的句柄中读取数据。我们使用它来将网页上的内容保存到一个字符数组中。

InternetReadFile函数每次获取的内容长度有限,所以应多次调用,直到全部获取完毕。

1
2
3
4
5
6
BOOLAPI InternetReadFile(
_In_ HINTERNET hFile,
_Out_writes_bytes_(dwNumberOfBytesToRead) __out_data_source(NETWORK) LPVOID lpBuffer,
_In_ DWORD dwNumberOfBytesToRead,
_Out_ LPDWORD lpdwNumberOfBytesRead
);

hFile为InternetOpenUrl,FtpOpenFile或HttpOpenRequest函数返回的句柄。我们使用InternetOpenUrl获得的HINTERNET句柄。

lpBuffer为指向缓冲器的指针。因此我们需要定义一个字符数组,然后将它作为参数。

dwNumberOfBytesToRead为要读取的字节数。这个需要根据自己的网络情况,我使用的为1024 * 1024,也就是1M。

lpdwNumberOfBytesRead为接收读取字节量的变量。我们用它来判断是否将网页内容全部获取完毕。

Return Value即返回值,成功为TRUE,失败为FALSE。若失败可用GetLastError查看更多错误信息。

InternetCloseHandle

到这,获取网页的代码基本完工了。但做事应善始善终,那些句柄占用的资源还需要释放。InternetCloseHandle函数用来关闭一个Internet句柄。

1
2
3
BOOLAPI InternetCloseHandle(
_In_ HINTERNET hInternet
);

hInternet为要关闭的句柄。我们要依次关闭那俩个HINTERNET句柄。

Return Value即返回值,成功为TRUE,失败为FALSE。若失败可用GetLastError查看更多错误信息。

小结

其实获取网页内容的代码是非常简单的,然而其中还蕴含着许许多多的知识。这段代码不仅可以获取网页,也可以用来获取图片或其他格式的文件。

这段函数下载后一般为Unicode编码文件,下一节我将讲解如何将它转换为ASCII编码。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×