一、准备工作
1.1 激活虚拟环境
activate envname
1.2 新建Scrapy项目
scrapy startproject projectname
1.3 新建Spider
scrapy genspider images image.baidu.com
1.4 PyCharm打开项目
1.5 选择项目解释器
至此,可以开始编写爬虫程序了
二、网页分析
用Chrome打开百度图片,输入关键字“单色釉瓷”,右键检查元素
发现图片的URL是在标签
main_img img-hover
中,而在网页源代码中却查找不到
因为百度图片的网页是动态的,采用的是Ajax+JSON机制。网页原始数据是没有图片的,通过运行JavaScript,把图片数据插入到网页的HTML标签中。所以我们在开发者工具中虽然能看到这个HTML标签,但是网页的原始数据其实没有这个标签,它只在运行时加载和渲染。真实的图片信息被打包放在JSON文件当中,所以真正要解析的是JSON文件。
点击Network–XHR,在往下滑动滚动条时,会连续出现名为acjson?tn=resultjson_com&ipn=···
的请求,点击其中一条,再点击Preview,我们看到这是一条JSON数据,点开data,我们看到这里面有30条数据,每一条都对应着一张图片。
说明百度图片一开始只加载30张图片,当往下滑动滚动条时,页面会动态加载1条JSON数据,每条JSON数据里面包含了30条信息,信息里面又包含了图片的URL,JavaScript会将这些URL解析并显示出来。这样,每次滚动就又加载了30张图片。
点击Headers,对比这些JSON数据的头部信息。发现Headers下的Query String Parameters中的字段大多保持不变,只有pn字段保持以30为步长递增。
点击其中一个JSON,在Headers下的General中就有一个Request URL,其内容就是JSON的URL
三、 代码编写
3.1 构造请求
在settings.py
中定义变量MAX_PAGE
作为爬取页数,并将ROBOTSTXT_OBEY
置为FALSE
在images.py
中定义start_requests()
方法,用来生成350次请求
def start_requests(self):
data = {'queryWord': '单色釉瓷', 'word': '单色釉瓷'}
base_url = 'https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord='
for page in range(1, self.settings.get('MAX_PAGE') + 1):
data['pn'] = page * 30
url = base_url + quote(data['queryWord']) + '&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=0&word=' + \
quote(data['word']) + '&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&fr=&expermode=&pn=' + \
quote(str(data['pn'])) + '&rn=30&gsm=' + str(hex(data['pn']))
yield Request(url, self.parse)
运行爬虫,可看到链接都请求成功。
scrapy crawl images
3.2 提取信息
定义Item
,叫作BaiduimagesItem
from scrapy import Item, Field
class BaiduimagesItem(Item):
url = Field()
pass
提取Spider里有关信息。改写parse()
方法
def parse(self, response):
images = json.loads(response.body)['data']
for image in images:
item = BaiduimagesItem()
try:
item['url'] = image.get('thumbURL')
yield item
except Exception as e:
print(e)
pass
首先解析JSON,遍历其thumbURL
字段,取出图片信息,再对BaiduimagesItem
赋值,生成Item
对象
3.3 存储信息
下载图片需要用到Scrapy提供的ImagesPipeline
,首先定义存储文件的路径,需要定义一个IMAGES_STORE
变量,在settings.py
中添加:
IMAGES_STORE = './images'
内置的ImagesPipeline
会默认读取Item的image_urls
字段,并认为该字段是一个列表形式,它会遍历Item的image_urls
字段,然后取出每个URL进行图片下载。
但是现在生成的Item的图片链接字段并不是image_urls
表示的,也不是列表形式,而是单个的URL。所以为了实现下载,我们需要重新定义下载的部分逻辑,即要自定义ImagesPipeline
,继承内置的ImagesPipeline
,重写几个方法。
from scrapy import Request
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline
class BaiduimagesPipeline(ImagesPipeline):
def file_path(self, request, response=None, info=None):
url = request.url
file_name = url.split('/')[-1]
return file_name
def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem('Image Downloaded Failed')
return item
def get_media_requests(self, item, info):
yield Request(item['url'])
get_media_requests()
,第一个参数item
是爬取生成的Item对象。我们将它的url
字段取出来,然后直接生成Request
对象。此Request
加入到调度队列,等待被调度,执行下载。
file_path()
,它的第一个参数request
就是当前下载对应的Request
对象。这个方法用来返回保存的文件名,直接将图片链接的最后一部分当作文件名即可。它利用split()
函数分割链接并提取最后一部分,返回结果。这样此图片下载之后保存的名称就是该函数返回的文件名。
item_complete()
,它是当单个Item完成下载时的处理方法。因为并不是每张照片都会下载成功,所以我们需要分析下载结果并剔除下载失败的图片。该方法的第一个参数results
就是该Item对应的下载结果,它是一个列表形式,列表每一个元素是一个元组,其中包含了下载成功或失败的信息。这里我们遍历下载结果找出所有下载成功的下载列表。如果列表为空,那么该Item对应的图片下载失败,随机抛出异常DropItem,该Item忽略。否则返回该Item,说明此Item有效。
修改settings.py
,设置ITEM_PIPELINES
,启用ImagesPipeline
ITEM_PIPELINES = {'BaiduImages.pipelines.BaiduimagesPipeline': 1}
3.4 运行爬虫
scrapy crawl images
3.5 源码记录
3.5.1 Item.py
# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html
from scrapy import Item, Field
class BaiduimagesItem(Item):
url = Field()
pass
3.5.2 images.py
# -*- coding: utf-8 -*-
from scrapy import Spider, Request
from urllib.parse import quote
from BaiduImages.items import BaiduimagesItem
import json
class ImagesSpider(Spider):
name = 'images'
allowed_domains = ['images.baidu.com']
start_urls = ['https://images.baidu.com/']
def parse(self, response):
images = json.loads(response.body)['data']
for image in images:
item = BaiduimagesItem()
try:
item['url'] = image.get('thumbURL')
yield item
except Exception as e:
print(e)
pass
def start_requests(self):
data = {'queryWord': '关键词', 'word': '关键词'}
base_url = 'https://image.baidu.com/search/acjson?tn=resultjson_com&ipn=rj&ct=201326592&is=&fp=result&queryWord='
for page in range(1, self.settings.get('MAX_PAGE') + 1):
data['pn'] = page * 30
url = base_url + quote(data['queryWord']) + '&cl=2&lm=-1&ie=utf-8&oe=utf-8&adpicid=&st=-1&z=&ic=0&word=' + \
quote(data['word']) + '&s=&se=&tab=&width=&height=&face=0&istype=2&qc=&nc=1&fr=&expermode=&pn=' + \
quote(str(data['pn'])) + '&rn=30&gsm=' + str(hex(data['pn']))
yield Request(url, self.parse)
3.5.3 pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from scrapy import Request
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline
class BaiduimagesPipeline(ImagesPipeline):
def file_path(self, request, response=None, info=None):
url = request.url
file_name = url.split('/')[-1]
return file_name
def item_completed(self, results, item, info):
image_paths = [x['path'] for ok, x in results if ok]
if not image_paths:
raise DropItem('Image Downloaded Failed')
return item
def get_media_requests(self, item, info):
yield Request(item['url'])
四、 Python踩坑
4.1 URL编码
4.1.1 urlencode
urllib库里面有urlencode函数,可以把key-value这样的键值对转换成我们想要的格式,返回的是a=1&b=2这样的字符串,比如:
如果只想对一个字符串进行urlencode转换,则使用urllib提供的另外一个函数:quote()
3##4.1.2 urldecode
当urlencode之后的字符串传递过来之后,接收完毕就要解码了。urllib提供了unquote()这个函数,没有urldecode()!
参考链接:
https://blog.csdn.net/dodouaj/article/details/54908665
https://blog.csdn.net/qq_25109263/article/details/79445085
http://www.maiziedu.com/wiki/crawler/photograph/
https://blog.csdn.net/qq_32166627/article/details/60882964
https://blog.csdn.net/haoni123321/article/details/15814111