前言
最近在学用PHP处理图像,并且做了一个“瀑布流”布局的图片墙。但是遇到一个问题,就是图片不够用了,所以就写了一个脚本,通过关键词来下载百度图片。
正文
写爬虫的步骤就是以下这些:
找URL
在使用百度图片搜索的时候,生成的URL如下:
https://image.baidu.com/search/index?tn=baiduimage&word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3
可以看到,word参数就是我们要查询的关键字(经过了urlencode)。
但是这是我们要找的URL吗?并不是。因为我们通过这个URL,我们只能得到大约30张图片。
但是,当我们向下划鼠标时,我们发现更多的图片加载了出来。这是,浏览器地址栏的URL并没有发生变化。
说明是异步调用了其他接口,这个接口才是我们要找的URL。
那么,怎么找到它呢?答案是通过F12。点击F12,打开Chrome的开发者模式,找到Network下的XHR标签,再次向下滑动鼠标,发现浏览器确实进行了异步请求,截图如下:
这个URL是这样的:
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord+=&cl=&lm=&ie=utf-8&oe=utf-8&adpicid=&st=&word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&z=&ic=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=&fr=&step_word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&pn=120&rn=30&gsm=78&1506604653115=
URL很长,参数很多。将它粘贴在浏览器的地址栏,可以看到它的返回值,如下:
可以看出是一个JSON格式的数据,从中我们可以发现,在data下有30个元素,每个对应着一张图片。
通过观察,猜测参数rn=30
是指每次调用返回的图片张数,修改该参数进行测试,发现猜测正确,但是最多只能返回30张。
所以,想要一次获取多张图片,我们还需要更多的参数,然后比对下多个URL
https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord+=&cl=&lm=&ie=utf-8&oe=utf-8&adpicid=&st=&word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&z=&ic=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=&fr=&step_word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&pn=60&rn=30&gsm=3c&1506608375614=
发现pn
这个参数在变化,而且是每次加30,所以猜想它是一个代表偏移量的参数。通过自己构造的URL确认了这个猜想。
爬取图片地址函数
得到了接口URL之后,就可以动手写爬虫函数了。
/**
* [spider 爬取百度图片]
* @param [string] $keyword [搜索关键字]
* @param [int] $num [图片数量]
* @return [array] [图片对应的地址]
*/
function spider($keyword, $num) {
$keyword = urlencode($keyword);
$res = array();
for ($pn = 0; $pn <= $num; $pn += 30) {
$url = "http://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&fp=result&queryWord+=&ie=utf-8&oe=utf-8&word=".$keyword."=&pn=".$pn."&rn=30";
$json = file_get_contents($url);
$array = json_decode($json);
foreach ($array->data as $key => $image) {
if (!in_array($image, $res)) {
if (isset($image->middleURL)) {
$res[] = $image->middleURL;
}elseif (isset($image->thumbURL)){
$res[] = $image->thumbURL;
}
}
}
}
return $res;
}
首先对关键字进行url编码,然后拼接URL,调用之后解析JSON,获取图片地址存入返回数组中即可。
下载图片到本地
得到了图片的地址之后,可以通过下面的函数将它下载到本地。
/**
* [download 下载图片到本地]
* @param [string] $url [图片地址]
* @param [string] $path [存储路径]
* @param string $referer [HTTP Header中的referer值]
*/
function download($url, $path, $referer = '') {
$filename = pathinfo($url, PATHINFO_BASENAME);
if (!file_exists($path.$filename)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_REFERER, $referer);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
$file = curl_exec($ch);
curl_close($ch);
$resource = fopen($path.$filename, 'a');
fwrite($resource, $file);
fclose($resource);
}
}
先判断指定路径下有没有同名图片,如果没有再进行下载。
其中,$referer
参数的含义是指HTTP Header中的referer值,目的在于跳过百度图片的防爬虫策略。因为如果不是从指定页面跳入图片地址,会返回一个403错误。
批量下载
通常情况下,我们要下载的图片都大于1张,所以可以采用批量下载的方式。
/**
* [batchDownload 批量下载图片]
* @param [type] $imgUrls [图片地址数组]
* @param [type] $path [目标路径]
* @param string $referer [HTTP Header中的referer值]
*/
function batchDownload($imgUrls, $path, $referer = '') {
foreach ($imgUrls as $key => $imgUrl) {
download($imgUrl, $path, $referer);
}
}
就是一个循环调用download()
的操作。
测试
写一个客户端类,代表用户行为。
set_time_limit(0);
class Client {
public static function main() {
$keyword = "新垣结衣";
$num = 60;
$path = "spider_test/";
$referer = "https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord+=&cl=&lm=&ie=utf-8&oe=utf-8&adpicid=&st=&word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&z=&ic=&s=&se=&tab=&width=&height=&face=&istype=&qc=&nc=&fr=&step_word=%E6%96%B0%E5%9E%A3%E7%BB%93%E8%A1%A3&pn=120&rn=30&gsm=78&1506604653115=";
$images = spider($keyword, $num);
batchDownload($images, $path, $referer);
}
}
Client::main();
首先取消脚本时间限制,因为如果数量大的话,很可能会超时。
脚本执行结束后,在spider_test
路径下,可以看到下载的图片。
其实,更好的方式是将爬取和下载的这3个函数封装成一个类(download可以是私有的,有时间改一下)。