用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 | #include <wininet.h> |
先大致说明一下函数参数,Url为你想下载网页的网址,filePath为保存到电脑的路径。
你可以找一个网址,选一个存文件的路径,调用函数并编译,你会发现这段代码是无法通过编译的,因为还需要连接静态链接库。
如果使用Visual Studio,需要在函数之前加入以下这句,并且忽略编号为4996的警告。
1 | #pragma comment(lib, "wininet.lib") |
如果使用MinGW,需要添加编译命令来/MinGW64/lib/libwininet.a文件。如果使用DevC++还可以通过项目属性来添加,这里不作细说。如果遇到了其他困难可以下方留言,我将会在第一时间解答。
InternetOpenA
先来讲解InternetOpenA函数。这个是我头文件里的定义。如果你使用Unicode遍码就改成InternetOpenW,用法类似。
1 | INTERNETAPI_(HINTERNET) InternetOpenA( |
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 | INTERNETAPI_(HINTERNET) InternetOpenUrlA( |
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 | BOOLAPI InternetReadFile( |
hFile为InternetOpenUrl,FtpOpenFile或HttpOpenRequest函数返回的句柄。我们使用InternetOpenUrl获得的HINTERNET句柄。
lpBuffer为指向缓冲器的指针。因此我们需要定义一个字符数组,然后将它作为参数。
dwNumberOfBytesToRead为要读取的字节数。这个需要根据自己的网络情况,我使用的为1024 * 1024,也就是1M。
lpdwNumberOfBytesRead为接收读取字节量的变量。我们用它来判断是否将网页内容全部获取完毕。
Return Value即返回值,成功为TRUE,失败为FALSE。若失败可用GetLastError查看更多错误信息。
InternetCloseHandle
到这,获取网页的代码基本完工了。但做事应善始善终,那些句柄占用的资源还需要释放。InternetCloseHandle函数用来关闭一个Internet句柄。
1 | BOOLAPI InternetCloseHandle( |
hInternet为要关闭的句柄。我们要依次关闭那俩个HINTERNET句柄。
Return Value即返回值,成功为TRUE,失败为FALSE。若失败可用GetLastError查看更多错误信息。
小结
其实获取网页内容的代码是非常简单的,然而其中还蕴含着许许多多的知识。这段代码不仅可以获取网页,也可以用来获取图片或其他格式的文件。
这段函数下载后一般为Unicode编码文件,下一节我将讲解如何将它转换为ASCII编码。