在Scrapy框架中,提供了一个CrawlSpider爬虫,这个爬虫会自动对所有符合特定条件的url地址进行爬取,我们无需再通过yield Request的方式爬取。
我们首先创建一个项目,在项目目录下使用下面的代码创建一个CrawlSpider,
scrapy genspider -t crawl 爬虫名称 "目标url二级域名"
创建好后,我们会在spiders文件夹下,找到爬虫文件,Scrapy提供的代码如下,
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class WxxcxSpider(CrawlSpider):
name = 'wxxcx'
allowed_domains = ['wxapp-union.com']
start_urls = ['http://www.wxapp-union.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
我们可以看到,相比较普通的爬虫,crawlspider多了一个rules变量,rules变量中的allow参数是目标url的格式,可以使用正则来表示url条件;callback参数是当url符合allow条件时,爬虫回调的函数;follow参数表示是否对url页面中的url进行进一步爬取,即url是否是最终的子页面。
我们这里通过crawlspider对微信小程序社区的页面进行爬取,获取页面的标题、作者、发布时间和内容,
首先,我们先建立好item模型,
import scrapy
class WxxcxItem(scrapy.Item):
title = scrapy.Field()
author = scrapy.Field()
pub_time = scrapy.Field()
content = scrapy.Field()
我们打开微信小程序社区,我们查看url可以发现,文章列表的url格式是“http://www.wxapp-union.com/portal.php?mod=list&catid=1&page=X”,其中X表示页数,所以,我们将start_urls参数设置为“http://www.wxapp-union.com/portal.php?mod=list&catid=1&page=1”;将rules变量中的allow参数设置为r’.+mod=list&catid=1&page=\d’,意为只要page参数值是数字,即为目标url。但是,我们只是获取了目标文章列表url,其并不是我们的最终目标url,所以,我们不回调函数,继续对url中的url进行爬取。所以follow参数值为True。
我们打开几篇文章,查看他们的url,例如,“http://www.wxapp-union.com/article-5947-1.html”,“http://www.wxapp-union.com/article-6146-1.html”。可以看出,只有article后面的数值发生了变化,我们将rules变量中的allow参数设置为r’.+article-.+\.html’,注意,.
需要转义。因为是最终url,我们将follow参数值为False,并回调函数。
爬虫代码如下,
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import WxxcxItem
class WxxcxSpider(CrawlSpider):
name = 'wxxcx'
allowed_domains = ['wxapp-union.com']
start_urls = ['http://www.wxapp-union.com/portal.php?mod=list&catid=1&page=1']
rules = (
Rule(LinkExtractor(allow=r'.+mod=list&catid=1&page=\d'), follow=True),
Rule(LinkExtractor(allow=r'.+article-.+\.html'), callback='parse_detail', follow=False),
)
def parse_detail(self, response):
title = response.xpath('//h1[@class="ph"]/text()').get()
author_p = response.xpath('//p[@class="authors"]')
author = author_p.xpath('.//a/text()').get()
pub_time = author_p.xpath('.//span/text()').get()
content = response.xpath('//td[@id="article_content"]/p/text()').getall()
content = "".join(content).strip()
item = WxxcxItem(title=title, author=author, pub_time=pub_time, content=content)
yield item # 注意返回item,使用yield,不要使用return
pipelines文件代码如下,
from itemadapter import ItemAdapter
from scrapy.exporters import JsonLinesItemExporter
class WxxcxPipeline:
def __init__(self):
self.fp = open("wxxcx.json", "wb")
self.exporter = JsonLinesItemExporter(self.fp, ensure_ascii=False, encoding='utf-8')
self.exporter.start_exporting()
def process_item(self, item, spider):
self.exporter.export_item(item)
return item
def close_spider(self, spider):
self.fp.close()
最后,将settings文件中的ROBOTSTXT_OBEY、DOWNLOAD_DELAY、DEFAULT_REQUEST_HEADERS和ITEM_PIPELINES取消注释,并修改相应的值。
如果,我们需要将页面中的图片按照页面标题的名称保存到相应的目录下,我们需要对pipelines管道文件尽心修改。
我们可以使用__file__获取当前文件位置,再通过os模块的path.dirname()找到上一级文件目录,使用os模块的path.join()方法对文件目录进行拼接,如果需要创建文件夹则使用os模块的mkdir()方法。
下面,我们下面爬取斗图啦网站的图片,并按照他们相应的标题创建文件夹并保存。
首先,创建item模型,
import scrapy
class DtlItem(scrapy.Item):
category = scrapy.Field() # 图片标题
img_url = scrapy.Field() # 图片下载链接
爬虫文件,
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import DtlItem
class DtlSpider(CrawlSpider):
name = 'dtl'
allowed_domains = ['doutula.com']
start_urls = ['https://www.doutula.com/article/list/?page=1']
rules = (
Rule(LinkExtractor(allow=r'.+page=\d'), callback='parse_urls', follow=False),
)
def parse_urls(self, response):
item = {}
aes = response.xpath('//a[@class="list-group-item random_list tg-article"]')
for a in aes:
category = a.xpath('.//div[@class="random_title"]/text()').get()
images = a.xpath('.//img[@class!="gif"]/@data-original').getall()
for image in images:
img_url = image
item = DtlItem(category=category, img_url=img_url)
yield item
pipelines管道文件,
class DtlPipeline:
def __init__(self):
# 寻找图片主文件夹,如果没有,则创建
self.path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "images")
if not os.path.exists(self.path):
os.mkdir(self.path)
def process_item(self, item, spider):
category = item['category'] # 获取图片标题
img_url = item['img_url'] # 获取图片下载地址
image_name = img_url.split("/")[-1] # 获取图片名称
# 寻找图片相应的文件夹,如果没有,则创建
category_path = os.path.join(self.path, category)
if not os.path.exists(category_path):
os.mkdir(category_path)
request.urlretrieve(img_url, os.path.join(category_path, image_name)) # 下载图片
return item
Scrapy为下载item中包含的文件(比如在爬取到产品时,同时也想保存对应的图片)提供了一个可重用的item pipelines
。这些pipeline
有些共同的方法和结构(我们称之为media pipeline
)。一般来说你会使用Files Pipeline
或者Images Pipeline
。
Scrapy内置下载器有以下优点,
使用Files Pipeline
下载文件的时候,按照以下步骤来完成:
Item
,然后在这个item
中定义两个属性,分别为file_urls
以及files
。file_urls
是用来存储需要下载的文件的url链接,需要给一个列表item
的files
属性中。比如下载路径、下载的url和文件的校验码等settings.py
中配置FILES_STORE
,这个配置是用来设置文件下载下来的路径pipeline
:在ITEM_PIPELINES
中设置scrapy.pipelines.files.FilesPipeline:1
,并将原有字段注释当使用Images Pipeline
下载文件的时候,按照以下步骤来完成:
Item
,然后在这个item
中定义两个属性,分别为image_urls
以及images
。image_urls
是用来存储需要下载的图片的url链接,需要给一个列表item
的images
属性中。比如下载路径、下载的url和图片的校验码等settings.py
中配置IMAGES_STORE
,这个配置是用来设置图片下载下来的路径pipeline
:在ITEM_PIPELINES
中设置scrapy.pipelines.images.ImagesPipeline:1
,并将原有字段注释但是,Scrapy内置下载器会将图片全部储存到一个名为full
的文件夹下,我们需要重写方法。
我们创建一个类并继承于scrapy.pipelines.images.ImagesPipeline类,我们查看ImagesPipeline类的代码,并搜索“full”,可以找到这样的代码,
def file_path(self, request, response=None, info=None):
image_guid = hashlib.sha1(to_bytes(request.url)).hexdigest()
return 'full/%s.jpg' % (image_guid)
说明这段代码返回的是图片存储位置,我们重写这段代码。
继续分析代码,
def get_media_requests(self, item, info):
urls = ItemAdapter(item).get(self.images_urls_field, [])
return [Request(u) for u in urls]
这段代码是用来返回图片下载链接的,我们同样需要重写。
重写之后的pipelines文件代码如下,
class DtlImagesPipeline(ImagesPipeline):
# 获取图片下载链接
def get_media_requests(self, item, info):
request_objs = super(DtlImagesPipeline, self).get_media_requests(item, info)
# 将item中的信息绑定到request对象上,以便获取图片标题
for request_obj in request_objs:
request_obj.item = item
return request_objs
# 返回图片存储路径
def file_path(self, request, response=None, info=None):
path = super(DtlImagesPipeline, self).file_path(request, response, info)
category = request.item.get('category') # 获取request对象上的图片标题信息
image_store = settings.IMAGES_STORE # 设定存储图片主目录位置
category_path = os.path.join(image_store, category) # 根据图片标题寻找文件夹,如果没有,创建
if not os.path.exists(category_path):
os.mkdir(category_path)
image_name = path.replace('full/', "") # 获取图片名称
image_path = os.path.join(category_path, image_name)
return image_path
items文件代码如下,
import scrapy
class DtlItem(scrapy.Item):
category = scrapy.Field()
image_urls = scrapy.Field()
images = scrapy.Field()
爬虫文件代码如下,
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from ..items import DtlItem
class DtlSpider(CrawlSpider):
name = 'dtl'
allowed_domains = ['doutula.com']
start_urls = ['https://www.doutula.com/article/list/?page=1']
rules = (
Rule(LinkExtractor(allow=r'.+page=\d'), callback='parse_urls', follow=False),
)
def parse_urls(self, response):
aes = response.xpath('//a[@class="list-group-item random_list tg-article"]')
for a in aes:
category = a.xpath('.//div[@class="random_title"]/text()').get()
images = a.xpath('.//img[@class!="gif"]/@data-original').getall()
images = list(map(lambda url: response.urljoin(url), images)) # 返回下载链接列表
item = DtlItem(category=category, image_urls=images)
yield item
settings文件中的ITEM_PIPELINES参数和IMAGES_STORE参数如下,
import os
ITEM_PIPELINES = {
'test.pipelines.DtlImagesPipeline': 300,
}
IMAGES_STORE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'images')