05 Scrapy框架
一. 介绍
'''
介绍: 通用的网络爬虫框架, 可以说它是爬界的django
作用: 网络页面抓取
起源: 由twisted框架开发而来, 开发的Scrapy是非阻塞
图分析: 五大组件 Components
1. spiders /ˈspaɪdə(r)z/ 网页爬虫
作用: 开发人员自定义的类, 解析response, 提取items, 或者发送新的请求request
2. ENGINE /ˈendʒɪn/ 引擎
作用: 负责控制数据的流向(总管)
3. SCHEDULER /ˈʃedjuːlə(r)/ 调度器
作用: 去重, 决定下一个通过配置实现深度优先爬取还是广度优先爬取 (原理: 队列 -> 广度, 堆栈 -> 深度), 由它来决定下一个要抓取的网址是什么.
4. DOWNLOADER 下载器
作用: 用于下载网页内容. 并将网页内容返回给EGINE, 下载器是建立在twisted这个搞笑的异步模型之上.
注意: 进过下载中间件过来. 中间件中可以使用user-agent, proxies
5. ITEM PIPELINES 项目管道
作用: 在items被提取后负责处理它们,主要包括清理、验证、持久化(比如: 存到数据库)等操作
两大中间件:
1. spiders和egine之间: 处理输入, 输出
2. downloader和egine之间: 加proxies, headers, 集成selenium
'''
二. Scrapy安装(windows, mac, linux)
'''
一. pip3 install scrapy
二. Windows平台(成功率80%)
1、pip3 install wheel # 安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
3、pip3 install lxml
4、pip3 install pyopenssl
5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
8、pip3 install scrapy
# Linux平台
1、pip3 install scrapy
'''
三. Scrapy创建项目, 创建爬虫, 运行爬虫
'''
# 1. scrapy startproject 项目名
scrapy startproject first_scrapy
# 2. 创建爬虫
cd first_scrapy
scrapy genspider 爬虫名 爬虫地址
scrapy genspider chouti dig.chouti.com
执行就会在spider文件夹下创建出一个py文件,名字叫chouti
# 3. 运行爬虫
scrapy crawl chouti # 带运行日志
scrapy crawl chouti --nolog # 不带日志
# 4. 支持右键执行爬虫
在项目路径下新建一个.py文件 推荐: main.py
from scrapy.cmdline import execute
execute(['scrapy','crawl','chouti','--nolog'])
'''
四. 目录介绍
first_scrapy/ # 项目名
├── first_scrapy/ # 包
├── spiders/ # 所有的爬虫文件存放路径
├── __init__.py
├── baidu.py # 新建的爬虫文件1
├── chouti.py # 新建的爬虫文件2
├── __init__.py
├── items.py # items类
├── main.py # 自己创建, 作为启动脚本
├── middlewares.py # 中间件(爬虫,下载中间件都写在这)
├── pipelines.py # 持久化相关(items.py中类的对象)
└── settings.py # 配置文件
└── scrapy.cfg # 上线相关
五. settings 介绍
ROBOTSTXT_OBEY = False # 关闭默认遵守的爬虫协议, 默认情况,scrapy会去遵循爬虫协议
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'
LOG_LEVEL = 'ERROR'
六. 解析器的使用方式
1. 在爬取页面以后爬取其他网址
def parse(self, response):
print(response.status)
from scrapy.http.request import Request
return Request('https://www.baidu.com', dont_filter=True)
2. 使用第三方解析
def parse(self, response):
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'lxml')
div_list = soup(class_='link-con')
print(div_list)
for div in div_list:
image_url = div.a.get('data-pic')
desc = div.find(class_='link-title').text
print(f"""
图片链接: {image_url}
内容详情: {desc}
""")
3. 使用自带解析
# 自带解析: xpath, css
resposne.xpath('xpath语法')
resposne.css('css语法')
# css
取文本: css('::text')
取属性: css('::attr(href)')
# xpath
取文本: xpath("/text()")
取属性: xpath("/@href")
# 取值: 注意: 如果指定取出来的结果是一个 Selector 对象
取出列表: .extract()
取出单个: .extract_first()
1) 使用css解析
def parse(self, response):
# 使用css解析
div_list = response.css('.link-item')
print(len(div_list))
for div in div_list:
desc = div.css('.link-title::text').extract_first()
image_url = div.css('.common-matching-con::attr(data-pic)').extract_first()
if not desc:
continue
print(f"""
文本描述: {desc}
图片链接: {image_url}
""")
2) 使用xpath解析
def parse(self, response):
# 使用xpath解析
div = response.xpath('//div[contains(@class, "link-item")]')[0]
desc_list = div.xpath('//a[contains(@class, "link-title")]/text()').extract()
image_url_list = div.xpath('//a[contains(@class, "common-matching-con")]/@data-pic').extract()
for desc, image_url in zip(desc_list, image_url_list):
if not desc:
continue
print(f"""
文本描述: {desc}
图片链接: {image_url}
""")
七. 数据持久化的两种方式
'''
持久化方案一:
提示: parse函数中必须返回列表套自己的格式 [{}, {}, ...]
scrapy crawl chouti -o chouti.csv
scrapy crawl chouti -o chouti.json
持久化支持格式: 'json', 'jsonl
ines', 'jl', 'csv', 'xml', 'marshal', 'pickle'
持久化方案二: 通过pipline item存储
1. items.py中定义items类
2. 在spinder中导入, 实例化, 把数据放入(注意: 使用yield)
item[key] = value
yield item
3. 配置文件中找到 ITEM_PIPELINES 配置pipelines.py中定义的pipelines类
ITEM_PIPELINES = {
'first_scrapy.pipelines.pipelines.py中定义的pipelines类': 300, # 数子越小级别越高
}
4. 在piplines.py中写配置的类
open_spider(self, spider) 开
process_item(self, item, spider)
注意: 一定要return item
close_spider(self, spider) 关
'''
1. 持久化方案一
def parse(self, response):
li = []
div = response.xpath('//div[contains(@class, "link-item")]')[0]
desc_list = div.xpath('//a[contains(@class, "link-title")]/text()').extract()
image_url_list = div.xpath('//a[contains(@class, "common-matching-con")]/@data-pic').extract()
li = []
for desc, image_url in zip(desc_list, image_url_list):
li.append({'desc': desc, 'image_url': image_url})
return li
# 命令行终端中执行持久化生成对应格式的文件:
scrapy crawl chouti -o chouti.csv
2. 持久化方案二
1) items.py
class ChouTiScrapyItem(scrapy.Item):
desc = scrapy.Field()
image_url = scrapy.Field()
2) spiders/chouti.py
def parse(self, response):
from first_scrapy.items import ChouTiScrapyItem
item = ChouTiScrapyItem()
div_list = response.css('.link-item')
print(len(div_list))
for div in div_list:
desc = div.css('.link-title::text').extract_first()
image_url = div.css('.common-matching-con::attr(data-pic)').extract_first()
if not desc or not image_url:
continue
item['desc'] = desc
item['image_url'] = image_url
yield item
3) pipelines.py: 书写存储到文件, mysql, redis中的持久化类
import json
import pymysql
from redis import Redis
from itemadapter import ItemAdapter
class FirstScrapyPipeline:
def process_item(self, item, spider):
return item
class ChouTiFilePipline:
"""存储到文件中的持久化类"""
def open_spider(self, spider):
print('ChouTiFilePipline 开始!')
self.f = open('chouti.json', 'w', encoding='utf-8')
def process_item(self, item, spider):
data = {'desc': item.get('desc'), 'image_url': item['image_url']}
json.dump(data, self.f, ensure_ascii=False)
self.f.write('\n\n')
# 注意: 这里一定要返回item, 不然后续的持久化类获取的值就是None
return item
def close_spider(self, spider):
print("ChouTiFilePipline 结束!")
self.f.close()
class ChouTiMySQLPipline:
"""存储到MySQL中的持久化类"""
def open_spider(self, spider):
print('ChouTiMySQLPipline 开始!')
self.conn = pymysql.connect(
host='127.0.0.1', user="root", password="123",
database='crawler', port=3306, charset='utf8'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = 'insert into chouti(desc_content, image_url) values(%s, %s);'
try:
self.cursor.execute(sql, [item.get("desc"), item.get('image_url')])
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self, spider):
print('ChouTiMySQLPipline 结束!')
self.cursor.close()
self.conn.close()
class ChouTiReidsPipline:
"""存储到redis中的持久化类"""
def open_spider(self, spider):
print('ChouTiReidsPipline 开始!')
self.conn = Redis.from_url('redis://127.0.0.1/0')
def process_item(self, item, spider):
data = {'desc': item.get('desc'), 'image_url': item['image_url']}
pipe = self.conn.pipeline(transaction=True)
pipe.multi() # 启动事务
self.conn.rpush('chouti', json.dumps(data, ensure_ascii=False))
pipe.execute() # 提交
return item
def close_spider(self, spider):
print('ChouTiReidsPipline 结束!')
self.conn.close()
八. scrapy的请求传参
# 传递: 把要传递的数据放到meta中
yield Request(url, meta={'item': item})
# 取值: 在response对象中取出来
item = response.meta.get('item')
# 实现思路:
downloader中将出去的request中包含的item, 保存. 在response返回的时候赋值给response. 之后通过engine调度返回给spider.
实例: 全站爬取cnblogs
'''
爬取原则: scrapy默认是先进先出
深度优先:详情页先爬 队列,先进先出
广度优先:每一页先爬 堆栈,后进先出
'''
# spiders/cnblogs.py
import scrapy
from scrapy.http.request import Request
from first_scrapy.items import CnblogsScrapyItem
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['www.cnblogs.com']
start_urls = ['http://www.cnblogs.com/']
def parse(self, response):
article_list = response.css('#post_list .post-item')
for article in article_list:
desc = article.css('.post-item-summary::text').extract_first().strip()
title_url = article.css('.post-item-title::attr(href)').extract_first()
title = article.css('.post-item-title::text').extract_first()
item = CnblogsScrapyItem()
item['title_url'] = title_url
item['title'] = title
item['desc'] = desc.strip()
# 继续爬取详情. 提示: callback如果不写,默认回调到parse方法, 如果写了,响应回来的对象就会调到自己写的解析方法中.
yield Request(title_url, callback=self.parse_article, meta={'item': item})
next_page_url = 'https://www.cnblogs.com' + response.css(
'#paging_block a:last-child::attr(href)').extract_first()
yield Request(next_page_url)
def parse_article(self, response):
# content = response.css('//[@id="cnblogs_post_body"]').extract()
content = response.css('#cnblogs_post_body').extract_first()
item = response.meta.get('item')
item['content'] = content
yield item
# items.py
class CnblogsScrapyItem(scrapy.Item):
"""
item['title_url'] = title_url
item['title'] = title
item['desc'] = desc
item['content'] = content
"""
title_url = scrapy.Field()
content = scrapy.Field()
desc = scrapy.Field()
title = scrapy.Field()
# settings.py
ITEM_PIPELINES = {
# cnblogs
'first_scrapy.pipelines.CnblogsMySQLPipline': 301,
}
# pipelines.py
import json
import pymysql
class CnblogsMySQLPipline:
"""将爬取的Cnblogs存储到MySQL中进行持久化"""
def open_spider(self, spider):
print('CnblogsMySQLPipline 开始!')
self.conn = pymysql.connect(
host='127.0.0.1', user="root", password="123",
database='crawler', port=3306, charset='utf8'
)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
sql = 'insert into cnblogs(title, title_url, `desc`, content) values(%s, %s, %s, %s);'
try:
self.cursor.execute(sql, [item['title'], item['title_url'], item['desc'], item['content']])
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
return item
def close_spider(self, spider):
print('CnblogsMySQLPipline 结束!')
self.cursor.close()
self.conn.close()
九. 提升scrapy爬取数据的效率
提示: 以下的实现在配置文件中进行相关的配置即可(注意: 默认还有一套setting)
# 1. 增加并发:CONCURRENT_REQUESTS = 100
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改 CONCURRENT_REQUESTS = 100 值为100,并发设置成了为100。
# 2. 降低日志级别: LOG_LEVEL = 'INFO'
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:LOG_LEVEL = 'ERROR'
# 3. 禁止cookie:COOKIES_ENABLED = False
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:COOKIES_ENABLED = False
# 4. 禁止重试:RETRY_ENABLED = False
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:RETRY_ENABLED = False
# 5. 减少下载超时:DOWNLOAD_TIMEOUT = 10
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:DOWNLOAD_TIMEOUT = 10 超时时间为10s
十. scrapy的中间件: 下载中间件
1. process_request
def process_request(self, request, spider):
# 返回值:
# 1. return None: 请求正常继续往后走
# 2. return Response: 请求不出去, 而是直接原路返回交给engine进行处理
# 3. return Request: 请求不出去, 而是直接原路返回交给engine进行处理
# 4. raise: 一旦抛出异常就会交给process_exception方法处理. 应用场景: 筛选排除不爬取的request.
1) 加随机请求头: fake-useragent 或者 Faker
方式一: 使用fake-useragent
# 1. 下载:
pip3 install fake-useragent
# 2. 导入使用:
# 声明成中间件类属性
from fake_useragent import UserAgent
ua = UserAgent()
# 在process_request中使用: 返回值None
request.headers['User-Agent'] = ua.random
方式二: 使用Faker
# 1. 下载:
pip3 install faker
# 2. 导入使用:
# 声明成中间件类属性
from faker import Faker
f = Faker(locale='zh_CN')
# 在process_request中使用: 返回值None
request.headers['User-Agent'] = f.user_agent()
注意: headers类最终继承了dict因此可以使用字段赋值的方式给请求头加user-agent
from scrapy.http.headers import Headers
2) 加cookie
加cookie
# 在process_request中使用: 返回值None
request.cookies = {'cookie1': 'value1'}
加cookie池
# 声明成spiders类属性
cookie_pool = [{'cookie1': 'value1'}, {'cookie2': 'value2'}, ...]
# 在process_request中使用: 返回值None
imoport random
request.cookies = random.choice(spider.cookie_pool)
3) 加代理
加代理
# 在process_request中使用: 返回值None
request.meta['download_timeout'] = 20
request.meta['proxy'] = 'http://111.72.149.117:30057'
加代理池
# spiders中
import requests
class DemoSpider(scrapy.Spider):
def __init__(self, name=None, **kwargs):
super().__init__(name=None, **kwargs)
self.proxy = None
@staticmethod
def get_proxy():
return requests.get("http://127.0.0.1:5010/get/").json().get("proxy")
@staticmethod
def delete_proxy(proxy):
requests.get("http://127.0.0.1:5010/delete/?proxy={}".format(proxy))
def close(self, reason):
"""self就是spider对象"""
self.delete_proxy(self.proxy)
...
# 在process_request中使用: 返回值None
request.meta['download_timeout'] = 20
spider.proxy = spider.get_proxy()
request.meta['proxy'] = f'http://{spider.proxy}'
4) 加selenium
# spiders中
from selenium import webdriver
class DemoSpider(scrapy.Spider):
browser = webdriver.Chrome()
def close(self, reason):
"""self就是spider对象"""
self.browser.close()
# 在process_request中使用: 返回response对象
from scrapy.http import HtmlResponse, Response
spider.browser.get(request.url)
response = HtmlResponse(url=request.url, body=spider.browser.page_source.encode('utf-8'), request=request)
return response
2. process_response
def process_response(self, request, response, spider):
# 返回值
# 1. return Response: 正常继续执行.
# 2. return Request: 交给engine, 让engine进行处理以后继续往下走.
# 3. raise: 一旦抛出异常就不因该走ITEMS进行持久化, 因为这样的数无法解析一解析就会抛出异常! 应用场景: 排除爬取网页频繁从而出现的验证校验.
3. process_exception
def process_exception(self, request, exception, spider):
# 返回值:
# 1. return None: 捕获process_request和process_response抛出的异常, 将本次数据及请求丢弃.
# 2. return Response: 捕获process_request和process_response抛出的异常, 停止当连接将返回的response的结果丢给engine, 让engine继续进行处理
# 3. return Request: 捕获process_request和process_response抛出的异常, 停止当连接将返回的request对象的结果丢给engine, 让engine继续进行处理
# 应用场景: 捕获process_request和process_response抛出的异常, 停止当连接指定一个新的url使用Request进行切换
'''
from scrapy import Request
# 注意: url不能直接修改
request.url = 'https://www.cnblogs.com'
# request = Request(url='https://www.cnblogs.com', callback=spider.spider, )
return request
'''
pass
十一. 去重
1. 内置去重
'''
# 去重关键: request_seen
# 默认内置: 用的是集合去重
'''
# 去重类: RFPDupeFilter
from scrapy.dupefilters import RFPDupeFilter
# 去重关键方法: RFPDupeFilter下的request_seen方法下的request_fingerprint方法
from scrapy.utils.request import request_fingerprint
# fingerprint指纹本质就是使用md5加密, 但是是通过对?号后面参数进行排序实现
from scrapy.utils.request import request_fingerprint
from scrapy import Request
url1 = Request(url='https://www.baidu.com/?age=18&name=yang')
url2 = Request(url='https://www.baidu.com/?name=yang&age=18')
res1 = request_fingerprint(url1)
res2 = request_fingerprint(url2)
print(res1) # 5cee3ea26b02e17f343ebb143dbbb3cc69f406bc
print(res2) # 5cee3ea26b02e17f343ebb143dbbb3cc69f406bc
print(res1 is res2) # False
2. 布隆过滤器去重
1) 下载
- 先去这个网站下载
bitarray
这个依赖https://www.lfd.uci.edu/~gohlke/pythonlibs/#bitarray
- 直接安装会报错
error: Microsoft Visual C++ 14.0 is required. Get it with "Build Tools for Visual Studio": https://visualstudio.microsoft.com/downloads/
- 安装
wheel
文件, 防止我们主动安装报这样的错误pip3 install bitarray-1.1.0-cp36-cp36m-win_amd64.whl
pip3 install pybloom_live
wheel文件如何安装: https://www.cnblogs.com/justblue/p/13198202.html
2) 使用
'''
# 更高效方式实现: 布隆过滤器
作用:去重, 解决缓存穿透
缺点: 错误率存在, 不过取绝与数组的位数
# 自定义使用布隆过滤器去重流程
1. 新建.py文件重写过滤类, 并继承BaseDupeFilter
2. 重写request_seen方法
3. 配置文件中配置. 使用它默认加载
项目结构: first_scrapy/first_scrapy/bloom_filter.py/BloomFilter
DUPEFILTER_CLASS = 'first_scrapy.bloom_filter.BloomFilter' # 默认的去重规则帮我们去重,去重规则在内存中, settings.py配置文件中需要自己配置
'''
from scrapy.dupefilters import BaseDupeFilter
from pybloom_live import ScalableBloomFilter
class BloomFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter"""
def __init__(self, path=None, debug=False):
# ScalableBloomFilter可自动扩容的布隆过滤器
self.bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
super().__init__(path=None, debug=False)
def request_seen(self, request):
if request.url in self.bloom:
return True
self.bloom.add(request.url)
参考: https://www.cnblogs.com/xiaoyuanqujing/protected/articles/11969224.html