爬虫之scrapy框架
1.scrapy框架介绍
Scrapy是用纯Python实现的一个为了爬取网站数据、提取结构性数据而编写的应用框架
Scrapy 特色是使用了 Twisted异步网络框架来处理网络通讯,加快了下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求
1.1 scrapy框架架构图
Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。
Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方.
Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。
Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
注意!只有当调度器中不存在任何request了,整个程序才会停止,(也就是说,对于下载失败的URL,Scrapy也会重新下载。)
1.2 scrapy安装
#Windows平台 1、pip3 install wheel #安装后,便支持通过wheel文件安装软件 3、pip3 install lxml 4、pip3 install pyopenssl 5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/ # 根据电脑Python版本和位数下载并安装最新版的pywin32,它会自动寻找Python的安装路径,所以不需要做任何修改,一直单击【下一步】即可。 # 这里有时候会报停止工作,但是通过pip3 list 命令可以看到他已经存在 6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted 7、执行pip3 install 下载目录路径\Twisted-17.9.0-cp36-cp36m-win_amd64.whl # Scrapy需要依赖Twisted。Twisted是Python下面一个非常重要的基于事件驱动的IO引擎。Twisted的安装依赖于pywin32 8、pip3 install scrapy #Linux平台 1、pip3 install scrapy
# 这里安装好pywin32后,后期可能还是无法运行(报ImportError: DLL load failed错误),这里一般都是我们该模块安装的有问题导致的,这里可以参考知乎或者参考Stack Overflow,
我的是参考Stack Overflow下的说明,执行了 pip install win10toast --ignore-installed 才好的
1.3 命令行相关指令
# 1 查看帮助 scrapy -h scrapy <command> -h # 2 有两种命令:其中Project-only必须切到项目文件夹下才能执行,而Global的命令则不需要 Global commands: startproject #创建项目 genspider #创建爬虫程序 settings #如果是在项目目录下,则得到的是该项目的配置 runspider #运行一个独立的python文件,不必创建项目 shell #scrapy shell url地址 在交互式调试,如选择器规则正确与否 fetch #独立于程单纯地爬取一个页面,可以拿到请求头 view #下载完毕后直接弹出浏览器,以此可以分辨出哪些数据是ajax请求 version #scrapy version 查看scrapy的版本,scrapy version -v查看scrapy依赖库的版本 Project-only commands: crawl #运行爬虫,必须创建项目才行,确保配置文件中ROBOTSTXT_OBEY = False check #检测项目中有无语法错误 list #列出项目中所包含的爬虫名 edit #编辑器,一般不用 parse #scrapy parse url地址 --callback 回调函数 #以此可以验证我们的回调函数是否正确 bench #scrapy bentch压力测试 # 3 官网链接 https://docs.scrapy.org/en/latest/topics/commands.html
1.4 框架结构
''' project_name/ scrapy.cfg # 项目的主配置信息,用来部署scrapy时使用 project_name/ __init__.py items.py # 设置数据存储模板,用于结构化数据,如:Django的Model pipelines.py # 数据处理行为,如:一般结构化的数据持久化 settings.py # 配置文件,如:递归的层数、并发数,延迟下载等。强调:配置文件的选项必须大写否则视为无效,正确写法USER_AGENT='xxxx' spiders/ # 爬虫目录,如:创建文件,编写爬虫规则 __init__.py 爬虫1.py # 爬虫程序1 爬虫2.py 爬虫3.py '''
1.5 项目流程
新建项目 (scrapy startproject xxx):新建一个新的爬虫项目 明确目标 (编写items.py):明确你想要抓取的目标 制作爬虫 (spiders/xxspider.py):制作爬虫开始爬取网页 存储内容 (pipelines.py):设计管道存储爬取内容
1.6 启动一个项目
1 scrapy startproject DianShang # 创建爬虫项目 2 scrapy genspider jd jd.com # 生成一个爬虫程序 3 scrapy crawl jd # 运行scrapy项目
现在我们有了基本的项目骨架
1.7 spider类的说明
Spiders是为站点爬网和解析页面定义自定义行为的地方
spiders下的jd.py文件
# -*- coding: utf-8 -*- import scrapy class JdSpider(scrapy.Spider): name = 'jd' allowed_domains = ['jd.com'] start_urls = ['http://jd.com/'] def parse(self, response): pass
Spider类中的start_requests方法:
def start_requests(self): cls = self.__class__ if method_is_overridden(cls, Spider, 'make_requests_from_url'): warnings.warn( "Spider.make_requests_from_url method is deprecated; it " "won't be called in future Scrapy releases. Please " "override Spider.start_requests method instead (see %s.%s)." % ( cls.__module__, cls.__name__ ), ) for url in self.start_urls: yield self.make_requests_from_url(url) else: for url in self.start_urls: yield Request(url, dont_filter=True)
有时候后台的start_requests方法会访问不到我们的目标站点,我们大多数情况需要自己构造此方法,它的最终返回值是用生成器yield返回的,我们自己写也建议使用生成器。
start_requests方法它默认的回调函数就是parse
2. 创建项目
我们的目标是爬取亚马逊商城iphoex的名称,价格以及配送方,注意的是:我们需要的这些信息都在手机详情页面,而在手机列表页面只有我们点击它的图片或者文字才会看到手机详细信息
想要获取手机的信息,现在我们需要进行分布爬取,第一次先获取每一个手机详情页面的url,可以通过手机列表页面的图片进行获取,也能通过文字获取,然后通过二次解析去拿到我们需要的信息
拿到信息后,通过MongoDB对我们的信息做持久化处理
2.1 创建项目文件
爬虫Scrapy命令: 1 scrapy startproject Amazon # 创建爬虫项目 2 scrapy genspider amazon amazon.cn # 生成一个爬虫程序 3 scrapy crawl amazon # 运行scrapy项目
创建好后 spiders下自动生成的amazon.py文件
# -*- coding: utf-8 -*- import scrapy class AmazonSpider(scrapy.Spider): name = 'amazon' allowed_domains = ['amazon.cn'] start_urls = ['http://amazon.cn/'] def parse(self, response): pass
2.2 创建启动文件
创建好后,它默认只能在终端运行,我们可以在它的根目录下创建一个bin.py文件,来作为它的执行文件
from scrapy.cmdline import execute execute(["scrapy","crawl","amazon",'--nolog']) # 不打印日志信息
如果不需要相关日志信息,,可以在列表后面追加一个参数:'--nolog'
2.3 关闭ROBOTSTXT_OBEY命令
关闭setting下的ROBOTSTXT_OBEY命令,该命令的作用是让你遵循爬虫协议的情况下爬取相关内容,我们为了避免它对我们爬取时的影响,可以把他修改为False
2.4 获取商品信息
获取商品列表的详情链接
amazon.py
import scrapy from scrapy import Request # 导入模块 class AmazonSpider(scrapy.Spider): name = 'amazon' allowed_domains = ['amazon.cn'] # 自定义配置,在Spider中custom_settings设置的是None custom_settings = { 'REQUEST_HEADERS': { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36', } } def start_requests(self): r1 = Request( url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex", headers=self.settings.get('REQUEST_HEADERS'), ) yield r1 def parse(self, response): # 获取商品名 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract() # 商品单个商品详情链接 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract() # 获取整个页面商品详情链接 # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract() detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract() print(detail_urls)
现在让scrapy去访问这些链接,只要parse函数返回一个Request对象,它就会放到异步请求列表里面,并由twisted发送异步请求
import scrapy from scrapy import Request # 导入模块 class AmazonSpider(scrapy.Spider): name = 'amazon' allowed_domains = ['amazon.cn'] # 自定义配置,在Spider中custom_settings设置的是None custom_settings = { 'REQUEST_HEADERS': { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36', } } def start_requests(self): r1 = Request( url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex", headers=self.settings.get('REQUEST_HEADERS'), ) yield r1 def parse(self, response): # 获取商品名 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract() # 商品单个商品详情链接 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract() # 获取整个页面商品详情链接 # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract() detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract() for url in detail_urls: yield Request(url=url, headers=self.settings.get('REQUEST_HEADERS'), # 请求头 callback=self.parse_detail, # 回调函数 dont_filter=True # 不去重 ) def parse_detail(self, response): # 获取商品详细信息 # 商品名,获取第一个结果 name = response.xpath('//*[@id="productTitle"]/text()').extract_first() if name: name = name.strip() # 商品价格 price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first() # 配送方式 delivery = response.xpath('//*[@id="ddmMerchantMessage"]/*[1]/text()').extract_first() print(name, price, delivery)
2.5 存储商品信息到MongoDB
我们需要使用items.py文件
import scrapy # 获取你想要的字段 class AmazonItem(scrapy.Item): # define the fields for your item here name = scrapy.Field() price= scrapy.Field() delivery=scrapy.Field()
并修改amazon.py最后的数据返回值
amazon.py
import scrapy from scrapy import Request # 导入模块 from Amazon.items import AmazonItem class AmazonSpider(scrapy.Spider): name = 'amazon' allowed_domains = ['amazon.cn'] # 自定义配置,在Spider中custom_settings设置的是None custom_settings = { 'REQUEST_HEADERS': { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36', } } def start_requests(self): r1 = Request( url="https://www.amazon.cn/s/ref=nb_sb_ss_i_3_6?field-keywords=iphonex", headers=self.settings.get('REQUEST_HEADERS'), ) yield r1 def parse(self, response): # 获取商品名 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/h2').extract() # 商品单个商品详情链接 # detail_urls = response.xpath('//*[@id="result_0"]/div/div[3]/div[1]/a/@href').extract() # 获取整个页面商品详情链接 # detail_urls = response.xpath('//li[contains(@id,"result_")]/div/div[3]/div[1]/a/@href').extract() detail_urls = response.xpath('//*[starts-with(@id,"result")]/div/div[3]/div[1]/a/@href').extract() for url in detail_urls: yield Request(url=url, headers=self.settings.get('REQUEST_HEADERS'), # 请求头 callback=self.parse_detail, # 回调函数 dont_filter=True # 不去重 ) def parse_detail(self, response): # 获取商品详细信息 # 商品名,获取第一个结果 name = response.xpath('//*[@id="productTitle"]/text()').extract_first() if name: name = name.strip() # 商品价格 price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first() # 配送方式 delivery = response.xpath('//*[@id="ddmMerchantMessage"]/*[1]/text()').extract_first() # 生成标准化数据 item = AmazonItem() # 实例化 # 增加键值对 item["name"] = name item["price"] = price item["delivery"] = delivery return item # 返回的是一个字典
处理Spider中获取到的Item ,需要PipeLine将数据储存到MongoDB中
pipelines.py
from pymongo import MongoClient class MongodbPipeline(object): def __init__(self, host, port, db, table): self.host = host self.port = port self.db = db self.table = table @classmethod def from_crawler(cls, crawler): """ Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完 成实例化 """ HOST = crawler.settings.get('HOST') PORT = crawler.settings.get('PORT') DB = crawler.settings.get('DB') TABLE = crawler.settings.get('TABLE') return cls(HOST, PORT, DB, TABLE) def open_spider(self, spider): """ 爬虫刚启动时执行一次 """ # self.client = MongoClient('mongodb://%s:%s@%s:%s' %(self.user,self.pwd,self.host,self.port)) self.client = MongoClient(host=self.host, port=self.port) def close_spider(self, spider): """ 爬虫关闭时执行一次 """ self.client.close() def process_item(self, item, spider): # 操作并进行持久化 d = dict(item) if all(d.values()): self.client[self.db][self.table].insert(d) print("添加成功一条")
修改settings.py,在下面增加MongoDB连接信息
# MongoDB连接信息 HOST="127.0.0.1" PORT=27017 DB="amazon" # 数据库名 TABLE="goods" # 表名
同时开启MongoDB的PipeLine信息,注意这里开启后还需要进行修改,我们pipelines下的名称是MongodbPipeline
ITEM_PIPELINES = { 'Amazon.pipelines.MongodbPipeline': 300, }
此时在cmd下启动我们的mongodb(mongod),进入我们的数据库(mongo),并自行创建数据库
执行我们的bin文件,此时会在你会发现数据都存储进我们的数据库内
2.6 存储商品信息到本地
我们只需要在pipelines.py里面再添加一个相关的类即可
class FilePipeline(object): def __init__(self, file_path): self.file_path=file_path @classmethod def from_crawler(cls, crawler): """ Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完 成实例化 """ file_path = crawler.settings.get('FILE_PATH') return cls(file_path) def open_spider(self, spider): """ 爬虫刚启动时执行一次 """ print('==============>爬虫程序刚刚启动') self.fileobj=open(self.file_path,'w',encoding='utf-8') def close_spider(self, spider): """ 爬虫关闭时执行一次 """ print('==============>爬虫程序运行完毕') self.fileobj.close() def process_item(self, item, spider): # 操作并进行持久化 print("items----->",item) # return表示会被后续的pipeline继续处理 d = dict(item) if all(d.values()): self.fileobj.write("%s\n" %str(d)) return item # 表示将item丢弃,不会被后续pipeline处理 # raise DropItem()
然后再在setting配置文件中配置ITEM_PIPELINES信息以及FILE_PATH信息即可
ITEM_PIPELINES = { 'DianShang.pipelines.MongodbPipeline': 300, # 优先级300 'DianShang.pipelines.FilePipeline': 500, # 优先级500先执行 } FILE_PATH="pipe.txt" # 文件名称
2.7 利用代理池进行目标爬取
这里主要是考虑目标网站检测我们ip问题,一般我们需要大量爬取相关信息,需要使用代理池(在github上搜索下载),每次更换我们的ip地址,下载后放入本地,根据readme文件进行相关配置
这里还需要使用中间件。从我们开始的那张图可以看到我们整个流程两个地方具有中间件:爬取内容的时候经过SpiderMidderware,存储的时候经过DownerMidderware,需要进行如下操作:
#1、与middlewares.py同级目录下新建proxy_handle.py import requests def get_proxy(): return requests.get("http://127.0.0.1:5010/get/").text def delete_proxy(proxy): requests.get("http://127.0.0.1:5010/delete/?proxy={}".format(proxy)) #2、middlewares.py from Amazon.proxy_handle import get_proxy,delete_proxy class DownMiddleware1(object): def process_request(self, request, spider): """ 请求需要被下载时,经过所有下载器中间件的process_request调用 :param request: :param spider: :return: None,继续后续中间件去下载; Response对象,停止process_request的执行,开始执行process_response Request对象,停止中间件的执行,将Request重新调度器 raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception """ proxy="http://" + get_proxy() request.meta['download_timeout']=20 request.meta["proxy"] = proxy print('为%s 添加代理%s ' % (request.url, proxy),end='') print('元数据为',request.meta) def process_response(self, request, response, spider): """ spider处理完成,返回时调用 :param response: :param result: :param spider: :return: Response 对象:转交给其他中间件process_response Request 对象:停止中间件,request会被重新调度下载 raise IgnoreRequest 异常:调用Request.errback """ print('返回状态吗',response.status) return response def process_exception(self, request, exception, spider): """ 当下载处理器(download handler)或 process_request() (下载中间件)抛出异常 :param response: :param exception: :param spider: :return: None:继续交给后续中间件处理异常; Response对象:停止后续process_exception方法 Request对象:停止中间件,request将会被重新调用下载 """ print('代理%s,访问%s出现异常:%s' %(request.meta['proxy'],request.url,exception)) import time time.sleep(5) delete_proxy(request.meta['proxy'].split("//")[-1]) request.meta['proxy']='http://'+get_proxy() return request
具体操作可以参考博客
项目最终骨架: